import sqlite3
import hashlib
import aiosqlite
from datetime import datetime
from pathlib import Path

DEFAULT_CACHE_DB = Path.home() / ".dracula" / "cache.db"


class _CacheBase:
    """Shared base for Cache and AsyncCache."""

    _SCHEMA = """
        CREATE TABLE IF NOT EXISTS response_cache (
            id           INTEGER PRIMARY KEY AUTOINCREMENT,
            session_id   TEXT NOT NULL,
            message_hash TEXT NOT NULL,
            response     TEXT NOT NULL,
            expires_at   REAL NOT NULL
        )
    """

    @staticmethod
    def _hash(message: str) -> str:
        return hashlib.sha256(message.encode()).hexdigest()

    def __init__(self, db_path=None, ttl: int = 0):
        self.db_path = Path(db_path) if db_path else DEFAULT_CACHE_DB
        self.db_path.parent.mkdir(parents=True, exist_ok=True)
        self.ttl = ttl  # seconds; 0 = disabled


class Cache(_CacheBase):
    """Synchronous SQLite-backed response cache with TTL expiry.

    Args:
        db_path (str | Path): Path to the cache database.
            Defaults to ``~/.dracula/cache.db``.
        ttl (int): Time-to-live in seconds for each cached entry.
            A value of ``0`` (the default) disables caching entirely.
    """

    def __init__(self, db_path=None, ttl: int = 0):
        super().__init__(db_path, ttl)
        if self.ttl > 0:
            self._init_db()

    def _init_db(self):
        with sqlite3.connect(self.db_path) as conn:
            conn.execute(self._SCHEMA)
            conn.commit()

    def get(self, session_id: str, message: str) -> str | None:
        """Return a cached response, or ``None`` on miss or if caching is disabled."""
        if self.ttl <= 0:
            return None
        h = self._hash(message)
        now = datetime.now().timestamp()
        with sqlite3.connect(self.db_path) as conn:
            row = conn.execute(
                "SELECT response FROM response_cache "
                "WHERE session_id=? AND message_hash=? AND expires_at>?",
                (session_id, h, now),
            ).fetchone()
        return row[0] if row else None

    def set(self, session_id: str, message: str, response: str):
        """Store a response in the cache."""
        if self.ttl <= 0:
            return
        h = self._hash(message)
        expires = datetime.now().timestamp() + self.ttl
        with sqlite3.connect(self.db_path) as conn:
            conn.execute(
                "INSERT INTO response_cache "
                "(session_id, message_hash, response, expires_at) VALUES (?,?,?,?)",
                (session_id, h, response, expires),
            )
            conn.commit()

    def invalidate(self, session_id: str):
        """Remove all cached responses for a session."""
        if self.ttl <= 0:
            return
        with sqlite3.connect(self.db_path) as conn:
            conn.execute(
                "DELETE FROM response_cache WHERE session_id=?", (session_id,)
            )
            conn.commit()

    def clear_expired(self):
        """Delete all entries whose TTL has elapsed."""
        if self.ttl <= 0:
            return
        now = datetime.now().timestamp()
        with sqlite3.connect(self.db_path) as conn:
            conn.execute(
                "DELETE FROM response_cache WHERE expires_at<=?", (now,)
            )
            conn.commit()


class AsyncCache(_CacheBase):
    """Asynchronous SQLite-backed response cache with TTL expiry.

    Args:
        db_path (str | Path): Path to the cache database.
            Defaults to ``~/.dracula/cache.db``.
        ttl (int): Time-to-live in seconds. ``0`` disables caching.
    """

    def __init__(self, db_path=None, ttl: int = 0):
        super().__init__(db_path, ttl)

    async def initialize(self):
        """Create the cache table. Must be called before first use."""
        if self.ttl <= 0:
            return
        async with aiosqlite.connect(self.db_path) as db:
            await db.execute(self._SCHEMA)
            await db.commit()

    async def get(self, session_id: str, message: str) -> str | None:
        """Return a cached response, or ``None`` on miss or if caching is disabled."""
        if self.ttl <= 0:
            return None
        h = self._hash(message)
        now = datetime.now().timestamp()
        async with aiosqlite.connect(self.db_path) as db:
            async with db.execute(
                "SELECT response FROM response_cache "
                "WHERE session_id=? AND message_hash=? AND expires_at>?",
                (session_id, h, now),
            ) as cursor:
                row = await cursor.fetchone()
        return row[0] if row else None

    async def set(self, session_id: str, message: str, response: str):
        """Store a response in the cache."""
        if self.ttl <= 0:
            return
        h = self._hash(message)
        expires = datetime.now().timestamp() + self.ttl
        async with aiosqlite.connect(self.db_path) as db:
            await db.execute(
                "INSERT INTO response_cache "
                "(session_id, message_hash, response, expires_at) VALUES (?,?,?,?)",
                (session_id, h, response, expires),
            )
            await db.commit()

    async def invalidate(self, session_id: str):
        """Remove all cached responses for a session."""
        if self.ttl <= 0:
            return
        async with aiosqlite.connect(self.db_path) as db:
            await db.execute(
                "DELETE FROM response_cache WHERE session_id=?", (session_id,)
            )
            await db.commit()