import json
import os
import sqlite3
from datetime import datetime
from pathlib import Path
from typing import Any, Optional

MEMORY_TYPES = {"general", "spec", "todo", "decision", "handoff", "progress"}
TODO_STATUSES = {"pending", "in_progress", "done"}

_db_path: Optional[Path] = None


def path_to_safe(project_path: str) -> str:
    """Mirror Anthropic's convention: /Users/foo/bar -> -Users-foo-bar"""
    return project_path.replace("/", "-")


def resolve_db_path(project_path: str) -> Path:
    safe = path_to_safe(project_path)
    return Path.home() / ".ctxbaton" / "projects" / safe / "memories.db"


def init(project_path: str) -> None:
    global _db_path
    _db_path = resolve_db_path(project_path)
    _db_path.parent.mkdir(parents=True, exist_ok=True)
    with _connect() as conn:
        conn.executescript("""
            CREATE TABLE IF NOT EXISTS memories (
                id          TEXT PRIMARY KEY,
                content     TEXT NOT NULL,
                summary     TEXT,
                type        TEXT NOT NULL DEFAULT 'general',
                status      TEXT,
                tags        TEXT NOT NULL DEFAULT '[]',
                agent       TEXT,
                session_id  TEXT,
                embedding   TEXT,
                created_at  TEXT NOT NULL,
                updated_at  TEXT NOT NULL
            );

            CREATE INDEX IF NOT EXISTS idx_type       ON memories(type);
            CREATE INDEX IF NOT EXISTS idx_status     ON memories(status);
            CREATE INDEX IF NOT EXISTS idx_agent      ON memories(agent);
            CREATE INDEX IF NOT EXISTS idx_session_id ON memories(session_id);
            CREATE INDEX IF NOT EXISTS idx_created_at ON memories(created_at DESC);

            CREATE TABLE IF NOT EXISTS messages (
                uuid         TEXT PRIMARY KEY,
                session_id   TEXT,
                parent_uuid  TEXT,
                role         TEXT NOT NULL,
                text         TEXT,
                tool_calls   TEXT NOT NULL DEFAULT '[]',
                tool_results TEXT NOT NULL DEFAULT '[]',
                timestamp    TEXT,
                cwd          TEXT,
                model        TEXT
            );

            CREATE INDEX IF NOT EXISTS idx_msg_session   ON messages(session_id);
            CREATE INDEX IF NOT EXISTS idx_msg_timestamp ON messages(timestamp);
            CREATE INDEX IF NOT EXISTS idx_msg_role      ON messages(role);

            CREATE VIRTUAL TABLE IF NOT EXISTS memories_fts USING fts5(
                content,
                summary,
                content='memories',
                content_rowid='rowid'
            );

            CREATE TRIGGER IF NOT EXISTS memories_fts_ai AFTER INSERT ON memories BEGIN
                INSERT INTO memories_fts(rowid, content, summary)
                VALUES (new.rowid, new.content, COALESCE(new.summary, ''));
            END;

            CREATE TRIGGER IF NOT EXISTS memories_fts_ad AFTER DELETE ON memories BEGIN
                INSERT INTO memories_fts(memories_fts, rowid, content, summary)
                VALUES ('delete', old.rowid, old.content, COALESCE(old.summary, ''));
            END;

            CREATE TRIGGER IF NOT EXISTS memories_fts_au AFTER UPDATE ON memories BEGIN
                INSERT INTO memories_fts(memories_fts, rowid, content, summary)
                VALUES ('delete', old.rowid, old.content, COALESCE(old.summary, ''));
                INSERT INTO memories_fts(rowid, content, summary)
                VALUES (new.rowid, new.content, COALESCE(new.summary, ''));
            END;

            CREATE TABLE IF NOT EXISTS sessions (
                id               TEXT PRIMARY KEY,
                agent            TEXT,
                project          TEXT,
                started_at       TEXT NOT NULL,
                last_seen        TEXT NOT NULL,
                transcript_path  TEXT
            );

            CREATE INDEX IF NOT EXISTS idx_sessions_agent    ON sessions(agent);
            CREATE INDEX IF NOT EXISTS idx_sessions_project  ON sessions(project);
            CREATE INDEX IF NOT EXISTS idx_sessions_last_seen ON sessions(last_seen DESC);

            CREATE TABLE IF NOT EXISTS transcript_cursors (
                transcript_path  TEXT PRIMARY KEY,
                offset           INTEGER NOT NULL DEFAULT 0,
                updated_at       TEXT NOT NULL
            );
        """)
        # Populate FTS index from any existing memories not yet indexed
        conn.execute("INSERT INTO memories_fts(memories_fts) VALUES('rebuild')")


def _connect() -> sqlite3.Connection:
    if not _db_path:
        raise RuntimeError("DB not initialised — call db.init(project_path) first")
    conn = sqlite3.connect(_db_path)
    conn.row_factory = sqlite3.Row
    conn.execute("PRAGMA journal_mode=WAL")
    conn.execute("PRAGMA foreign_keys=ON")
    return conn


