"""
LLM response cache backed by SQLite.

Avoids redundant API calls by caching responses keyed on
SHA-256(model_name || system_prompt || user_prompt).
"""

from __future__ import annotations

import sqlite3
import time
from typing import Optional

from tracescope.utils.hashing import sha256_key


class LLMResponseCache:
    """SQLite-backed LLM response cache.

    Args:
        db_path: Path to the SQLite database file.
        enabled: If False, all lookups miss and stores are skipped.
    """

    def __init__(self, db_path: str, enabled: bool = True):
        self.db_path = db_path
        self.enabled = enabled
        if enabled:
            self._conn = sqlite3.connect(db_path, check_same_thread=False)
            self._init_table()
        else:
            self._conn = None

    def _init_table(self):
        self._conn.execute(
            """
            CREATE TABLE IF NOT EXISTS llm_cache (
                hash        TEXT PRIMARY KEY,
                model_name  TEXT NOT NULL,
                response    TEXT NOT NULL,
                created_at  REAL NOT NULL
            )
            """
        )
        self._conn.commit()

    def get(self, model_name: str, system_prompt: str, user_prompt: str) -> Optional[str]:
        """Look up a cached response. Returns None on miss."""
        if not self.enabled:
            return None
        key = sha256_key(model_name, system_prompt, user_prompt)
        row = self._conn.execute(
            "SELECT response FROM llm_cache WHERE hash = ?", (key,)
        ).fetchone()
        return row[0] if row else None

    def put(self, model_name: str, system_prompt: str, user_prompt: str, response: str):
        """Store a response in the cache."""
        if not self.enabled:
            return
        key = sha256_key(model_name, system_prompt, user_prompt)
        self._conn.execute(
            """
            INSERT OR REPLACE INTO llm_cache (hash, model_name, response, created_at)
            VALUES (?, ?, ?, ?)
            """,
            (key, model_name, response, time.time()),
        )
        self._conn.commit()

    def clear(self):
        """Clear all cached responses."""
        if self._conn:
            self._conn.execute("DELETE FROM llm_cache")
            self._conn.commit()

    def close(self):
        if self._conn:
            self._conn.close()


class ResultCache:
    """SQLite-backed cache for expensive ML computation results.

    Caches clustering, dimension reduction, and velocity grid results
    keyed on embeddings fingerprint (matching Android's DimensionReducerAdapter).

    Uses the same SQLite database as LLMResponseCache but a separate table.

    Args:
        db_path: Path to the SQLite database file.
        enabled: If False, all lookups miss and stores are skipped.
    """

    def __init__(self, db_path: str, enabled: bool = True):
        self.db_path = db_path
        self.enabled = enabled
        if enabled:
            self._conn = sqlite3.connect(db_path, check_same_thread=False)
            self._init_table()
        else:
            self._conn = None

    def _init_table(self):
        self._conn.execute(
            """
            CREATE TABLE IF NOT EXISTS result_cache (
                key         TEXT PRIMARY KEY,
                step        TEXT NOT NULL,
                data        BLOB NOT NULL,
                created_at  REAL NOT NULL
            )
            """
        )
        self._conn.commit()

    def get_result(self, step: str, fingerprint: str) -> Optional[bytes]:
        """Look up a cached computation result.

        Args:
            step: Pipeline step name (e.g. "clustering", "dim_reduction", "velocity_grid").
            fingerprint: Embeddings fingerprint (SHA-256 hex digest).

        Returns:
            Cached data as bytes, or None on miss.
        """
        if not self.enabled:
            return None
        key = sha256_key(step, fingerprint)
        row = self._conn.execute(
            "SELECT data FROM result_cache WHERE key = ?", (key,)
        ).fetchone()
        return row[0] if row else None

    def put_result(self, step: str, fingerprint: str, data: bytes):
        """Store a computation result in the cache.

        Args:
            step: Pipeline step name.
            fingerprint: Embeddings fingerprint.
            data: Result data as bytes (JSON-encoded or raw numpy).
        """
        if not self.enabled:
            return
        key = sha256_key(step, fingerprint)
        self._conn.execute(
            """
            INSERT OR REPLACE INTO result_cache (key, step, data, created_at)
            VALUES (?, ?, ?, ?)
            """,
            (key, step, data, time.time()),
        )
        self._conn.commit()

    def clear(self):
        """Clear all cached results."""
        if self._conn:
            self._conn.execute("DELETE FROM result_cache")
            self._conn.commit()

    def close(self):
        if self._conn:
            self._conn.close()
