"""SQLite persistence layer for routing outcomes.

Uses WAL mode for concurrent read access, PRAGMA user_version for
schema migrations, and asyncio.to_thread() for non-blocking async calls.

No external dependencies — stdlib sqlite3 only.
"""

from __future__ import annotations

import asyncio
import shutil
import sqlite3
from contextlib import contextmanager
from datetime import datetime, timezone
from pathlib import Path
from typing import Any

import structlog

from robot_resources.tracking.models import OutcomeRecord, OutcomeSummary

logger = structlog.get_logger()

# Current schema version — increment when schema changes
SCHEMA_VERSION = 1

# Default database location (canonical: ~/.robot-resources/)
DEFAULT_DB_DIR = Path.home() / ".robot-resources"
DEFAULT_DB_PATH = DEFAULT_DB_DIR / "outcomes.db"

# Legacy path (pre-session-26, before consolidation)
_OLD_DB_DIR = Path.home() / ".router"
_OLD_DB_PATH = _OLD_DB_DIR / "outcomes.db"


def _migrate_legacy_db(
    *,
    old_db_path: Path | None = None,
    new_db_path: Path | None = None,
    new_db_dir: Path | None = None,
    old_db_dir: Path | None = None,
) -> None:
    """One-time migration from ~/.router/ to ~/.robot-resources/.

    Moves outcomes.db (and WAL/SHM files) if the old path exists and
    the new path does not. If migration fails, logs a warning and
    continues — the caller will create a fresh DB at the new path.

    Parameters accept overrides for testing; defaults use module constants.
    """
    old_path = old_db_path or _OLD_DB_PATH
    new_path = new_db_path or DEFAULT_DB_PATH
    new_dir = new_db_dir or DEFAULT_DB_DIR
    old_dir = old_db_dir or _OLD_DB_DIR

    if not old_path.exists():
        return

    if new_path.exists():
        logger.info(
            "outcome_db_both_exist",
            old=str(old_path),
            new=str(new_path),
            note="Using new path. Old file at ~/.router/ can be removed.",
        )
        return

    try:
        new_dir.mkdir(parents=True, exist_ok=True)
        shutil.move(str(old_path), str(new_path))

        # Also move WAL and SHM files if they exist
        for suffix in ("-wal", "-shm"):
            old_extra = old_path.parent / f"{old_path.name}{suffix}"
            if old_extra.exists():
                new_extra = new_path.parent / f"{new_path.name}{suffix}"
                shutil.move(str(old_extra), str(new_extra))

        logger.info(
            "outcome_db_migrated",
            old=str(old_path),
            new=str(new_path),
        )

        # Remove old dir if now empty
        try:
            old_dir.rmdir()
        except OSError:
            pass  # Dir has other files — leave it

    except Exception:
        logger.warning(
            "outcome_db_migration_failed",
            exc_info=True,
            note="Continuing with new path. Old data preserved at legacy location.",
        )

_SCHEMA_SQL = """\
CREATE TABLE IF NOT EXISTS outcomes (
    request_id   TEXT PRIMARY KEY,
    timestamp    TEXT NOT NULL,
    task_type    TEXT NOT NULL,
    model_used   TEXT NOT NULL,
    model_baseline TEXT NOT NULL,
    provider     TEXT NOT NULL,
    input_tokens  INTEGER NOT NULL DEFAULT 0,
    output_tokens INTEGER NOT NULL DEFAULT 0,
    cost_actual  REAL NOT NULL DEFAULT 0.0,
    cost_baseline REAL NOT NULL DEFAULT 0.0,
    cost_saved   REAL NOT NULL DEFAULT 0.0,
    success      INTEGER,
    latency_ms   INTEGER NOT NULL DEFAULT 0,
    detection_confidence REAL,
    capability_score     REAL,
    savings_percent      REAL
);

CREATE INDEX IF NOT EXISTS idx_outcomes_timestamp
    ON outcomes(timestamp);
CREATE INDEX IF NOT EXISTS idx_outcomes_task_type
    ON outcomes(task_type);
CREATE INDEX IF NOT EXISTS idx_outcomes_model
    ON outcomes(model_used);
CREATE INDEX IF NOT EXISTS idx_outcomes_ts_task
    ON outcomes(timestamp, task_type);
"""