def _row_to_dict(row: sqlite3.Row) -> dict[str, Any]:
    d = dict(row)
    d["tags"] = json.loads(d["tags"] or "[]")
    d.pop("embedding", None)
    return d


def insert_memory(
    id: str,
    content: str,
    summary: Optional[str] = None,
    type: str = "general",
    status: Optional[str] = None,
    tags: Optional[list[str]] = None,
    agent: Optional[str] = None,
    session_id: Optional[str] = None,
) -> dict[str, Any]:
    now = datetime.utcnow().isoformat()
    with _connect() as conn:
        conn.execute(
            """
            INSERT INTO memories
                (id, content, summary, type, status, tags, agent, session_id, created_at, updated_at)
            VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
            """,
            (id, content, summary, type, status, json.dumps(tags or []), agent, session_id, now, now),
        )
    return get_memory(id)  # type: ignore[return-value]


def get_memory(id: str) -> Optional[dict[str, Any]]:
    with _connect() as conn:
        row = conn.execute("SELECT * FROM memories WHERE id = ?", (id,)).fetchone()
    return _row_to_dict(row) if row else None


def list_memories(
    type: Optional[str] = None,
    status: Optional[str] = None,
    tags: Optional[list[str]] = None,
    agent: Optional[str] = None,
    session_id: Optional[str] = None,
    limit: int = 30,
) -> list[dict[str, Any]]:
    conditions: list[str] = []
    params: list[Any] = []

    if type:        conditions.append("type = ?");        params.append(type)
    if status:      conditions.append("status = ?");      params.append(status)
    if agent:       conditions.append("agent = ?");       params.append(agent)
    if session_id:  conditions.append("session_id = ?");  params.append(session_id)

    where = f"WHERE {' AND '.join(conditions)}" if conditions else ""
    params.append(limit)

    with _connect() as conn:
        rows = conn.execute(
            f"SELECT * FROM memories {where} ORDER BY created_at DESC LIMIT ?", params
        ).fetchall()

    results = [_row_to_dict(r) for r in rows]
    if tags:
        results = [m for m in results if all(t in m["tags"] for t in tags)]
    return results


def _fts_query(q: str) -> str:
    """Convert a plain search string to an FTS5 prefix query (each token gets *)."""
    tokens = q.strip().split()
    return " ".join(t + "*" for t in tokens if t)


def search_memories_fts(
    query: str,
    type: Optional[str] = None,
    limit: int = 20,
) -> list[dict[str, Any]]:
    """Full-text search over memories using SQLite FTS5."""
    conditions: list[str] = []
    params: list[Any] = [_fts_query(query)]
    if type:
        conditions.append("m.type = ?")
        params.append(type)
    where = f"AND {' AND '.join(conditions)}" if conditions else ""
    params.append(limit)
    with _connect() as conn:
        rows = conn.execute(
            f"""
            SELECT m.*
            FROM memories_fts
            JOIN memories m ON m.rowid = memories_fts.rowid
            WHERE memories_fts MATCH ?
            {where}
            ORDER BY rank
            LIMIT ?
            """,
            params,
        ).fetchall()
    return [_row_to_dict(r) for r in rows]


def update_memory(
    id: str,
    content: Optional[str] = None,
    summary: Optional[str] = None,
    type: Optional[str] = None,
    status: Optional[str] = None,
    tags: Optional[list[str]] = None,
) -> Optional[dict[str, Any]]:
    existing = get_memory(id)
    if not existing:
        return None
    with _connect() as conn:
        conn.execute(
            """
            UPDATE memories SET
                content = ?, summary = ?, type = ?, status = ?, tags = ?, updated_at = ?
            WHERE id = ?
            """,
            (
                content if content is not None else existing["content"],
                summary if summary is not None else existing["summary"],
                type if type is not None else existing["type"],
                status if status is not None else existing["status"],
                json.dumps(tags) if tags is not None else json.dumps(existing["tags"]),
                datetime.utcnow().isoformat(),
                id,
            ),
        )
    return get_memory(id)


def delete_memory(id: str) -> bool:
    with _connect() as conn:
        result = conn.execute("DELETE FROM memories WHERE id = ?", (id,))
    return result.rowcount > 0


# ---------------------------------------------------------------------------
# Messages (transcript bronze layer)
# ---------------------------------------------------------------------------

def insert_message(
    uuid: str,
    role: str,
    session_id: Optional[str] = None,
    parent_uuid: Optional[str] = None,
    text: Optional[str] = None,
    tool_calls: Optional[list] = None,
    tool_results: Optional[list] = None,
    timestamp: Optional[str] = None,
    cwd: Optional[str] = None,
    model: Optional[str] = None,
) -> None:
    with _connect() as conn:
        conn.execute(
            """
            INSERT OR IGNORE INTO messages
                (uuid, session_id, parent_uuid, role, text, tool_calls, tool_results, timestamp, cwd, model)
            VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
            """,
            (
                uuid,
                session_id,
                parent_uuid,
                role,
                text,
                json.dumps(tool_calls or []),
                json.dumps(tool_results or []),
                timestamp,
                cwd,
                model,
            ),
        )


