"""SQLite storage backend — traces in a local SQLite database.

Tables are auto-created on init if they don't exist.
Uses Python's built-in sqlite3 module.
"""

from __future__ import annotations

import json
import sqlite3
from datetime import datetime
from pathlib import Path
from typing import Any, Iterator

from ..models import Trace
from .base import StoreBase
from .normalize import normalize_trace
from .summary import summarize_trace


class SQLiteStore(StoreBase):
    """SQLite storage backend."""

    def __init__(
        self,
        db_path: str | Path | None = None,
        auto_create: bool = True,
    ) -> None:
        if db_path is None:
            db_path = Path.home() / ".bandito" / "traces" / "bandito.db"
        self._db_path = Path(db_path)
        self._db_path.parent.mkdir(parents=True, exist_ok=True)
        self._conn = sqlite3.connect(str(self._db_path), check_same_thread=False)
        self._conn.row_factory = sqlite3.Row

        if auto_create:
            self._create_tables()

    def validate(self) -> str | None:
        try:
            with self._conn.cursor() as cur:
                cur.execute("SELECT 1")
            return None
        except Exception as e:
            return f"Cannot connect to SQLite: {e}"

    def _create_tables(self) -> None:
        cur = self._conn.cursor()
        cur.execute("""
            CREATE TABLE IF NOT EXISTS bandito_traces (
                trace_id TEXT PRIMARY KEY,
                project TEXT NOT NULL,
                data TEXT NOT NULL,
                started_at TEXT,
                created_at TEXT NOT NULL DEFAULT (datetime('now'))
            )
        """)
        cur.execute("""
            CREATE INDEX IF NOT EXISTS idx_bandito_traces_project
            ON bandito_traces(project)
        """)
        cur.execute("""
            CREATE INDEX IF NOT EXISTS idx_bandito_traces_started_at
            ON bandito_traces(started_at)
        """)
        cur.execute("""
            CREATE INDEX IF NOT EXISTS idx_bandito_traces_project_started
            ON bandito_traces(project, started_at DESC)
        """)
        self._conn.commit()
        cur.close()

    def close(self) -> None:
        self._conn.close()

    def __enter__(self) -> "SQLiteStore":
        return self

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

    def __del__(self) -> None:
        try:
            self._conn.close()
        except Exception:
            pass

    def write_trace(self, trace: Trace) -> None:
        data = trace.to_dict()
        started_at = data.get("started_at")
        with self._conn.cursor() as cur:
            cur.execute(
                """
                INSERT INTO bandito_traces (trace_id, project, data, started_at)
                VALUES (?, ?, ?, ?)
                ON CONFLICT(trace_id) DO UPDATE SET
                    data = excluded.data,
                    started_at = excluded.started_at
                """,
                (trace.trace_id, trace.project, json.dumps(data), started_at),
            )
            self._conn.commit()

    def _read_trace(self, trace_id: str) -> dict[str, Any] | None:
        with self._conn.cursor() as cur:
            cur.execute(
                "SELECT data FROM bandito_traces WHERE trace_id = ?",
                (trace_id,),
            )
            row = cur.fetchone()
            if row is None:
                return None
            return _ensure_dict(row["data"])

    def iter_traces(self, project: str | None = None) -> Iterator[dict[str, Any]]:
        with self._conn.cursor() as cur:
            if project is not None:
                cur.execute(
                    "SELECT data FROM bandito_traces WHERE project = ? ORDER BY started_at DESC",
                    (project,),
                )
            else:
                cur.execute("SELECT data FROM bandito_traces ORDER BY started_at DESC")
            for row in cur:
                d = _ensure_dict(row["data"])
                if d:
                    yield d

    def list_projects(self) -> list[str]:
        with self._conn.cursor() as cur:
            cur.execute("SELECT DISTINCT project FROM bandito_traces ORDER BY project")
            return [row["project"] for row in cur]

    def _query_traces(
        self,
        project: str | None = None,
        since: datetime | None = None,
        limit: int | None = None,
        offset: int = 0,
    ) -> list[dict[str, Any]]:
        """Execute a paginated trace query with SQL-level filtering."""
        query = "SELECT data FROM bandito_traces"
        conditions: list[str] = []
        params: list[Any] = []

        if project is not None:
            conditions.append("project = ?")
            params.append(project)
        if since is not None:
            conditions.append("started_at >= ?")
            params.append(since.isoformat())

        if conditions:
            query += " WHERE " + " AND ".join(conditions)
        query += " ORDER BY started_at DESC"

        if offset:
            query += " OFFSET ?"
            params.append(offset)
        if limit is not None and limit > 0:
            query += " LIMIT ?"
            params.append(limit)

        with self._conn.cursor() as cur:
            cur.execute(query, params)
            rows = []
            for row in cur:
                d = _ensure_dict(row["data"])
                if d:
                    rows.append(d)
            return rows

    def list_traces_filtered(
        self,
        project: str | None = None,
        tag: str | None = None,
        since: datetime | None = None,
        limit: int | None = None,
        offset: int = 0,
    ) -> list[dict[str, Any]]:
        """Override with SQL pushdown for since/limit/offset."""
        if tag is not None:
            sql_limit = (limit * 3) if limit else None
            rows = self._query_traces(project, since, sql_limit, offset=0)
            traces = []
            for raw in rows:
                trace = normalize_trace(raw)
                if tag not in trace.get("tags", []):
                    continue
                traces.append(trace)
            if offset:
                traces = traces[offset:]
            if limit is not None and limit > 0:
                traces = traces[:limit]
            if len(traces) < (limit or 0) and sql_limit and len(rows) >= sql_limit:
                return super().list_traces_filtered(project, tag, since, limit, offset)
            return traces

        rows = self._query_traces(project, since, limit, offset)
        return [normalize_trace(raw) for raw in rows]

    def list_trace_summaries(
        self,
        project: str | None = None,
        tag: str | None = None,
        since: datetime | None = None,
        limit: int | None = None,
        offset: int = 0,
    ) -> list[dict[str, Any]]:
        """Override with SQL pushdown for since/limit/offset."""
        if tag is not None:
            sql_limit = (limit * 3) if limit else None
            rows = self._query_traces(project, since, sql_limit, offset=0)
            summaries = []
            for raw in rows:
                trace = normalize_trace(raw)
                if tag not in trace.get("tags", []):
                    continue
                summaries.append(summarize_trace(trace))
            if offset:
                summaries = summaries[offset:]
            if limit is not None and limit > 0:
                summaries = summaries[:limit]
            if len(summaries) < (limit or 0) and sql_limit and len(rows) >= sql_limit:
                return super().list_trace_summaries(project, tag, since, limit, offset)
            return summaries

        rows = self._query_traces(project, since, limit, offset)
        return [summarize_trace(normalize_trace(raw)) for raw in rows]


def _ensure_dict(value: Any) -> dict[str, Any] | None:
    """Ensure value is a dict — data is stored as JSON string."""
    if isinstance(value, dict):
        return value
    if isinstance(value, str):
        try:
            return json.loads(value)
        except (json.JSONDecodeError, TypeError):
            return None
    return None
