"""LLMHosts audit logger -- async SQLite-backed request audit trail.

Every proxied API request is recorded with full routing, cost, and
performance metadata.  Supports filtered queries and time-period summaries
for the dashboard and CLI.
"""

from __future__ import annotations

import contextlib
import json
import logging
from datetime import datetime, timedelta, timezone
from typing import TYPE_CHECKING

import aiosqlite

from llmhosts.audit.hmac_chain import HMACChain
from llmhosts.audit.models import AuditEntry, AuditQuery, AuditSummary

if TYPE_CHECKING:
    from pathlib import Path

logger = logging.getLogger(__name__)

# ---------------------------------------------------------------------------
# SQL
# ---------------------------------------------------------------------------

_CREATE_TABLE = """
CREATE TABLE IF NOT EXISTS audit_log (
    request_id          TEXT PRIMARY KEY,
    timestamp           TEXT NOT NULL,
    source_ip           TEXT NOT NULL,
    method              TEXT NOT NULL,
    path                TEXT NOT NULL,
    dialect             TEXT NOT NULL,
    model_requested     TEXT NOT NULL,
    model_used          TEXT NOT NULL,
    backend_type        TEXT NOT NULL,
    routing_tier        TEXT NOT NULL,
    routing_reasoning   TEXT NOT NULL DEFAULT '',
    routing_confidence  REAL NOT NULL DEFAULT 0.0,
    input_tokens        INTEGER NOT NULL DEFAULT 0,
    output_tokens       INTEGER NOT NULL DEFAULT 0,
    actual_cost         REAL NOT NULL DEFAULT 0.0,
    cloud_equivalent_cost REAL NOT NULL DEFAULT 0.0,
    cached              INTEGER NOT NULL DEFAULT 0,
    cache_tier          TEXT,
    latency_ms          REAL NOT NULL DEFAULT 0.0,
    status_code         INTEGER NOT NULL DEFAULT 200,
    error               TEXT,
    api_key_id          TEXT NOT NULL DEFAULT '',
    user_id             TEXT NOT NULL DEFAULT '',
    hmac                TEXT
);
"""

_CREATE_INDEXES = [
    "CREATE INDEX IF NOT EXISTS idx_audit_timestamp ON audit_log (timestamp);",
    "CREATE INDEX IF NOT EXISTS idx_audit_model ON audit_log (model_used);",
    "CREATE INDEX IF NOT EXISTS idx_audit_backend ON audit_log (backend_type);",
    "CREATE INDEX IF NOT EXISTS idx_audit_cached ON audit_log (cached);",
    "CREATE INDEX IF NOT EXISTS idx_audit_status ON audit_log (status_code);",
    "CREATE INDEX IF NOT EXISTS idx_audit_dialect ON audit_log (dialect);",
    "CREATE INDEX IF NOT EXISTS idx_audit_api_key ON audit_log (api_key_id);",
    "CREATE INDEX IF NOT EXISTS idx_audit_user ON audit_log (user_id);",
]

_INSERT = """
INSERT INTO audit_log (
    request_id, timestamp, source_ip, method, path, dialect,
    model_requested, model_used, backend_type,
    routing_tier, routing_reasoning, routing_confidence,
    input_tokens, output_tokens, actual_cost, cloud_equivalent_cost,
    cached, cache_tier, latency_ms, status_code, error,
    api_key_id, user_id, hmac
) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
"""

_ADD_HMAC_COLUMN = "ALTER TABLE audit_log ADD COLUMN hmac TEXT;"
_ADD_API_KEY_ID_COLUMN = "ALTER TABLE audit_log ADD COLUMN api_key_id TEXT NOT NULL DEFAULT '';"
_ADD_USER_ID_COLUMN = "ALTER TABLE audit_log ADD COLUMN user_id TEXT NOT NULL DEFAULT '';"


# ---------------------------------------------------------------------------
# Audit logger
# ---------------------------------------------------------------------------


