"""SQLite result storage for eval history and regression detection."""

from __future__ import annotations

import json
import sqlite3
import uuid
from dataclasses import dataclass
from datetime import datetime
from pathlib import Path
from typing import Any

from hatchdx.eval.models import AssertionOutcome, EvalCase, EvalError, EvalResult


DB_FILENAME = ".hdx/eval_results.db"


def _wrap_locked_error(exc: sqlite3.OperationalError, db_path: Path) -> EvalError:
    """Convert a SQLite 'database is locked' error into a user-friendly EvalError."""
    return EvalError(
        "Database is locked — another hdx eval may be running.\n"
        "Wait for it to finish or remove the lock file at:\n"
        f"  {db_path}"
    )


@dataclass
class StoredEvalResult:
    """A flattened eval result as stored in the database."""

    run_id: str
    suite_name: str
    eval_name: str
    passed: bool
    latency_ms: float
    timestamp: str
    assertion_details: list[dict[str, Any]]


@dataclass
class LatencyPoint:
    """A single latency measurement for trend analysis."""

    timestamp: str
    latency_ms: float
    passed: bool


@dataclass
class Regression:
    """An eval that passed previously but now fails."""

    eval_name: str
    suite_name: str
    previous_run_id: str
    current_run_id: str


class EvalStorage:
    """SQLite-based storage for eval results.

    The database is created automatically on first use at
    ``{project_root}/.hdx/eval_results.db``.
    """

    def __init__(self, project_root: Path | None = None) -> None:
        root = project_root or Path.cwd()
        self._db_path = root / DB_FILENAME
        self._db_path.parent.mkdir(parents=True, exist_ok=True)
        self._conn: sqlite3.Connection | None = None

    def _connect(self) -> sqlite3.Connection:
        """Return an open connection, creating the DB and tables if needed."""
        if self._conn is not None:
            return self._conn

        self._conn = sqlite3.connect(str(self._db_path), timeout=5)
        self._conn.execute("PRAGMA journal_mode=WAL")
        self._create_tables()
        return self._conn

    def _create_tables(self) -> None:
        """Create the eval results tables if they don't exist."""
        conn = self._conn
        if conn is None:
            raise EvalError("Database connection not established.")

        try:
            conn.execute("""
                CREATE TABLE IF NOT EXISTS eval_runs (
                    run_id TEXT PRIMARY KEY,
                    started_at TEXT NOT NULL,
                    finished_at TEXT
                )
            """)

            conn.execute("""
                CREATE TABLE IF NOT EXISTS eval_results (
                    id INTEGER PRIMARY KEY AUTOINCREMENT,
                    run_id TEXT NOT NULL,
                    suite_name TEXT NOT NULL,
                    eval_name TEXT NOT NULL,
                    passed INTEGER NOT NULL,
                    latency_ms REAL NOT NULL,
                    timestamp TEXT NOT NULL,
                    assertion_details TEXT NOT NULL,
                    FOREIGN KEY (run_id) REFERENCES eval_runs(run_id)
                )
            """)

            conn.execute("""
                CREATE INDEX IF NOT EXISTS idx_eval_results_suite
                ON eval_results(suite_name, eval_name)
            """)

            conn.execute("""
                CREATE INDEX IF NOT EXISTS idx_eval_results_run
                ON eval_results(run_id)
            """)

            conn.commit()
        except sqlite3.OperationalError as exc:
            if "locked" in str(exc).lower():
                raise _wrap_locked_error(exc, self._db_path) from exc
            raise

    def close(self) -> None:
        """Close the database connection."""
        if self._conn is not None:
            self._conn.close()
            self._conn = None

    def start_run(self) -> str:
        """Create a new eval run and return its ID."""
        conn = self._connect()
        run_id = str(uuid.uuid4())
        now = datetime.now().isoformat()
        try:
            conn.execute(
                "INSERT INTO eval_runs (run_id, started_at) VALUES (?, ?)",
                (run_id, now),
            )
            conn.commit()
        except sqlite3.OperationalError as exc:
            if "locked" in str(exc).lower():
                raise _wrap_locked_error(exc, self._db_path) from exc
            raise
        return run_id

    def finish_run(self, run_id: str) -> None:
        """Mark an eval run as finished."""
        conn = self._connect()
        now = datetime.now().isoformat()
        try:
            conn.execute(
                "UPDATE eval_runs SET finished_at = ? WHERE run_id = ?",
                (now, run_id),
            )
            conn.commit()
        except sqlite3.OperationalError as exc:
            if "locked" in str(exc).lower():
                raise _wrap_locked_error(exc, self._db_path) from exc
            raise

    def store_result(
        self,
        run_id: str,
        suite_name: str,
        result: EvalResult,
    ) -> None:
        """Store a single eval result."""
        conn = self._connect()

        assertion_details = [
            {
                "type": ao.assertion.type,
                "passed": ao.passed,
                "reason": ao.reason,
            }
            for ao in result.assertion_results
        ]

        try:
            conn.execute(
                """
                INSERT INTO eval_results
                    (run_id, suite_name, eval_name, passed, latency_ms, timestamp, assertion_details)
                VALUES (?, ?, ?, ?, ?, ?, ?)
                """,
                (
                    run_id,
                    suite_name,
                    result.eval_case.name,
                    1 if result.passed else 0,
                    result.latency_ms,
                    result.timestamp.isoformat(),
                    json.dumps(assertion_details),
                ),
            )
        except sqlite3.OperationalError as exc:
            if "locked" in str(exc).lower():
                raise _wrap_locked_error(exc, self._db_path) from exc
            raise
        # Note: commits are deferred to finish_run() for batch efficiency.
        # Callers should call finish_run() after storing all results.

    def get_history(
        self,
        suite_name: str,
        limit: int = 10,
    ) -> list[StoredEvalResult]:
        """Get recent eval results for a suite, ordered by most recent first."""
        conn = self._connect()
        cursor = conn.execute(
            """
            SELECT run_id, suite_name, eval_name, passed, latency_ms, timestamp, assertion_details
            FROM eval_results
            WHERE suite_name = ?
            ORDER BY timestamp DESC
            LIMIT ?
            """,
            (suite_name, limit),
        )

        results: list[StoredEvalResult] = []
        for row in cursor.fetchall():
            results.append(StoredEvalResult(
                run_id=row[0],
                suite_name=row[1],
                eval_name=row[2],
                passed=bool(row[3]),
                latency_ms=row[4],
                timestamp=row[5],
                assertion_details=json.loads(row[6]),
            ))
        return results

    def detect_regressions(
        self,
        suite_name: str,
        current_run_id: str,
    ) -> list[Regression]:
        """Compare the current run against the previous run for a suite.

        Returns a list of regressions — evals that passed in the previous
        run but fail in the current one.
        """
        conn = self._connect()

        # Find the previous run ID (the most recent run before the current one)
        cursor = conn.execute(
            """
            SELECT DISTINCT run_id
            FROM eval_results
            WHERE suite_name = ? AND run_id != ?
            ORDER BY timestamp DESC
            LIMIT 1
            """,
            (suite_name, current_run_id),
        )
        row = cursor.fetchone()
        if row is None:
            # No previous run — no regressions possible
            return []

        previous_run_id = row[0]

        # Get results from previous run
        cursor = conn.execute(
            """
            SELECT eval_name, passed
            FROM eval_results
            WHERE run_id = ? AND suite_name = ?
            """,
            (previous_run_id, suite_name),
        )
        previous_results = {row[0]: bool(row[1]) for row in cursor.fetchall()}

        # Get results from current run
        cursor = conn.execute(
            """
            SELECT eval_name, passed
            FROM eval_results
            WHERE run_id = ? AND suite_name = ?
            """,
            (current_run_id, suite_name),
        )
        current_results = {row[0]: bool(row[1]) for row in cursor.fetchall()}

        # Find regressions
        regressions: list[Regression] = []
        for eval_name, previously_passed in previous_results.items():
            currently_passed = current_results.get(eval_name)
            if previously_passed and currently_passed is False:
                regressions.append(Regression(
                    eval_name=eval_name,
                    suite_name=suite_name,
                    previous_run_id=previous_run_id,
                    current_run_id=current_run_id,
                ))

        return regressions

    def get_latency_trend(
        self,
        eval_name: str,
        limit: int = 20,
    ) -> list[LatencyPoint]:
        """Get latency measurements for a specific eval over time."""
        conn = self._connect()
        cursor = conn.execute(
            """
            SELECT timestamp, latency_ms, passed
            FROM eval_results
            WHERE eval_name = ?
            ORDER BY timestamp DESC
            LIMIT ?
            """,
            (eval_name, limit),
        )

        points: list[LatencyPoint] = []
        for row in cursor.fetchall():
            points.append(LatencyPoint(
                timestamp=row[0],
                latency_ms=row[1],
                passed=bool(row[2]),
            ))

        # Return in chronological order (oldest first)
        points.reverse()
        return points
