"""SQLite storage layer: schema init, idempotent upserts, and read queries.

All writes are UPSERTs keyed on (source, source_id), so re-running a sync over
an overlapping id range is a no-op rather than a duplicate. Reads back the
Adolf query CLI (`cli.py query ...`).
"""
import os
import sqlite3
from datetime import datetime, timezone

SCHEMA_PATH = os.path.join(os.path.dirname(os.path.dirname(__file__)), "schema.sql")


def _now_iso():
    return datetime.now(tz=timezone.utc).isoformat()


def connect(db_path):
    os.makedirs(os.path.dirname(os.path.abspath(db_path)), exist_ok=True)
    conn = sqlite3.connect(db_path)
    conn.row_factory = sqlite3.Row
    return conn


def init_db(conn):
    with open(SCHEMA_PATH) as f:
        conn.executescript(f.read())
    conn.commit()


# --- writes ---------------------------------------------------------------

def upsert_entries(conn, rows, source="moodtracker"):
    now = _now_iso()
    n = 0
    for r in rows:
        conn.execute(
            """
            INSERT INTO mood_entries
                (source, source_id, ts, mood, tags, note, affirmation, ingested_at)
            VALUES (?,?,?,?,?,?,?,?)
            ON CONFLICT(source, source_id) DO UPDATE SET
                ts=excluded.ts,
                mood=excluded.mood,
                tags=excluded.tags,
                note=excluded.note,
                affirmation=excluded.affirmation,
                ingested_at=excluded.ingested_at
            """,
            (
                source, r["id"], r["ts"], r["mood"], r.get("tags", "[]"),
                r.get("note"), r.get("affirmation"), now,
            ),
        )
        n += 1
    conn.commit()
    return n


# --- sync cursor & run audit ---------------------------------------------

def get_last_synced_id(conn, stream_key):
    row = conn.execute(
        "SELECT last_synced_id FROM sync_state WHERE stream_key=?", (stream_key,)
    ).fetchone()
    return row["last_synced_id"] if row else 0


def set_sync_state(conn, stream_key, last_synced_id, status="ok", error=None):
    conn.execute(
        """
        INSERT INTO sync_state (stream_key, last_synced_id, last_run_at, last_status, last_error)
        VALUES (?,?,?,?,?)
        ON CONFLICT(stream_key) DO UPDATE SET
            last_synced_id=MAX(sync_state.last_synced_id, excluded.last_synced_id),
            last_run_at=excluded.last_run_at,
            last_status=excluded.last_status,
            last_error=excluded.last_error
        """,
        (stream_key, last_synced_id, _now_iso(), status, error),
    )
    conn.commit()


def start_run(conn):
    cur = conn.execute(
        "INSERT INTO ingest_runs (started_at, status) VALUES (?, 'running')", (_now_iso(),)
    )
    conn.commit()
    return cur.lastrowid


def finish_run(conn, run_id, status, entries=0, error=None):
    conn.execute(
        """UPDATE ingest_runs SET finished_at=?, status=?, entries_upserted=?, error=?
           WHERE id=?""",
        (_now_iso(), status, entries, error, run_id),
    )
    conn.commit()


# --- reads (Adolf query tool + reports) -----------------------------------

def summary(conn):
    """Compact snapshot: count, coverage, freshness, last run."""
    out = {}
    out["entries"] = conn.execute("SELECT COUNT(*) c FROM mood_entries").fetchone()["c"]
    span = conn.execute("SELECT MIN(ts) a, MAX(ts) b FROM mood_entries").fetchone()
    out["coverage"] = {"earliest": span["a"], "latest": span["b"]}
    out["avg_mood_all_time"] = conn.execute(
        "SELECT ROUND(AVG(mood), 2) a FROM mood_entries"
    ).fetchone()["a"]
    out["sync_state"] = [dict(r) for r in conn.execute(
        "SELECT stream_key, last_synced_id, last_run_at, last_status FROM sync_state"
    ).fetchall()]
    last = conn.execute(
        "SELECT started_at, finished_at, status, entries_upserted FROM ingest_runs "
        "ORDER BY id DESC LIMIT 1"
    ).fetchone()
    out["last_run"] = dict(last) if last else None
    return out


def recent_entries(conn, days=30, limit=200):
    return [dict(r) for r in conn.execute(
        """SELECT source_id, ts, mood, tags, note, affirmation
           FROM mood_entries WHERE ts >= datetime('now', ?)
           ORDER BY ts DESC LIMIT ?""",
        (f"-{int(days)} days", int(limit)),
    ).fetchall()]


def daily_mood(conn, days=30):
    """Average mood and entry count per calendar day (report #1: mood over time)."""
    return [dict(r) for r in conn.execute(
        """SELECT date(ts) AS day, ROUND(AVG(mood), 2) AS avg_mood, COUNT(*) AS entries
           FROM mood_entries WHERE ts >= datetime('now', ?)
           GROUP BY day ORDER BY day DESC""",
        (f"-{int(days)} days",),
    ).fetchall()]


def tag_breakdown(conn, days=90, min_count=2):
    """Average mood per tag (simple correlation: which tags co-occur with
    higher/lower mood). Tags are stored as a JSON array per entry; this
    unpacks them in Python since SQLite has no native JSON array explode
    without the (not always compiled-in) json1 table-valued functions."""
    import json
    rows = conn.execute(
        "SELECT mood, tags FROM mood_entries WHERE ts >= datetime('now', ?)",
        (f"-{int(days)} days",),
    ).fetchall()
    by_tag = {}
    for r in rows:
        try:
            tags = json.loads(r["tags"]) or []
        except (TypeError, ValueError):
            tags = []
        for t in tags:
            by_tag.setdefault(t, []).append(r["mood"])
    out = [
        {"tag": t, "avg_mood": round(sum(v) / len(v), 2), "count": len(v)}
        for t, v in by_tag.items()
        if len(v) >= min_count
    ]
    out.sort(key=lambda x: x["avg_mood"])
    return out


# --- correlation hook (generic, no other Agap service wired) --------------

def correlate_with_series(conn, external_daily, days=90):
    """Pearson correlation between daily average mood and an arbitrary
    externally-supplied daily series.

    `external_daily` is a dict {'YYYY-MM-DD': float}. This is a deliberate
    seam for future cross-source correlation (e.g. googlefit sleep/steps) —
    it takes plain data, not a live connection to another service's DB, so
    wiring a second source later is a one-line change at the call site
    (build the dict from that source's own query CLI) and never requires
    this service to know about the other service's schema or container.

    Returns {'n': overlap_days, 'r': pearson_r_or_None, 'points': [...]}."""
    mood_by_day = {
        r["day"]: r["avg_mood"] for r in daily_mood(conn, days=days)
    }
    common_days = sorted(set(mood_by_day) & set(external_daily))
    xs = [mood_by_day[d] for d in common_days]
    ys = [external_daily[d] for d in common_days]
    n = len(xs)
    if n < 2:
        return {"n": n, "r": None, "points": list(zip(common_days, xs, ys))}
    mx, my = sum(xs) / n, sum(ys) / n
    cov = sum((x - mx) * (y - my) for x, y in zip(xs, ys))
    varx = sum((x - mx) ** 2 for x in xs)
    vary = sum((y - my) ** 2 for y in ys)
    r = cov / (varx ** 0.5 * vary ** 0.5) if varx > 0 and vary > 0 else None
    return {
        "n": n,
        "r": round(r, 3) if r is not None else None,
        "points": [{"day": d, "mood": x, "value": y} for d, x, y in zip(common_days, xs, ys)],
    }
