"""Routing Decision Log — SQLite persistence for training data.

Logs every routing decision (fast path and smart path) to SQLite
for analysis and future ML training. Follows the OutcomeDB pattern
from tracking/db.py: WAL mode, context manager, async wrappers.

Fire-and-forget: log() never raises. Failures are logged via structlog.
"""

from __future__ import annotations

import asyncio
import sqlite3
from datetime import datetime, timezone
from pathlib import Path
from typing import Any

import structlog
from pydantic import BaseModel, ConfigDict, Field

logger = structlog.get_logger()

# Default database location (same dir as outcomes.db)
DEFAULT_DECISIONS_DB_DIR = Path.home() / ".robot-resources"
DEFAULT_DECISIONS_DB_PATH = DEFAULT_DECISIONS_DB_DIR / "decisions.db"

_SCHEMA_SQL = """\
CREATE TABLE IF NOT EXISTS routing_decisions (
    id              INTEGER PRIMARY KEY AUTOINCREMENT,
    timestamp       TEXT NOT NULL,
    prompt_hash     TEXT NOT NULL,
    prompt_preview  TEXT NOT NULL DEFAULT '',
    keyword_task_type    TEXT NOT NULL,
    keyword_confidence   REAL NOT NULL,
    classification_source TEXT NOT NULL,
    llm_task_type   TEXT,
    llm_complexity  INTEGER,
    llm_model       TEXT,
    final_task_type TEXT NOT NULL,
    final_model     TEXT NOT NULL,
    capability_threshold REAL NOT NULL,
    latency_ms      INTEGER NOT NULL DEFAULT 0
);

CREATE INDEX IF NOT EXISTS idx_decisions_timestamp
    ON routing_decisions(timestamp);
CREATE INDEX IF NOT EXISTS idx_decisions_source
    ON routing_decisions(classification_source);
CREATE INDEX IF NOT EXISTS idx_decisions_final_task
    ON routing_decisions(final_task_type);
"""


def _now_iso() -> str:
    return datetime.now(timezone.utc).isoformat()


class RoutingDecision(BaseModel):
    """A single routing decision record."""

    model_config = ConfigDict(frozen=True)

    timestamp: str = Field(default_factory=_now_iso)
    prompt_hash: str
    prompt_preview: str
    keyword_task_type: str
    keyword_confidence: float
    classification_source: str  # "keyword" or "llm"
    llm_task_type: str | None = None
    llm_complexity: int | None = None
    llm_model: str | None = None
    final_task_type: str
    final_model: str
    capability_threshold: float
    latency_ms: int = 0


class DecisionLog:
    """SQLite-backed store for routing decision records.

    Thread-safe via ``check_same_thread=False`` and WAL journal mode.

    Usage::

        with DecisionLog() as dl:
            dl.log(decision)
            results = dl.query(classification_source="llm")
    """

    def __init__(self, db_path: str | Path | None = None) -> None:
        if db_path is None:
            db_path = DEFAULT_DECISIONS_DB_PATH
        self._db_path = Path(db_path)
        self._conn: sqlite3.Connection | None = None

    # ------------------------------------------------------------------
    # Lifecycle
    # ------------------------------------------------------------------

    def connect(self) -> DecisionLog:
        """Open the database connection and initialise schema."""
        self._db_path.parent.mkdir(parents=True, exist_ok=True)

        self._conn = sqlite3.connect(
            str(self._db_path),
            check_same_thread=False,
            timeout=10.0,
        )
        self._conn.row_factory = sqlite3.Row

        # Performance pragmas
        self._conn.execute("PRAGMA journal_mode=WAL")
        self._conn.execute("PRAGMA synchronous=NORMAL")

        self._conn.executescript(_SCHEMA_SQL)

        logger.debug("decision_log_connected", path=str(self._db_path))
        return self

    def close(self) -> None:
        """Close the database connection cleanly."""
        if self._conn is not None:
            try:
                self._conn.close()
            except sqlite3.Error:
                pass
            self._conn = None

    def __enter__(self) -> DecisionLog:
        return self.connect()

    def __exit__(self, *exc: object) -> None:
        self.close()

    @property
    def db_path(self) -> Path:
        return self._db_path

    # ------------------------------------------------------------------
    # Write
    # ------------------------------------------------------------------

    def log(self, decision: RoutingDecision) -> None:
        """Persist a routing decision. Never raises."""
        if self._conn is None:
            logger.debug("decision_log_not_connected")
            return
        try:
            self._conn.execute(
                """\
                INSERT INTO routing_decisions (
                    timestamp, prompt_hash, prompt_preview,
                    keyword_task_type, keyword_confidence,
                    classification_source,
                    llm_task_type, llm_complexity, llm_model,
                    final_task_type, final_model,
                    capability_threshold, latency_ms
                ) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
                """,
                (
                    decision.timestamp,
                    decision.prompt_hash,
                    decision.prompt_preview,
                    decision.keyword_task_type,
                    decision.keyword_confidence,
                    decision.classification_source,
                    decision.llm_task_type,
                    decision.llm_complexity,
                    decision.llm_model,
                    decision.final_task_type,
                    decision.final_model,
                    decision.capability_threshold,
                    decision.latency_ms,
                ),
            )
            self._conn.commit()
        except Exception:
            logger.warning("decision_log_write_error", exc_info=True)

    # ------------------------------------------------------------------
    # Read
    # ------------------------------------------------------------------

    def query(
        self,
        *,
        limit: int = 100,
        offset: int = 0,
        classification_source: str | None = None,
        final_task_type: str | None = None,
    ) -> list[dict[str, Any]]:
        """Query routing decisions with optional filters.

        Returns a list of dicts, most recent first.
        """
        if self._conn is None:
            return []

        clauses: list[str] = []
        params: list[Any] = []

        if classification_source is not None:
            clauses.append("classification_source = ?")
            params.append(classification_source)
        if final_task_type is not None:
            clauses.append("final_task_type = ?")
            params.append(final_task_type)

        where = f"WHERE {' AND '.join(clauses)}" if clauses else ""
        sql = (
            f"SELECT * FROM routing_decisions {where} "
            f"ORDER BY timestamp DESC LIMIT ? OFFSET ?"
        )
        params.extend([limit, offset])

        try:
            rows = self._conn.execute(sql, params).fetchall()
            return [dict(row) for row in rows]
        except Exception:
            logger.warning("decision_log_query_error", exc_info=True)
            return []

    def count(self) -> int:
        """Return total number of decision records."""
        if self._conn is None:
            return 0
        try:
            row = self._conn.execute(
                "SELECT COUNT(*) FROM routing_decisions"
            ).fetchone()
            return row[0] if row else 0
        except Exception:
            return 0

    # ------------------------------------------------------------------
    # Async wrappers
    # ------------------------------------------------------------------

    async def alog(self, decision: RoutingDecision) -> None:
        """Async wrapper for log() — runs in thread pool."""
        await asyncio.to_thread(self.log, decision)

    async def aquery(self, **kwargs: Any) -> list[dict[str, Any]]:
        """Async wrapper for query()."""
        return await asyncio.to_thread(self.query, **kwargs)
