"""Nexus Session Persistence — durable conversation history.

The v1 Nexus kept conversations in a Python ``list`` that vanished on
every process restart. This module provides a SQLite-backed store so
Nexus conversations survive reboots while remaining fully local.

Schema:
    nexus_messages(id, role, content, timestamp, session_tag)

Design decisions:
- One table, one DB file (``data/nexus.db``).  Cheap, portable, sovereign.
- ``session_tag`` enables future per-operator sessions (#1090 deferred scope).
- Bounded history: ``MAX_MESSAGES`` rows per session tag.  Oldest are pruned
  automatically on insert.
- Thread-safe via SQLite WAL mode + module-level singleton.

Refs: #1090 (Nexus Epic — session persistence), architecture-v2.md §Data Layer
"""

from __future__ import annotations

import logging
import sqlite3
from contextlib import closing
from datetime import UTC, datetime
from pathlib import Path
from typing import TypedDict

logger = logging.getLogger(__name__)

# ── Defaults ─────────────────────────────────────────────────────────────────

_DEFAULT_DB_DIR = Path("data")
DB_PATH: Path = _DEFAULT_DB_DIR / "nexus.db"

MAX_MESSAGES = 500  # per session tag
DEFAULT_SESSION_TAG = "nexus"

# ── Schema ───────────────────────────────────────────────────────────────────

_SCHEMA = """\
CREATE TABLE IF NOT EXISTS nexus_messages (
    id          INTEGER PRIMARY KEY AUTOINCREMENT,
    role        TEXT    NOT NULL,
    content     TEXT    NOT NULL,
    timestamp   TEXT    NOT NULL,
    session_tag TEXT    NOT NULL DEFAULT 'nexus'
);
CREATE INDEX IF NOT EXISTS idx_nexus_session ON nexus_messages(session_tag);
CREATE INDEX IF NOT EXISTS idx_nexus_ts      ON nexus_messages(timestamp);
"""


# ── Typed dict for rows ──────────────────────────────────────────────────────


class NexusMessage(TypedDict):
    id: int
    role: str
    content: str
    timestamp: str
    session_tag: str


# ── Store ────────────────────────────────────────────────────────────────────


class NexusStore:
    """SQLite-backed persistence for Nexus conversations.

    Usage::

        store = NexusStore()          # uses module-level DB_PATH
        store.append("user", "hi")
        msgs = store.get_history()    # → list[NexusMessage]
        store.clear()                 # wipe session
    """

    def __init__(self, db_path: Path | None = None) -> None:
        self._db_path = db_path or DB_PATH
        self._conn: sqlite3.Connection | None = None

    # ── Connection management ─────────────────────────────────────────────

    def _get_conn(self) -> sqlite3.Connection:
        if self._conn is None:
            self._db_path.parent.mkdir(parents=True, exist_ok=True)
            self._conn = sqlite3.connect(
                str(self._db_path),
                check_same_thread=False,
            )
            self._conn.row_factory = sqlite3.Row
            self._conn.execute("PRAGMA journal_mode=WAL")
            self._conn.executescript(_SCHEMA)
        return self._conn

    def close(self) -> None:
        """Close the underlying connection (idempotent)."""
        if self._conn is not None:
            try:
                self._conn.close()
            except Exception:
                pass
            self._conn = None

    # ── Write ─────────────────────────────────────────────────────────────

    def append(
        self,
        role: str,
        content: str,
        *,
        timestamp: str | None = None,
        session_tag: str = DEFAULT_SESSION_TAG,
    ) -> int:
        """Insert a message and return its row id.

        Automatically prunes oldest messages when the session exceeds
        ``MAX_MESSAGES``.
        """
        ts = timestamp or datetime.now(UTC).strftime("%H:%M:%S")
        conn = self._get_conn()
        with closing(conn.cursor()) as cur:
            cur.execute(
                "INSERT INTO nexus_messages (role, content, timestamp, session_tag) "
                "VALUES (?, ?, ?, ?)",
                (role, content, ts, session_tag),
            )
            row_id: int = cur.lastrowid  # type: ignore[assignment]
        conn.commit()

        # Prune
        self._prune(session_tag)

        return row_id

    def _prune(self, session_tag: str) -> None:
        """Remove oldest rows that exceed MAX_MESSAGES for *session_tag*."""
        conn = self._get_conn()
        with closing(conn.cursor()) as cur:
            cur.execute(
                "SELECT COUNT(*) FROM nexus_messages WHERE session_tag = ?",
                (session_tag,),
            )
            count = cur.fetchone()[0]
            if count > MAX_MESSAGES:
                excess = count - MAX_MESSAGES
                cur.execute(
                    "DELETE FROM nexus_messages WHERE id IN ("
                    "  SELECT id FROM nexus_messages "
                    "  WHERE session_tag = ? ORDER BY id ASC LIMIT ?"
                    ")",
                    (session_tag, excess),
                )
                conn.commit()

    # ── Read ──────────────────────────────────────────────────────────────

    def get_history(
        self,
        session_tag: str = DEFAULT_SESSION_TAG,
        limit: int = 200,
    ) -> list[NexusMessage]:
        """Return the most recent *limit* messages for *session_tag*.

        Results are ordered oldest-first (ascending id).
        """
        conn = self._get_conn()
        with closing(conn.cursor()) as cur:
            cur.execute(
                "SELECT id, role, content, timestamp, session_tag "
                "FROM nexus_messages "
                "WHERE session_tag = ? "
                "ORDER BY id DESC LIMIT ?",
                (session_tag, limit),
            )
            rows = cur.fetchall()

        # Reverse to chronological order
        messages: list[NexusMessage] = [
            NexusMessage(
                id=r["id"],
                role=r["role"],
                content=r["content"],
                timestamp=r["timestamp"],
                session_tag=r["session_tag"],
            )
            for r in reversed(rows)
        ]
        return messages

    def message_count(self, session_tag: str = DEFAULT_SESSION_TAG) -> int:
        """Return total message count for *session_tag*."""
        conn = self._get_conn()
        with closing(conn.cursor()) as cur:
            cur.execute(
                "SELECT COUNT(*) FROM nexus_messages WHERE session_tag = ?",
                (session_tag,),
            )
            return cur.fetchone()[0]

    # ── Delete ────────────────────────────────────────────────────────────

    def clear(self, session_tag: str = DEFAULT_SESSION_TAG) -> int:
        """Delete all messages for *session_tag*. Returns count deleted."""
        conn = self._get_conn()
        with closing(conn.cursor()) as cur:
            cur.execute(
                "DELETE FROM nexus_messages WHERE session_tag = ?",
                (session_tag,),
            )
            deleted: int = cur.rowcount
        conn.commit()
        return deleted

    def clear_all(self) -> int:
        """Delete every message across all session tags."""
        conn = self._get_conn()
        with closing(conn.cursor()) as cur:
            cur.execute("DELETE FROM nexus_messages")
            deleted: int = cur.rowcount
        conn.commit()
        return deleted


# ── Module singleton ─────────────────────────────────────────────────────────

nexus_store = NexusStore()
