"""SQLite cache backend."""
from __future__ import annotations

import asyncio
import json
import sqlite3
import time
from typing import Any

from logos_adk.cache import CacheBackend, CacheEntry, _cosine_similarity
from logos_adk.persistence import get_sqlite_schema_version, set_sqlite_schema_version


CACHE_SCHEMA_KIND = "cache_backend"
CACHE_SCHEMA_VERSION = 1


class SQLiteCacheBackend(CacheBackend):
    """SQLite-backed cache with TTL support."""

    def __init__(self, path: str = ".logos_cache.db") -> None:
        self._path = path
        self._conn = sqlite3.connect(path, check_same_thread=False)
        self._conn.execute(
            """CREATE TABLE IF NOT EXISTS cache (
                key TEXT PRIMARY KEY,
                response_data TEXT NOT NULL,
                created_at TEXT NOT NULL,
                hit_count INTEGER DEFAULT 0,
                vector TEXT,
                scope TEXT,
                expires_at REAL
            )"""
        )
        recorded_version = get_sqlite_schema_version(self._conn, CACHE_SCHEMA_KIND)
        if recorded_version != CACHE_SCHEMA_VERSION:
            set_sqlite_schema_version(self._conn, CACHE_SCHEMA_KIND, CACHE_SCHEMA_VERSION)
        self._conn.commit()

    def close(self) -> None:
        """Close the underlying SQLite connection."""
        if self._conn is not None:
            try:
                self._conn.close()
            except Exception:
                pass
            self._conn = None  # type: ignore[assignment]

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

    def _get_sync(self, key: str) -> CacheEntry | None:
        row = self._conn.execute(
            "SELECT response_data, created_at, hit_count, vector, scope, expires_at "
            "FROM cache WHERE key = ?",
            (key,),
        ).fetchone()
        if row is None:
            return None

        response_data, created_at, hit_count, vector_json, scope, expires_at = row
        if expires_at is not None and time.time() > expires_at:
            self._conn.execute("DELETE FROM cache WHERE key = ?", (key,))
            self._conn.commit()
            return None

        self._conn.execute(
            "UPDATE cache SET hit_count = hit_count + 1 WHERE key = ?", (key,)
        )
        self._conn.commit()

        return CacheEntry(
            response_data=json.loads(response_data),
            created_at=created_at,
            hit_count=hit_count + 1,
            vector=json.loads(vector_json) if vector_json else None,
            scope=scope,
        )

    async def get(self, key: str) -> CacheEntry | None:
        return await asyncio.to_thread(self._get_sync, key)

    def _put_sync(self, key: str, entry: CacheEntry, ttl: int | None) -> None:
        expires_at = (time.time() + ttl) if ttl is not None else None
        self._conn.execute(
            """INSERT OR REPLACE INTO cache
               (key, response_data, created_at, hit_count, vector, scope, expires_at)
               VALUES (?, ?, ?, ?, ?, ?, ?)""",
            (
                key,
                json.dumps(entry.response_data),
                entry.created_at,
                entry.hit_count,
                json.dumps(entry.vector) if entry.vector else None,
                entry.scope,
                expires_at,
            ),
        )
        self._conn.commit()

    async def put(self, key: str, entry: CacheEntry, ttl: int | None) -> None:
        await asyncio.to_thread(self._put_sync, key, entry, ttl)

    def _invalidate_sync(self, key: str) -> None:
        self._conn.execute("DELETE FROM cache WHERE key = ?", (key,))
        self._conn.commit()

    async def invalidate(self, key: str) -> None:
        await asyncio.to_thread(self._invalidate_sync, key)

    def _clear_sync(self) -> None:
        self._conn.execute("DELETE FROM cache")
        self._conn.commit()

    async def clear(self) -> None:
        await asyncio.to_thread(self._clear_sync)

    def _get_stats_sync(self) -> dict[str, Any]:
        count = self._conn.execute("SELECT COUNT(*) FROM cache").fetchone()[0]
        hits = self._conn.execute("SELECT SUM(hit_count) FROM cache").fetchone()[0] or 0
        return {
            "backend": "SQLite",
            "total_entries": count,
            "total_hits": hits,
        }

    async def get_stats(self) -> dict[str, Any]:
        return await asyncio.to_thread(self._get_stats_sync)

    def _search_similar_sync(
        self,
        vector: list[float],
        threshold: float,
        limit: int = 1,
        scope: str | None = None,
    ) -> list[CacheEntry]:
        query = "SELECT key, response_data, created_at, hit_count, vector, scope, expires_at FROM cache WHERE vector IS NOT NULL"
        params: list[Any] = []
        if scope is not None:
            query += " AND scope = ?"
            params.append(scope)

        rows = self._conn.execute(query, params).fetchall()
        results: list[tuple[float, CacheEntry]] = []

        now = time.time()
        for _key, resp_data, created_at, hit_count, vec_json, sc, expires_at in rows:
            if expires_at is not None and now > expires_at:
                continue
            stored_vec = json.loads(vec_json)
            sim = _cosine_similarity(vector, stored_vec)
            if sim >= threshold:
                results.append((sim, CacheEntry(
                    response_data=json.loads(resp_data),
                    created_at=created_at,
                    hit_count=hit_count,
                    vector=stored_vec,
                    scope=sc,
                )))

        results.sort(key=lambda x: x[0], reverse=True)
        return [entry for _, entry in results[:limit]]

    async def search_similar(
        self,
        vector: list[float],
        threshold: float,
        limit: int = 1,
        scope: str | None = None,
    ) -> list[CacheEntry]:
        return await asyncio.to_thread(self._search_similar_sync, vector, threshold, limit, scope)
