"""Session search index — SQLite FTS5 over conversation history.

Maintains a full-text index of all session messages so the agent can
recall past conversations.  The index is a *derived copy* — the
canonical data remains in the JSONL session files.  If the index DB
is deleted it will be rebuilt on next startup.

DB location: ``~/.flowly/session_index.sqlite``
"""

from __future__ import annotations

import re
import sqlite3
import time
from pathlib import Path
from typing import Any

from loguru import logger

def _default_db_path() -> Path:
    from flowly.profile import get_flowly_home
    return get_flowly_home() / "session_index.sqlite"

_SCHEMA_SQL = """
CREATE TABLE IF NOT EXISTS sessions (
    key        TEXT PRIMARY KEY,
    created_at REAL,
    updated_at REAL,
    msg_count  INTEGER DEFAULT 0
);

CREATE TABLE IF NOT EXISTS messages (
    id          INTEGER PRIMARY KEY AUTOINCREMENT,
    session_key TEXT NOT NULL,
    role        TEXT NOT NULL,
    content     TEXT NOT NULL,
    timestamp   REAL NOT NULL,
    FOREIGN KEY (session_key) REFERENCES sessions(key)
);

CREATE INDEX IF NOT EXISTS idx_messages_session
    ON messages(session_key, timestamp);

CREATE VIRTUAL TABLE IF NOT EXISTS messages_fts USING fts5(
    content,
    content=messages,
    content_rowid=id
);

CREATE TRIGGER IF NOT EXISTS fts_insert AFTER INSERT ON messages BEGIN
    INSERT INTO messages_fts(rowid, content) VALUES (new.id, new.content);
END;

CREATE TRIGGER IF NOT EXISTS fts_delete AFTER DELETE ON messages BEGIN
    INSERT INTO messages_fts(messages_fts, rowid, content)
        VALUES('delete', old.id, old.content);
END;
"""


def _sanitize_fts5_query(query: str) -> str:
    """Sanitize user input for safe FTS5 MATCH queries.

    Strips special FTS5 characters, wraps hyphenated terms, and removes
    dangling boolean operators so the query never causes OperationalError.
    """
    if not query:
        return ""
    # Preserve balanced double-quoted phrases
    quoted: list[str] = []

    def _keep(m: re.Match) -> str:
        quoted.append(m.group(0))
        return f"\x00Q{len(quoted) - 1}\x00"

    s = re.sub(r'"[^"]*"', _keep, query)
    # Strip FTS5 specials
    s = re.sub(r'[+{}()\"^]', " ", s)
    s = re.sub(r"\*+", "*", s)
    s = re.sub(r"(^|\s)\*", r"\1", s)
    # Remove dangling boolean operators
    s = re.sub(r"(?i)^(AND|OR|NOT)\b\s*", "", s.strip())
    s = re.sub(r"(?i)\s+(AND|OR|NOT)\s*$", "", s.strip())
    # Wrap hyphenated terms
    s = re.sub(r"\b(\w+(?:-\w+)+)\b", r'"\1"', s)
    # Restore quoted phrases
    for i, q in enumerate(quoted):
        s = s.replace(f"\x00Q{i}\x00", q)
    # Auto-prefix: append * to bare words for agglutinative languages
    # (Turkish: anne → annemin, Docker → Dockerfile, etc.)
    # Skips quoted phrases, words already ending in *, and boolean operators
    words = s.strip().split()
    prefixed = []
    in_quote = False
    for w in words:
        if w.startswith('"'):
            in_quote = True
        if in_quote:
            prefixed.append(w)
            if w.endswith('"'):
                in_quote = False
        elif w.endswith('*') or w.upper() in ("AND", "OR", "NOT"):
            prefixed.append(w)
        else:
            prefixed.append(w + "*")
    return " ".join(prefixed)