class OutcomeDB:
    """SQLite-backed store for routing outcome records.

    Thread-safe via ``check_same_thread=False`` and WAL journal mode.
    Async callers should use the ``a*`` methods which run I/O in a
    thread-pool executor via ``asyncio.to_thread()``.

    Usage::

        db = OutcomeDB()
        db.connect()
        db.record(outcome)
        results = db.query(task_type="coding")
        db.close()

    Or as a context manager::

        with OutcomeDB() as db:
            db.record(outcome)
    """

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

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

    def connect(self) -> OutcomeDB:
        """Open the database connection and initialise schema.

        On first call with default path, attempts one-time migration
        from legacy ~/.router/ location.
        """
        # Run legacy migration before connecting (noop if not needed)
        if self._db_path == DEFAULT_DB_PATH:
            _migrate_legacy_db()

        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.execute("PRAGMA foreign_keys=ON")

        self._apply_migrations()

        logger.info(
            "tracking_db_connected",
            path=str(self._db_path),
            schema_version=self._get_schema_version(),
        )
        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) -> OutcomeDB:
        return self.connect()

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

    @property
    def is_connected(self) -> bool:
        return self._conn is not None

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

    # ------------------------------------------------------------------
    # Schema management
    # ------------------------------------------------------------------

    def _get_schema_version(self) -> int:
        assert self._conn is not None
        row = self._conn.execute("PRAGMA user_version").fetchone()
        return row[0] if row else 0

    def _set_schema_version(self, version: int) -> None:
        assert self._conn is not None
        # PRAGMA doesn't support parameter binding
        self._conn.execute(f"PRAGMA user_version={version}")

    def _apply_migrations(self) -> None:
        """Run schema migrations up to SCHEMA_VERSION."""
        assert self._conn is not None
        current = self._get_schema_version()

        if current < 1:
            self._conn.executescript(_SCHEMA_SQL)
            self._set_schema_version(1)
            logger.info("tracking_schema_created", version=1)

        # Future migrations go here:
        # if current < 2:
        #     self._conn.executescript(_MIGRATION_V2_SQL)
        #     self._set_schema_version(2)

    # ------------------------------------------------------------------
    # Write operations
    # ------------------------------------------------------------------

    def record(self, outcome: OutcomeRecord) -> None:
        """Insert a single outcome record.

        Silently replaces on conflict (same request_id) to support
        idempotent writes from retry scenarios.
        """
        assert self._conn is not None
        ts = outcome.timestamp.isoformat() if outcome.timestamp else _now_iso()
        success_int = _bool_to_int(outcome.success)

        self._conn.execute(
            """\
            INSERT OR REPLACE INTO outcomes (
                request_id, timestamp, task_type, model_used,
                model_baseline, provider,
                input_tokens, output_tokens,
                cost_actual, cost_baseline, cost_saved,
                success, latency_ms,
                detection_confidence, capability_score, savings_percent
            ) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
            """,
            (
                outcome.request_id,
                ts,
                outcome.task_type,
                outcome.model_used,
                outcome.model_baseline,
                outcome.provider,
                outcome.input_tokens,
                outcome.output_tokens,
                outcome.cost_actual,
                outcome.cost_baseline,
                outcome.cost_saved,
                success_int,
                outcome.latency_ms,
                outcome.detection_confidence,
                outcome.capability_score,
                outcome.savings_percent,
            ),
        )
        self._conn.commit()

    def record_many(self, outcomes: list[OutcomeRecord]) -> int:
        """Insert multiple outcome records in a single transaction.

        Returns the number of records inserted.
        """
        assert self._conn is not None
        count = 0
        with _transaction(self._conn):
            for outcome in outcomes:
                ts = outcome.timestamp.isoformat() if outcome.timestamp else _now_iso()
                success_int = _bool_to_int(outcome.success)
                self._conn.execute(
                    """\
                    INSERT OR REPLACE INTO outcomes (
                        request_id, timestamp, task_type, model_used,
                        model_baseline, provider,
                        input_tokens, output_tokens,
                        cost_actual, cost_baseline, cost_saved,
                        success, latency_ms,
                        detection_confidence, capability_score, savings_percent
                    ) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
                    """,
                    (
                        outcome.request_id,
                        ts,
                        outcome.task_type,
                        outcome.model_used,
                        outcome.model_baseline,
                        outcome.provider,
                        outcome.input_tokens,
                        outcome.output_tokens,
                        outcome.cost_actual,
                        outcome.cost_baseline,
                        outcome.cost_saved,
                        success_int,
                        outcome.latency_ms,
                        outcome.detection_confidence,
                        outcome.capability_score,
                        outcome.savings_percent,
                    ),
                )
                count += 1
        return count

    # ------------------------------------------------------------------
    # Read operations
    # ------------------------------------------------------------------

    def query(
        self,
        *,
        since: datetime | str | None = None,
        until: datetime | str | None = None,
        task_type: str | None = None,
        model: str | None = None,
        provider: str | None = None,
        limit: int = 100,
    ) -> list[OutcomeRecord]:
        """Query outcome records with optional filters.

        Returns a list of OutcomeRecord objects, most recent first.
        """
        assert self._conn is not None
        clauses: list[str] = []
        params: list[Any] = []

        if since is not None:
            clauses.append("timestamp >= ?")
            params.append(_to_iso(since))
        if until is not None:
            clauses.append("timestamp <= ?")
            params.append(_to_iso(until))
        if task_type is not None:
            clauses.append("task_type = ?")
            params.append(task_type)
        if model is not None:
            clauses.append("model_used = ?")
            params.append(model)
        if provider is not None:
            clauses.append("provider = ?")
            params.append(provider)

        where = f"WHERE {' AND '.join(clauses)}" if clauses else ""
        sql = f"SELECT * FROM outcomes {where} ORDER BY timestamp DESC LIMIT ?"
        params.append(limit)

        rows = self._conn.execute(sql, params).fetchall()
        return [_row_to_record(row) for row in rows]

    def get_summary(
        self,
        *,
        since: datetime | str | None = None,
        until: datetime | str | None = None,
    ) -> OutcomeSummary:
        """Get aggregated statistics for a time period."""
        assert self._conn is not None
        clauses: list[str] = []
        params: list[Any] = []

        if since is not None:
            clauses.append("timestamp >= ?")
            params.append(_to_iso(since))
        if until is not None:
            clauses.append("timestamp <= ?")
            params.append(_to_iso(until))

        where = f"WHERE {' AND '.join(clauses)}" if clauses else ""

        # Main aggregation
        agg = self._conn.execute(
            f"""\
            SELECT
                COUNT(*) as total_requests,
                COALESCE(SUM(cost_actual), 0) as total_cost_actual,
                COALESCE(SUM(cost_baseline), 0) as total_cost_baseline,
                COALESCE(SUM(cost_saved), 0) as total_cost_saved,
                COALESCE(SUM(input_tokens), 0) as total_input_tokens,
                COALESCE(SUM(output_tokens), 0) as total_output_tokens,
                COALESCE(AVG(latency_ms), 0) as avg_latency_ms,
                COALESCE(AVG(savings_percent), 0) as avg_savings_percent,
                SUM(CASE WHEN success = 1 THEN 1 ELSE 0 END) as success_count,
                SUM(CASE WHEN success IS NOT NULL THEN 1 ELSE 0 END) as rated_count
            FROM outcomes {where}
            """,
            params,
        ).fetchone()

        if agg is None or agg["total_requests"] == 0:
            return OutcomeSummary()

        # Success rate (only if we have rated outcomes)
        success_rate = None
        if agg["rated_count"] > 0:
            success_rate = agg["success_count"] / agg["rated_count"]

        # Breakdowns
        by_task = dict(
            self._conn.execute(
                f"SELECT task_type, COUNT(*) FROM outcomes {where} GROUP BY task_type",
                params,
            ).fetchall()
        )
        by_model = dict(
            self._conn.execute(
                f"SELECT model_used, COUNT(*) FROM outcomes {where} GROUP BY model_used",
                params,
            ).fetchall()
        )
        by_provider = dict(
            self._conn.execute(
                f"SELECT provider, COUNT(*) FROM outcomes {where} GROUP BY provider",
                params,
            ).fetchall()
        )

        return OutcomeSummary(
            total_requests=agg["total_requests"],
            total_cost_actual=round(agg["total_cost_actual"], 6),
            total_cost_baseline=round(agg["total_cost_baseline"], 6),
            total_cost_saved=round(agg["total_cost_saved"], 6),
            total_input_tokens=agg["total_input_tokens"],
            total_output_tokens=agg["total_output_tokens"],
            avg_latency_ms=round(agg["avg_latency_ms"], 1),
            avg_savings_percent=round(agg["avg_savings_percent"], 1),
            success_rate=round(success_rate, 4) if success_rate is not None else None,
            by_task_type=by_task,
            by_model=by_model,
            by_provider=by_provider,
        )

    def get_cost_by_task_type(
        self,
        *,
        since: datetime | str | None = None,
        until: datetime | str | None = None,
    ) -> dict[str, dict[str, float | int]]:
        """Get cost savings aggregated by task type.

        Returns a dict of ``{task_type: {"count": N, "cost_saved": X}}``.
        Used by CLI reports for cost breakdown tables.
        """
        assert self._conn is not None
        clauses: list[str] = []
        params: list[Any] = []

        if since is not None:
            clauses.append("timestamp >= ?")
            params.append(_to_iso(since))
        if until is not None:
            clauses.append("timestamp <= ?")
            params.append(_to_iso(until))

        where = f"WHERE {' AND '.join(clauses)}" if clauses else ""

        rows = self._conn.execute(
            f"""\
            SELECT
                task_type,
                COUNT(*) as cnt,
                COALESCE(SUM(cost_saved), 0) as total_saved
            FROM outcomes {where}
            GROUP BY task_type
            """,
            params,
        ).fetchall()

        return {
            row["task_type"]: {
                "count": row["cnt"],
                "cost_saved": round(row["total_saved"], 6),
            }
            for row in rows
        }

    def get_cost_by_provider(
        self,
        *,
        since: datetime | str | None = None,
        until: datetime | str | None = None,
    ) -> dict[str, float]:
        """Get cost_saved aggregated by provider.

        Returns a dict of ``{provider: total_cost_saved}``.
        Used by the stats endpoint for provider cost breakdown.
        """
        assert self._conn is not None
        clauses: list[str] = []
        params: list[Any] = []

        if since is not None:
            clauses.append("timestamp >= ?")
            params.append(_to_iso(since))
        if until is not None:
            clauses.append("timestamp <= ?")
            params.append(_to_iso(until))

        where = f"WHERE {' AND '.join(clauses)}" if clauses else ""

        rows = self._conn.execute(
            f"""\
            SELECT provider, COALESCE(SUM(cost_saved), 0) as total_saved
            FROM outcomes {where}
            GROUP BY provider
            """,
            params,
        ).fetchall()

        return {row["provider"]: round(row["total_saved"], 6) for row in rows}

    def get_user_baseline_model(
        self, valid_models: set[str] | None = None
    ) -> str | None:
        """Return the user's most frequently used model in direct requests.

        Looks at non-routed requests (``task_type='direct'``) to determine
        what model the user would use without the Router.  Returns ``None``
        if no direct requests have been recorded yet.

        Args:
            valid_models: If provided, only consider models in this set.
                Filters out deprecated models no longer in the DB.
        """
        assert self._conn is not None
        rows = self._conn.execute(
            """\
            SELECT model_used, COUNT(*) as cnt
            FROM outcomes
            WHERE task_type = 'direct'
            GROUP BY model_used
            ORDER BY cnt DESC
            """,
        ).fetchall()
        for row in rows:
            name = row["model_used"]
            if valid_models is None or name in valid_models:
                return name
        return None

    def count(self) -> int:
        """Return total number of outcome records."""
        assert self._conn is not None
        row = self._conn.execute("SELECT COUNT(*) FROM outcomes").fetchone()
        return row[0] if row else 0

    # ------------------------------------------------------------------
    # Async wrappers (for FastAPI handlers)
    # ------------------------------------------------------------------

    async def arecord(self, outcome: OutcomeRecord) -> None:
        """Async wrapper for record() — runs in thread pool."""
        await asyncio.to_thread(self.record, outcome)

    async def arecord_many(self, outcomes: list[OutcomeRecord]) -> int:
        """Async wrapper for record_many()."""
        return await asyncio.to_thread(self.record_many, outcomes)

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

    async def aget_summary(self, **kwargs: Any) -> OutcomeSummary:
        """Async wrapper for get_summary()."""
        return await asyncio.to_thread(self.get_summary, **kwargs)