def list_messages(
    session_id: Optional[str] = None,
    role: Optional[str] = None,
    limit: int = 50,
    offset: int = 0,
) -> list[dict[str, Any]]:
    conditions: list[str] = []
    params: list[Any] = []
    if session_id:
        conditions.append("session_id = ?")
        params.append(session_id)
    if role:
        conditions.append("role = ?")
        params.append(role)
    where = f"WHERE {' AND '.join(conditions)}" if conditions else ""
    params += [limit, offset]
    with _connect() as conn:
        rows = conn.execute(
            f"SELECT * FROM messages {where} ORDER BY timestamp ASC LIMIT ? OFFSET ?",
            params,
        ).fetchall()
    results = []
    for row in rows:
        d = dict(row)
        d["tool_calls"]   = json.loads(d["tool_calls"]   or "[]")
        d["tool_results"] = json.loads(d["tool_results"] or "[]")
        results.append(d)
    return results


def count_messages(session_id: Optional[str] = None) -> int:
    params: list[Any] = []
    where = ""
    if session_id:
        where = "WHERE session_id = ?"
        params.append(session_id)
    with _connect() as conn:
        row = conn.execute(f"SELECT COUNT(*) FROM messages {where}", params).fetchone()
    return row[0] if row else 0


def search_messages_text(
    query: str,
    session_id: Optional[str] = None,
    limit: int = 20,
) -> list[dict[str, Any]]:
    conditions = ["text LIKE ?"]
    params: list[Any] = [f"%{query}%"]
    if session_id:
        conditions.append("session_id = ?")
        params.append(session_id)
    where = f"WHERE {' AND '.join(conditions)}"
    params.append(limit)
    with _connect() as conn:
        rows = conn.execute(
            f"SELECT * FROM messages {where} ORDER BY timestamp ASC LIMIT ?", params
        ).fetchall()
    results = []
    for row in rows:
        d = dict(row)
        d["tool_calls"]   = json.loads(d["tool_calls"]   or "[]")
        d["tool_results"] = json.loads(d["tool_results"] or "[]")
        results.append(d)
    return results


# ---------------------------------------------------------------------------
# Sessions
# ---------------------------------------------------------------------------

def upsert_session(
    id: str,
    agent: str,
    project: str,
    transcript_path: Optional[str] = None,
    started_at: Optional[str] = None,
) -> None:
    now = datetime.utcnow().isoformat()
    sa  = started_at or now
    with _connect() as conn:
        conn.execute(
            """
            INSERT OR IGNORE INTO sessions
                (id, agent, project, started_at, last_seen, transcript_path)
            VALUES (?, ?, ?, ?, ?, ?)
            """,
            (id, agent, project, sa, now, transcript_path),
        )
        conn.execute(
            """
            UPDATE sessions SET
                agent = ?, project = ?, last_seen = ?,
                transcript_path = COALESCE(?, transcript_path)
            WHERE id = ?
            """,
            (agent, project, now, transcript_path, id),
        )


def get_session(id: str) -> Optional[dict[str, Any]]:
    with _connect() as conn:
        row = conn.execute("SELECT * FROM sessions WHERE id = ?", (id,)).fetchone()
    return dict(row) if row else None


def list_sessions(
    agent: Optional[str] = None,
    project: Optional[str] = None,
    limit: int = 10,
) -> list[dict[str, Any]]:
    conditions: list[str] = []
    params: list[Any] = []
    if agent:
        conditions.append("agent = ?")
        params.append(agent)
    if project:
        conditions.append("project = ?")
        params.append(project)
    where = f"WHERE {' AND '.join(conditions)}" if conditions else ""
    params.append(limit)
    with _connect() as conn:
        rows = conn.execute(
            f"SELECT * FROM sessions {where} ORDER BY last_seen DESC LIMIT ?", params
        ).fetchall()
    return [dict(r) for r in rows]


# ---------------------------------------------------------------------------
# Transcript cursors
# ---------------------------------------------------------------------------

def get_transcript_cursor(transcript_path: str) -> int:
    with _connect() as conn:
        row = conn.execute(
            "SELECT offset FROM transcript_cursors WHERE transcript_path = ?",
            (transcript_path,),
        ).fetchone()
    return row[0] if row else 0


def set_transcript_cursor(transcript_path: str, offset: int) -> None:
    now = datetime.utcnow().isoformat()
    with _connect() as conn:
        conn.execute(
            """
            INSERT INTO transcript_cursors (transcript_path, offset, updated_at)
            VALUES (?, ?, ?)
            ON CONFLICT(transcript_path) DO UPDATE SET offset = excluded.offset, updated_at = excluded.updated_at
            """,
            (transcript_path, offset, now),
        )