class SessionIndexer:
    """SQLite FTS5 index over session messages."""

    def __init__(self, db_path: Path | None = None) -> None:
        self._path = db_path or _default_db_path()
        self._path.parent.mkdir(parents=True, exist_ok=True)
        self._conn = sqlite3.connect(str(self._path), timeout=5)
        self._conn.row_factory = sqlite3.Row
        self._conn.execute("PRAGMA journal_mode=WAL")
        self._conn.execute("PRAGMA busy_timeout=5000")
        self._conn.executescript(_SCHEMA_SQL)
        self._conn.commit()

    # ── Indexing ───────────────────────────────────────────────────

    def index_session(self, key: str, messages: list[dict[str, Any]]) -> None:
        """Index (or re-index) all messages for a session."""
        now = time.time()
        content_messages = [
            m for m in messages
            if m.get("content") and m.get("role") in ("user", "assistant")
        ]
        try:
            with self._conn:
                # Delete old messages for this session (triggers FTS delete)
                self._conn.execute(
                    "DELETE FROM messages WHERE session_key = ?", (key,)
                )
                # Insert fresh
                for m in content_messages:
                    ts_str = m.get("timestamp", "")
                    try:
                        from datetime import datetime
                        ts = datetime.fromisoformat(ts_str).timestamp()
                    except (ValueError, TypeError):
                        ts = now
                    self._conn.execute(
                        "INSERT INTO messages (session_key, role, content, timestamp) "
                        "VALUES (?, ?, ?, ?)",
                        (key, m["role"], m["content"], ts),
                    )
                # Upsert session record
                created_at = now
                if content_messages:
                    try:
                        first_ts = content_messages[0].get("timestamp", "")
                        created_at = datetime.fromisoformat(first_ts).timestamp()
                    except (ValueError, TypeError):
                        pass
                self._conn.execute(
                    "INSERT INTO sessions (key, created_at, updated_at, msg_count) "
                    "VALUES (?, ?, ?, ?) "
                    "ON CONFLICT(key) DO UPDATE SET "
                    "updated_at=excluded.updated_at, msg_count=excluded.msg_count",
                    (key, created_at, now, len(content_messages)),
                )
        except Exception as e:
            logger.debug("Session index failed for %s: %s", key, e)

    # ── Search ─────────────────────────────────────────────────────

    def search(
        self,
        query: str,
        limit: int = 20,
        exclude_session: str | None = None,
    ) -> list[dict[str, Any]]:
        """Full-text search across all session messages.

        Returns matches with snippet, rank, session_key, role, timestamp,
        and ±1 surrounding context messages.
        """
        q = _sanitize_fts5_query(query)
        if not q:
            return []

        params: list[Any] = [q]
        exclude_clause = ""
        if exclude_session:
            exclude_clause = "AND m.session_key != ?"
            params.append(exclude_session)
        params.append(limit)

        sql = f"""
            SELECT
                m.id,
                m.session_key,
                m.role,
                snippet(messages_fts, 0, '>>>', '<<<', '...', 40) AS snippet,
                m.timestamp,
                s.created_at AS session_created,
                s.msg_count
            FROM messages_fts
            JOIN messages m ON m.id = messages_fts.rowid
            JOIN sessions s ON s.key = m.session_key
            WHERE messages_fts MATCH ?
            {exclude_clause}
            ORDER BY rank
            LIMIT ?
        """
        try:
            rows = self._conn.execute(sql, params).fetchall()
        except sqlite3.OperationalError:
            return []

        results = []
        seen_sessions: set[str] = set()
        for row in rows:
            r = dict(row)
            sk = r["session_key"]
            # Deduplicate by session (show best match per session)
            if sk in seen_sessions:
                continue
            seen_sessions.add(sk)
            # Add ±1 context messages
            try:
                ctx = self._conn.execute(
                    "SELECT role, content FROM messages "
                    "WHERE session_key = ? AND id BETWEEN ? - 1 AND ? + 1 "
                    "ORDER BY id",
                    (sk, r["id"], r["id"]),
                ).fetchall()
                r["context"] = [
                    {"role": c["role"], "content": (c["content"] or "")[:300]}
                    for c in ctx
                ]
            except Exception:
                r["context"] = []
            r.pop("id", None)
            results.append(r)

        return results

    # ── Recent sessions ────────────────────────────────────────────

    def list_recent(
        self,
        limit: int = 10,
        exclude_session: str | None = None,
    ) -> list[dict[str, Any]]:
        """List most recent sessions with a preview (no FTS, no LLM)."""
        params: list[Any] = []
        exclude = ""
        if exclude_session:
            exclude = "WHERE s.key != ?"
            params.append(exclude_session)
        params.append(limit)

        sql = f"""
            SELECT
                s.key,
                s.created_at,
                s.updated_at,
                s.msg_count,
                (SELECT SUBSTR(m.content, 1, 80)
                 FROM messages m
                 WHERE m.session_key = s.key AND m.role = 'user'
                 ORDER BY m.timestamp LIMIT 1) AS preview
            FROM sessions s
            {exclude}
            ORDER BY s.updated_at DESC
            LIMIT ?
        """
        rows = self._conn.execute(sql, params).fetchall()
        return [dict(r) for r in rows]

    # ── Maintenance ────────────────────────────────────────────────

    def rebuild_from_sessions_dir(self, sessions_dir: Path) -> int:
        """Rebuild entire index from JSONL session files. Returns count."""
        import json as _json
        count = 0
        for path in sessions_dir.glob("*.jsonl"):
            try:
                messages = []
                with open(path, encoding="utf-8") as f:
                    for line in f:
                        line = line.strip()
                        if not line:
                            continue
                        data = _json.loads(line)
                        if data.get("_type") == "metadata":
                            continue
                        messages.append(data)
                if messages:
                    key = path.stem.replace("_", ":", 1)
                    self.index_session(key, messages)
                    count += 1
            except Exception as e:
                logger.debug("Skipped %s during rebuild: %s", path.name, e)
        logger.info("Session index rebuilt: %d sessions", count)
        return count

    def close(self) -> None:
        """Close the database connection."""
        try:
            self._conn.close()
        except Exception:
            pass