# ------------------------------------------------------------------
# Helpers
# ------------------------------------------------------------------


@contextmanager
def _transaction(conn: sqlite3.Connection):
    """Execute a block inside a transaction with rollback on error."""
    try:
        yield conn
        conn.commit()
    except Exception:
        conn.rollback()
        raise


def _bool_to_int(value: bool | None) -> int | None:
    if value is None:
        return None
    return 1 if value else 0


def _int_to_bool(value: int | None) -> bool | None:
    if value is None:
        return None
    return value != 0


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


def _to_iso(value: datetime | str) -> str:
    if isinstance(value, datetime):
        return value.isoformat()
    return value


def _row_to_record(row: sqlite3.Row) -> OutcomeRecord:
    """Convert a database row to an OutcomeRecord."""
    return OutcomeRecord(
        request_id=row["request_id"],
        timestamp=datetime.fromisoformat(row["timestamp"]),
        task_type=row["task_type"],
        model_used=row["model_used"],
        model_baseline=row["model_baseline"],
        provider=row["provider"],
        input_tokens=row["input_tokens"],
        output_tokens=row["output_tokens"],
        cost_actual=row["cost_actual"],
        cost_baseline=row["cost_baseline"],
        cost_saved=row["cost_saved"],
        success=_int_to_bool(row["success"]),
        latency_ms=row["latency_ms"],
        detection_confidence=row["detection_confidence"],
        capability_score=row["capability_score"],
        savings_percent=row["savings_percent"],
    )