class AuditLogger:
    """Async audit logger backed by SQLite.

    Each proxied request produces one :class:`AuditEntry` that is written to
    the ``audit_log`` table.  Supports filtered retrieval and time-window
    summaries.
    """

    def __init__(self, db_path: Path, hmac_key: bytes | None = None) -> None:
        self._db_path = db_path
        self._db: aiosqlite.Connection | None = None
        self._hmac_chain: HMACChain | None = None
        self._last_hmac: str = ""
        if hmac_key is not None:
            self._hmac_chain = HMACChain(hmac_key)
            self._last_hmac = self._hmac_chain.genesis_hmac()

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

    async def initialize(self) -> None:
        """Create audit table and indexes.  Enable WAL."""
        self._db_path.parent.mkdir(parents=True, exist_ok=True)
        self._db = await aiosqlite.connect(str(self._db_path))
        await self._db.execute("PRAGMA journal_mode=WAL;")
        await self._db.execute("PRAGMA synchronous=NORMAL;")
        await self._db.execute(_CREATE_TABLE)
        for idx_sql in _CREATE_INDEXES:
            await self._db.execute(idx_sql)

        # Migrate: add columns if they don't exist (for existing databases)
        with contextlib.suppress(Exception):
            await self._db.execute(_ADD_HMAC_COLUMN)
        with contextlib.suppress(Exception):
            await self._db.execute(_ADD_API_KEY_ID_COLUMN)
        with contextlib.suppress(Exception):
            await self._db.execute(_ADD_USER_ID_COLUMN)

        await self._db.commit()

        # Recover last HMAC from chain to continue appending
        if self._hmac_chain is not None:
            async with self._db.execute(
                "SELECT hmac FROM audit_log WHERE hmac IS NOT NULL ORDER BY timestamp DESC LIMIT 1"
            ) as cursor:
                row = await cursor.fetchone()
                if row and row[0]:
                    self._last_hmac = row[0]

        logger.info("AuditLogger initialised (db=%s, hmac=%s)", self._db_path, self._hmac_chain is not None)

    async def close(self) -> None:
        """Close database connection."""
        if self._db is not None:
            await self._db.close()
            self._db = None
            logger.debug("AuditLogger closed")

    def _ensure_db(self) -> aiosqlite.Connection:
        if self._db is None:
            raise RuntimeError("AuditLogger not initialised -- call initialize() first")
        return self._db

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

    @staticmethod
    def _iso(dt: datetime) -> str:
        return dt.isoformat()

    @staticmethod
    def _parse_dt(val: str | None) -> datetime | None:
        if val is None:
            return None
        return datetime.fromisoformat(val)

    @staticmethod
    def _row_to_entry(row: aiosqlite.Row) -> AuditEntry:
        """Convert a database row tuple to an AuditEntry."""
        return AuditEntry(
            request_id=row[0],
            timestamp=datetime.fromisoformat(row[1]),
            source_ip=row[2],
            method=row[3],
            path=row[4],
            dialect=row[5],
            model_requested=row[6],
            model_used=row[7],
            backend_type=row[8],
            routing_tier=row[9],
            routing_reasoning=row[10],
            routing_confidence=row[11],
            input_tokens=row[12],
            output_tokens=row[13],
            actual_cost=row[14],
            cloud_equivalent_cost=row[15],
            cached=bool(row[16]),
            cache_tier=row[17],
            latency_ms=row[18],
            status_code=row[19],
            error=row[20],
            api_key_id=row[21] if len(row) > 21 else "",
            user_id=row[22] if len(row) > 22 else "",
            hmac=row[23] if len(row) > 23 else None,
        )

    # ------------------------------------------------------------------
    # Public API
    # ------------------------------------------------------------------

    async def log(self, entry: AuditEntry) -> None:
        """Write an audit entry to the database.

        If an HMAC chain key was provided at init, computes a chained HMAC
        for tamper-evidence (TrustLedger).
        """
        db = self._ensure_db()

        # Compute HMAC chain if enabled
        entry_hmac: str | None = None
        if self._hmac_chain is not None:
            # Serialize entry WITHOUT hmac for HMAC computation
            entry_dict = entry.model_dump(mode="json", exclude={"hmac"})
            entry_json = json.dumps(entry_dict, sort_keys=True, default=str)
            entry_hmac = self._hmac_chain.compute_hmac(self._last_hmac, entry_json)
            self._last_hmac = entry_hmac

        await db.execute(
            _INSERT,
            (
                entry.request_id,
                self._iso(entry.timestamp),
                entry.source_ip,
                entry.method,
                entry.path,
                entry.dialect,
                entry.model_requested,
                entry.model_used,
                entry.backend_type,
                entry.routing_tier,
                entry.routing_reasoning,
                entry.routing_confidence,
                entry.input_tokens,
                entry.output_tokens,
                entry.actual_cost,
                entry.cloud_equivalent_cost,
                1 if entry.cached else 0,
                entry.cache_tier,
                entry.latency_ms,
                entry.status_code,
                entry.error,
                entry.api_key_id,
                entry.user_id,
                entry_hmac,
            ),
        )
        await db.commit()
        logger.debug(
            "Audit logged request_id=%s model=%s hmac=%s", entry.request_id, entry.model_used, bool(entry_hmac)
        )

    async def query(self, q: AuditQuery) -> list[AuditEntry]:
        """Query audit log with filters.

        Supports date range, model, backend type, and cache-only filters.
        Results ordered by timestamp descending (newest first).
        """
        db = self._ensure_db()

        conditions: list[str] = []
        params: list[str | int | float] = []

        if q.start_date is not None:
            conditions.append("timestamp >= ?")
            params.append(self._iso(q.start_date))
        if q.end_date is not None:
            conditions.append("timestamp <= ?")
            params.append(self._iso(q.end_date))
        if q.model is not None:
            conditions.append("(model_requested = ? OR model_used = ?)")
            params.extend([q.model, q.model])
        if q.backend_type is not None:
            conditions.append("backend_type = ?")
            params.append(q.backend_type)
        if q.cached_only:
            conditions.append("cached = 1")

        where_clause = " AND ".join(conditions) if conditions else "1=1"
        sql = (
            f"SELECT request_id, timestamp, source_ip, method, path, dialect, "  # nosec B608
            f"model_requested, model_used, backend_type, "
            f"routing_tier, routing_reasoning, routing_confidence, "
            f"input_tokens, output_tokens, actual_cost, cloud_equivalent_cost, "
            f"cached, cache_tier, latency_ms, status_code, error, "
            f"api_key_id, user_id, hmac "
            f"FROM audit_log WHERE {where_clause} "
            f"ORDER BY timestamp DESC LIMIT ? OFFSET ?"
        )
        params.extend([q.limit, q.offset])

        entries: list[AuditEntry] = []
        async with db.execute(sql, params) as cursor:
            async for row in cursor:
                entries.append(self._row_to_entry(row))

        return entries

    async def summary(self, period: str = "day") -> AuditSummary:
        """Generate summary for period (hour, day, week, month, all).

        Computes totals, averages, and breakdowns for the given time window.
        """
        db = self._ensure_db()
        now = datetime.now(timezone.utc)

        # Compute the start-of-period boundary
        period_deltas: dict[str, timedelta | None] = {
            "hour": timedelta(hours=1),
            "day": timedelta(days=1),
            "week": timedelta(weeks=1),
            "month": timedelta(days=30),
            "all": None,
        }
        delta = period_deltas.get(period)
        if delta is None and period != "all":
            delta = timedelta(days=1)  # fallback

        conditions: list[str] = []
        params: list[str] = []
        if delta is not None:
            start = now - delta
            conditions.append("timestamp >= ?")
            params.append(start.isoformat())

        where = " AND ".join(conditions) if conditions else "1=1"

        # Total requests
        async with db.execute(f"SELECT COUNT(*) FROM audit_log WHERE {where}", params) as cur:  # nosec B608
            total_requests = (await cur.fetchone())[0]  # type: ignore[index]

        # Unique models
        async with db.execute(f"SELECT COUNT(DISTINCT model_used) FROM audit_log WHERE {where}", params) as cur:  # nosec B608
            unique_models = (await cur.fetchone())[0]  # type: ignore[index]

        # By backend
        by_backend: dict[str, int] = {}
        async with db.execute(
            f"SELECT backend_type, COUNT(*) FROM audit_log WHERE {where} GROUP BY backend_type",  # nosec B608
            params,
        ) as cur:
            async for row in cur:
                by_backend[row[0]] = row[1]

        # By dialect
        by_dialect: dict[str, int] = {}
        async with db.execute(f"SELECT dialect, COUNT(*) FROM audit_log WHERE {where} GROUP BY dialect", params) as cur:  # nosec B608
            async for row in cur:
                by_dialect[row[0]] = row[1]

        # Cache hit rate
        async with db.execute(f"SELECT COALESCE(SUM(cached), 0), COUNT(*) FROM audit_log WHERE {where}", params) as cur:  # nosec B608
            row_cache = await cur.fetchone()
            cache_hits = row_cache[0] if row_cache else 0
            total_for_rate = row_cache[1] if row_cache else 0
        cache_hit_rate = cache_hits / total_for_rate if total_for_rate > 0 else 0.0

        # Average latency
        async with db.execute(f"SELECT COALESCE(AVG(latency_ms), 0.0) FROM audit_log WHERE {where}", params) as cur:  # nosec B608
            avg_latency = (await cur.fetchone())[0]  # type: ignore[index]

        # Total cost and savings
        async with db.execute(
            f"SELECT COALESCE(SUM(actual_cost), 0.0), "
            f"COALESCE(SUM(cloud_equivalent_cost - actual_cost), 0.0) "
            f"FROM audit_log WHERE {where}",  # nosec B608
            params,
        ) as cur:
            row_cost = await cur.fetchone()
            total_cost = row_cost[0] if row_cost else 0.0
            total_savings = row_cost[1] if row_cost else 0.0

        return AuditSummary(
            period=period,
            total_requests=total_requests,
            unique_models=unique_models,
            by_backend=by_backend,
            by_dialect=by_dialect,
            cache_hit_rate=round(cache_hit_rate, 4),
            avg_latency_ms=round(avg_latency, 2),
            total_cost=round(total_cost, 6),
            total_savings=round(total_savings, 6),
        )
