"""MemoryIndex — SQLite + FTS5 structured memory store."""

import json
import sqlite3
import time
from typing import Optional

from neo_cortex.models import CompactMemory, MemoryRecord, StructuredFields


_SCHEMA = """
CREATE TABLE IF NOT EXISTS memories (
    id TEXT PRIMARY KEY,
    session_id TEXT NOT NULL,
    timestamp REAL NOT NULL,
    turn_number INTEGER DEFAULT 0,
    project TEXT DEFAULT 'general',
    topic TEXT DEFAULT 'unknown',
    activity TEXT DEFAULT 'discussion',
    memory_type TEXT DEFAULT 'episodic',
    energy REAL DEFAULT 1.0,
    model TEXT DEFAULT 'opus',
    source TEXT DEFAULT 'neo-cortex',
    title TEXT,
    summary TEXT,
    facts TEXT,
    concepts TEXT,
    files_touched TEXT,
    question TEXT,
    answer_preview TEXT,
    document TEXT
);

CREATE VIRTUAL TABLE IF NOT EXISTS memories_fts USING fts5(
    title, summary, facts, question, answer_preview,
    content=memories, content_rowid=rowid
);

CREATE INDEX IF NOT EXISTS idx_mem_timestamp ON memories(timestamp DESC);
CREATE INDEX IF NOT EXISTS idx_mem_project ON memories(project);
CREATE INDEX IF NOT EXISTS idx_mem_activity ON memories(activity);
CREATE INDEX IF NOT EXISTS idx_mem_session ON memories(session_id);

CREATE TABLE IF NOT EXISTS runtime_days (
    day TEXT PRIMARY KEY
);

CREATE TABLE IF NOT EXISTS pending_merges (
    id INTEGER PRIMARY KEY AUTOINCREMENT,
    memory_id_a TEXT NOT NULL,
    memory_id_b TEXT NOT NULL,
    similarity REAL NOT NULL,
    created_at REAL NOT NULL,
    processed INTEGER DEFAULT 0,
    UNIQUE(memory_id_a, memory_id_b)
);

CREATE TRIGGER IF NOT EXISTS memories_ai AFTER INSERT ON memories BEGIN
    INSERT INTO memories_fts(rowid, title, summary, facts, question, answer_preview)
    VALUES (new.rowid, new.title, new.summary, new.facts, new.question, new.answer_preview);
END;

CREATE TRIGGER IF NOT EXISTS memories_ad AFTER DELETE ON memories BEGIN
    INSERT INTO memories_fts(memories_fts, rowid, title, summary, facts, question, answer_preview)
    VALUES ('delete', old.rowid, old.title, old.summary, old.facts, old.question, old.answer_preview);
END;

CREATE TRIGGER IF NOT EXISTS memories_au AFTER UPDATE ON memories BEGIN
    INSERT INTO memories_fts(memories_fts, rowid, title, summary, facts, question, answer_preview)
    VALUES ('delete', old.rowid, old.title, old.summary, old.facts, old.question, old.answer_preview);
    INSERT INTO memories_fts(rowid, title, summary, facts, question, answer_preview)
    VALUES (new.rowid, new.title, new.summary, new.facts, new.question, new.answer_preview);
END;
"""


class MemoryIndex:
    def __init__(self, db_path: str):
        self._conn = sqlite3.connect(db_path, check_same_thread=False)
        self._conn.row_factory = sqlite3.Row
        self._conn.executescript(_SCHEMA)
        self._migrate()
        self._current_rd: int = 0  # set by register_runtime_day()

    @property
    def conn(self) -> sqlite3.Connection:
        """Expose connection for shared use by DedupStore and ConceptGraph."""
        return self._conn

    def register_runtime_day(self) -> int:
        """Register today as an active runtime day. Returns total count."""
        import datetime
        today = datetime.date.today().isoformat()
        self._conn.execute("INSERT OR IGNORE INTO runtime_days (day) VALUES (?)", (today,))
        self._conn.commit()
        self._current_rd = self.get_runtime_day_count()
        return self._current_rd

    def get_runtime_day_count(self) -> int:
        """Total number of distinct runtime days."""
        row = self._conn.execute("SELECT COUNT(*) FROM runtime_days").fetchone()
        return row[0]

    def insert(self, record: MemoryRecord, structured: Optional[StructuredFields] = None):
        self._conn.execute(
            """INSERT OR REPLACE INTO memories
            (id, session_id, timestamp, turn_number, project, topic, activity,
             memory_type, energy, model, source, title, summary, facts, concepts,
             files_touched, question, answer_preview, document, observation_level,
             created_at_rd, last_accessed_rd, evidence_ids)
            VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)""",
            (
                record.id,
                record.session_id,
                record.timestamp,
                record.turn_number,
                record.project,
                record.topic,
                record.activity.value if hasattr(record.activity, "value") else record.activity,
                record.memory_type.value if hasattr(record.memory_type, "value") else record.memory_type,
                record.energy,
                record.model,
                record.source,
                structured.title if structured else None,
                structured.summary if structured else None,
                json.dumps(structured.facts) if structured else None,
                json.dumps(structured.concepts) if structured else None,
                json.dumps(structured.files_touched) if structured else None,
                record.question,
                record.answer_preview,
                record.document,
                record.observation_level.value if hasattr(record.observation_level, "value") else record.observation_level,
                self._current_rd,
                self._current_rd,
                ",".join(record.evidence_ids) if record.evidence_ids else "",
            ),
        )
        self._conn.commit()

    def get_by_id(self, id: str) -> Optional[MemoryRecord]:
        row = self._conn.execute("SELECT * FROM memories WHERE id = ?", (id,)).fetchone()
        if not row:
            return None
        return self._row_to_record(row)

    def get_by_ids(self, ids: list[str]) -> list[MemoryRecord]:
        if not ids:
            return []
        placeholders = ",".join("?" for _ in ids)
        rows = self._conn.execute(
            f"SELECT * FROM memories WHERE id IN ({placeholders})", ids
        ).fetchall()
        return [self._row_to_record(r) for r in rows]

    def search_fts(self, query: str, project: Optional[str] = None,
                   activity: Optional[str] = None, n: int = 10) -> list[CompactMemory]:
        if query.strip():
            safe_query = query.replace('"', '""')
            sql = """SELECT m.id, m.title, m.project, m.topic, m.activity, m.timestamp, m.energy
                     FROM memories_fts f
                     JOIN memories m ON f.rowid = m.rowid
                     WHERE memories_fts MATCH ?"""
            params: list = [f'"{safe_query}"']
            if project:
                sql += " AND m.project = ?"
                params.append(project)
            if activity:
                sql += " AND m.activity = ?"
                params.append(activity)
            sql += " ORDER BY rank LIMIT ?"
            params.append(n)
        else:
            sql = """SELECT id, title, project, topic, activity, timestamp, energy
                     FROM memories WHERE 1=1"""
            params = []
            if project:
                sql += " AND project = ?"
                params.append(project)
            if activity:
                sql += " AND activity = ?"
                params.append(activity)
            sql += " ORDER BY timestamp DESC LIMIT ?"
            params.append(n)

        rows = self._conn.execute(sql, params).fetchall()
        return [
            CompactMemory(
                id=r["id"],
                title=r["title"],
                project=r["project"],
                topic=r["topic"],
                activity=r["activity"],
                timestamp=r["timestamp"],
                energy=r["energy"],
            )
            for r in rows
        ]

    def timeline(self, n: int = 10, project: Optional[str] = None) -> list[CompactMemory]:
        sql = """SELECT id, title, project, topic, activity, timestamp, energy
                 FROM memories WHERE 1=1"""
        params: list = []
        if project:
            sql += " AND project = ?"
            params.append(project)
        sql += " ORDER BY timestamp DESC LIMIT ?"
        params.append(n)
        rows = self._conn.execute(sql, params).fetchall()
        return [
            CompactMemory(
                id=r["id"],
                title=r["title"],
                project=r["project"],
                topic=r["topic"],
                activity=r["activity"],
                timestamp=r["timestamp"],
                energy=r["energy"],
            )
            for r in rows
        ]

    def stats(self, dream_count: int = 0) -> dict:
        row = self._conn.execute(
            """SELECT COUNT(*) as total, COUNT(DISTINCT session_id) as sessions,
                      COALESCE(AVG(energy), 0) as avg_energy
               FROM memories"""
        ).fetchone()
        return {
            "total_memories": row["total"],
            "sessions": row["sessions"],
            "avg_energy": row["avg_energy"],
            "dream_count": dream_count,
        }

    def update_energy(self, id: str, energy: float):
        self._conn.execute("UPDATE memories SET energy = ? WHERE id = ?", (energy, id))
        self._conn.commit()

    def update_stability(self, id: str, stability: float):
        self._conn.execute("UPDATE memories SET stability = ? WHERE id = ?", (stability, id))
        self._conn.commit()

    def get_stability(self, id: str) -> float:
        """Get current stability for a memory. Returns 1.0 if not found."""
        row = self._conn.execute(
            "SELECT stability FROM memories WHERE id = ?", (id,)
        ).fetchone()
        return row[0] if row and row[0] is not None else 1.0

    def update_last_accessed(self, id: str, timestamp: float, runtime_day: int | None = None):
        if runtime_day is not None:
            self._conn.execute(
                "UPDATE memories SET last_accessed = ?, access_count = access_count + 1, last_accessed_rd = ? WHERE id = ?",
                (timestamp, runtime_day, id),
            )
        else:
            self._conn.execute(
                "UPDATE memories SET last_accessed = ?, access_count = access_count + 1 WHERE id = ?",
                (timestamp, id),
            )
        self._conn.commit()

    def get_metacognitive_memories(
        self, min_energy: float = 0.7, min_stability: float = 0.5, limit: int = 50
    ) -> list[dict]:
        """Fetch mature metacognitive memories."""
        rows = self._conn.execute(
            "SELECT id, summary, energy, stability, observation_level, evidence_ids, concepts "
            "FROM memories WHERE observation_level = 'metacognitive' "
            "AND energy >= ? AND stability >= ? "
            "ORDER BY (energy * stability) DESC LIMIT ?",
            (min_energy, min_stability, limit),
        ).fetchall()
        return [
            {
                "id": r["id"], "summary": r["summary"], "energy": r["energy"],
                "stability": r["stability"], "observation_level": r["observation_level"],
                "evidence_ids": r["evidence_ids"] or "", "concepts": r["concepts"] or "",
            }
            for r in rows
        ]

    def get_recent_summaries(self, limit: int = 20) -> list[dict]:
        """Fetch recent memory summaries for reflection."""
        rows = self._conn.execute(
            "SELECT id, summary, timestamp FROM memories "
            "WHERE summary IS NOT NULL ORDER BY timestamp DESC LIMIT ?",
            (limit,),
        ).fetchall()
        return [{"id": r["id"], "summary": r["summary"], "timestamp": r["timestamp"]} for r in rows]

    def get_all_for_dream(self) -> list[dict]:
        """Get id, timestamp, energy, stability, runtime days for dream cycle."""
        rows = self._conn.execute(
            "SELECT id, timestamp, energy, stability, last_accessed, access_count, created_at_rd, last_accessed_rd FROM memories"
        ).fetchall()
        return [
            {
                "id": r["id"],
                "timestamp": r["timestamp"],
                "energy": r["energy"],
                "stability": r["stability"] or 1.0,
                "last_accessed": r["last_accessed"] or r["timestamp"],
                "access_count": r["access_count"] or 0,
                "created_at_rd": r["created_at_rd"] or 0,
                "last_accessed_rd": r["last_accessed_rd"] or 0,
            }
            for r in rows
        ]

    def _migrate(self):
        """Add columns for Ebbinghaus decay if not present."""
        cols = {r[1] for r in self._conn.execute("PRAGMA table_info(memories)").fetchall()}
        if "stability" not in cols:
            self._conn.execute("ALTER TABLE memories ADD COLUMN stability REAL DEFAULT 1.0")
        if "last_accessed" not in cols:
            self._conn.execute("ALTER TABLE memories ADD COLUMN last_accessed REAL")
        if "access_count" not in cols:
            self._conn.execute("ALTER TABLE memories ADD COLUMN access_count INTEGER DEFAULT 0")
        if "observation_level" not in cols:
            self._conn.execute("ALTER TABLE memories ADD COLUMN observation_level TEXT DEFAULT 'direct'")
        if "created_at_rd" not in cols:
            self._conn.execute("ALTER TABLE memories ADD COLUMN created_at_rd INTEGER DEFAULT 0")
        if "last_accessed_rd" not in cols:
            self._conn.execute("ALTER TABLE memories ADD COLUMN last_accessed_rd INTEGER DEFAULT 0")
        if "evidence_ids" not in cols:
            self._conn.execute("ALTER TABLE memories ADD COLUMN evidence_ids TEXT DEFAULT ''")
        self._conn.commit()

    def update(self, record: MemoryRecord, structured: Optional[StructuredFields] = None) -> None:
        """Update an existing memory. Uses INSERT OR REPLACE — FTS5 triggers fire automatically."""
        self.insert(record, structured)

    def delete(self, memory_id: str) -> None:
        """Delete a memory. FTS5 trigger AFTER DELETE fires automatically."""
        self._conn.execute("DELETE FROM memories WHERE id = ?", (memory_id,))
        self._conn.commit()

    def clear(self) -> int:
        """Delete all memories. FTS5 triggers fire automatically."""
        n = self.count()
        self._conn.execute("DELETE FROM memories")
        self._conn.commit()
        return n

    def count(self) -> int:
        return self._conn.execute("SELECT COUNT(*) FROM memories").fetchone()[0]

    def get_structured(self, id: str) -> Optional[StructuredFields]:
        row = self._conn.execute(
            "SELECT title, summary, facts, concepts, files_touched FROM memories WHERE id = ?",
            (id,),
        ).fetchone()
        if not row:
            return None
        return StructuredFields(
            title=row["title"],
            summary=row["summary"],
            facts=json.loads(row["facts"]) if row["facts"] else [],
            concepts=json.loads(row["concepts"]) if row["concepts"] else [],
            files_touched=json.loads(row["files_touched"]) if row["files_touched"] else [],
        )

    def search_by_concept(self, concept: str, n: int = 10) -> list[CompactMemory]:
        """Find memories containing a concept (exact or partial match)."""
        rows = self._conn.execute(
            """SELECT id, title, project, topic, activity, timestamp, energy
               FROM memories WHERE concepts LIKE ?
               ORDER BY timestamp DESC LIMIT ?""",
            (f'%"{concept}%', n),
        ).fetchall()
        return [
            CompactMemory(
                id=r["id"], title=r["title"], project=r["project"],
                topic=r["topic"], activity=r["activity"],
                timestamp=r["timestamp"], energy=r["energy"],
            )
            for r in rows
        ]

    def search_by_file(self, file_path: str, n: int = 10) -> list[CompactMemory]:
        """Find memories that touch a file."""
        rows = self._conn.execute(
            """SELECT id, title, project, topic, activity, timestamp, energy
               FROM memories WHERE files_touched LIKE ?
               ORDER BY timestamp DESC LIMIT ?""",
            (f"%{file_path}%", n),
        ).fetchall()
        return [
            CompactMemory(
                id=r["id"], title=r["title"], project=r["project"],
                topic=r["topic"], activity=r["activity"],
                timestamp=r["timestamp"], energy=r["energy"],
            )
            for r in rows
        ]

    def get_all_concepts(self) -> dict[str, int]:
        """Return all unique concepts with their frequency count."""
        rows = self._conn.execute(
            "SELECT concepts FROM memories WHERE concepts IS NOT NULL"
        ).fetchall()
        counts: dict[str, int] = {}
        for row in rows:
            try:
                concepts = json.loads(row["concepts"])
                for c in concepts:
                    counts[c] = counts.get(c, 0) + 1
            except (json.JSONDecodeError, TypeError):
                pass
        return counts

    def get_related_by_concepts(self, memory_id: str, n: int = 5) -> list[CompactMemory]:
        """Find memories sharing at least 1 concept with the given memory."""
        row = self._conn.execute(
            "SELECT concepts FROM memories WHERE id = ?", (memory_id,)
        ).fetchone()
        if not row or not row["concepts"]:
            return []
        try:
            concepts = json.loads(row["concepts"])
        except (json.JSONDecodeError, TypeError):
            return []
        if not concepts:
            return []

        # Build OR conditions for each concept
        conditions = []
        params: list = []
        for c in concepts:
            conditions.append("concepts LIKE ?")
            params.append(f'%"{c}"%')

        where = " OR ".join(conditions)
        params.append(memory_id)
        params.append(n)
        rows = self._conn.execute(
            f"""SELECT id, title, project, topic, activity, timestamp, energy
                FROM memories WHERE ({where}) AND id != ?
                ORDER BY timestamp DESC LIMIT ?""",
            params,
        ).fetchall()
        return [
            CompactMemory(
                id=r["id"], title=r["title"], project=r["project"],
                topic=r["topic"], activity=r["activity"],
                timestamp=r["timestamp"], energy=r["energy"],
            )
            for r in rows
        ]

    # ── pending_merges CRUD ─────────────────────────────────────────

    def add_pending_merge(self, memory_id_a: str, memory_id_b: str, similarity: float) -> None:
        """Record a pair of similar memories for later consolidation."""
        a, b = sorted([memory_id_a, memory_id_b])
        self._conn.execute(
            "INSERT OR IGNORE INTO pending_merges (memory_id_a, memory_id_b, similarity, created_at) VALUES (?, ?, ?, ?)",
            (a, b, similarity, time.time()),
        )
        self._conn.commit()

    def get_pending_merges(self) -> list[tuple[str, str, float, int]]:
        """Return unprocessed merge pairs as (id_a, id_b, similarity, row_id)."""
        rows = self._conn.execute(
            "SELECT id, memory_id_a, memory_id_b, similarity FROM pending_merges WHERE processed = 0"
        ).fetchall()
        return [(r[1], r[2], r[3], r[0]) for r in rows]

    def mark_merges_processed(self, row_ids: list[int]) -> None:
        """Mark merge rows as processed."""
        if not row_ids:
            return
        placeholders = ",".join("?" for _ in row_ids)
        self._conn.execute(
            f"UPDATE pending_merges SET processed = 1 WHERE id IN ({placeholders})", row_ids
        )
        self._conn.commit()

    def clear_pending_merges(self) -> int:
        """Delete all pending merges. Returns count deleted."""
        n = self._conn.execute("SELECT COUNT(*) FROM pending_merges").fetchone()[0]
        self._conn.execute("DELETE FROM pending_merges")
        self._conn.commit()
        return n

    def _row_to_record(self, row: sqlite3.Row) -> MemoryRecord:
        raw_evidence = row["evidence_ids"] if "evidence_ids" in row.keys() else ""
        evidence_ids = [e for e in raw_evidence.split(",") if e] if raw_evidence else []
        return MemoryRecord(
            id=row["id"],
            session_id=row["session_id"],
            timestamp=row["timestamp"],
            turn_number=row["turn_number"],
            question=row["question"] or "",
            answer_preview=row["answer_preview"] or "",
            document=row["document"] or "",
            project=row["project"],
            topic=row["topic"],
            activity=row["activity"],
            memory_type=row["memory_type"],
            energy=row["energy"],
            model=row["model"],
            source=row["source"],
            stability=row["stability"] if "stability" in row.keys() and row["stability"] is not None else 1.0,
            observation_level=row["observation_level"] if "observation_level" in row.keys() and row["observation_level"] else "direct",
            evidence_ids=evidence_ids,
        )
