//! # Response Cache
//!
//! Avoid burning tokens and reduce latency on repeated LLM prompts by caching
//! responses.
//!
//! Stores LLM responses in a separate SQLite table keyed by a SHA-256 hash of
//! `(model, system_prompt, user_prompt)`. Entries expire after a
//! configurable TTL (Time To Live). The cache is optional and disabled by
//! default — users opt in via `[memory] response_cache_enabled = true`.
//!
//! The cache is stored in `response_cache.db` within the workspace's `memory`
//! directory. This separation from the main `brain.db` allows the cache to be
//! cleared or deleted without affecting permanent memories.

use anyhow::Result;
use chrono::{Duration, Local};
use parking_lot::Mutex;
use rusqlite::{params, Connection};
use sha2::{Digest, Sha256};
use std::path::{Path, PathBuf};

/// Response cache backed by a dedicated SQLite database.
///
/// Handles storage, retrieval, and eviction (TTL and LRU) of LLM responses.
pub struct ResponseCache {
    /// Thread-safe connection to the SQLite database.
    conn: Mutex<Connection>,
    /// Path to the SQLite database file.
    #[allow(dead_code)]
    db_path: PathBuf,
    /// Time-to-live for cache entries in minutes.
    ttl_minutes: i64,
    /// Maximum number of entries to keep in the cache.
    max_entries: usize,
}

impl ResponseCache {
    /// Open (or create) the response cache database.
    ///
    /// # Arguments
    ///
    /// * `workspace_dir` - Path to the project workspace.
    /// * `ttl_minutes` - How long each entry should remain valid.
    /// * `max_entries` - The maximum number of entries to store before LRU eviction.
    ///
    /// # Errors
    ///
    /// Returns an error if the database directory cannot be created or if the
    /// SQLite connection fails to open.
    pub fn new(workspace_dir: &Path, ttl_minutes: u32, max_entries: usize) -> Result<Self> {
        let db_dir = workspace_dir.join("memory");
        std::fs::create_dir_all(&db_dir)?;
        let db_path = db_dir.join("response_cache.db");

        let conn = Connection::open(&db_path)?;

        // Performance tuning for SQLite.
        conn.execute_batch(
            "PRAGMA journal_mode = WAL;
             PRAGMA synchronous  = NORMAL;
             PRAGMA temp_store   = MEMORY;",
        )?;

        // Initialize the cache table and indices.
        conn.execute_batch(
            "CREATE TABLE IF NOT EXISTS response_cache (
                prompt_hash TEXT PRIMARY KEY,
                model       TEXT NOT NULL,
                response    TEXT NOT NULL,
                token_count INTEGER NOT NULL DEFAULT 0,
                created_at  TEXT NOT NULL,
                accessed_at TEXT NOT NULL,
                hit_count   INTEGER NOT NULL DEFAULT 0
            );
            CREATE INDEX IF NOT EXISTS idx_rc_accessed ON response_cache(accessed_at);
            CREATE INDEX IF NOT EXISTS idx_rc_created ON response_cache(created_at);",
        )?;

        Ok(Self {
            conn: Mutex::new(conn),
            db_path,
            ttl_minutes: i64::from(ttl_minutes),
            max_entries,
        })
    }

    /// Build a deterministic cache key from model + system prompt + user prompt.
    ///
    /// Uses SHA-256 to create a unique identifier for the specific combination
    /// of model and inputs.
    ///
    /// # Arguments
    ///
    /// * `model` - The name of the LLM model (e.g., "gpt-4o").
    /// * `system_prompt` - The optional system instruction prompt.
    /// * `user_prompt` - The main user query/prompt.
    pub fn cache_key(model: &str, system_prompt: Option<&str>, user_prompt: &str) -> String {
        let mut hasher = Sha256::new();
        hasher.update(model.as_bytes());
        hasher.update(b"|");
        if let Some(sys) = system_prompt {
            hasher.update(sys.as_bytes());
        }
        hasher.update(b"|");
        hasher.update(user_prompt.as_bytes());
        let hash = hasher.finalize();
        format!("{:064x}", hash)
    }

    /// Look up a cached response. Returns `None` on miss or expired entry.
    ///
    /// If a match is found, it updates the `accessed_at` timestamp and
    /// increments the `hit_count`.
    ///
    /// # Arguments
    ///
    /// * `key` - The SHA-256 cache key generated by [`Self::cache_key`].
    pub fn get(&self, key: &str) -> Result<Option<String>> {
        let conn = self.conn.lock();

        let now = Local::now();
        let cutoff = (now - Duration::minutes(self.ttl_minutes)).to_rfc3339();

        // Retrieve the response only if it hasn't expired according to the TTL.
        let mut stmt = conn.prepare(
            "SELECT response FROM response_cache
             WHERE prompt_hash = ?1 AND created_at > ?2",
        )?;

        let result: Option<String> = stmt.query_row(params![key, cutoff], |row| row.get(0)).ok();

        if result.is_some() {
            // Bump hit count and accessed_at for LRU tracking and statistics.
            let now_str = now.to_rfc3339();
            conn.execute(
                "UPDATE response_cache
                 SET accessed_at = ?1, hit_count = hit_count + 1
                 WHERE prompt_hash = ?2",
                params![now_str, key],
            )?;
        }

        Ok(result)
    }

    /// Store a response in the cache.
    ///
    /// This also triggers background maintenance:
    /// 1. Evicts entries that have exceeded the TTL.
    /// 2. Performs LRU (Least Recently Used) eviction if the cache size
    ///    exceeds `max_entries`.
    ///
    /// # Arguments
    ///
    /// * `key` - The unique SHA-256 key for this prompt.
    /// * `model` - The model that generated the response.
    /// * `response` - The generated text response.
    /// * `token_count` - The number of tokens used (for savings tracking).
    pub fn put(&self, key: &str, model: &str, response: &str, token_count: u32) -> Result<()> {
        let conn = self.conn.lock();

        let now = Local::now().to_rfc3339();

        // Insert the new entry or replace an existing one.
        conn.execute(
            "INSERT OR REPLACE INTO response_cache
             (prompt_hash, model, response, token_count, created_at, accessed_at, hit_count)
             VALUES (?1, ?2, ?3, ?4, ?5, ?6, 0)",
            params![key, model, response, token_count, now, now],
        )?;

        // 1. Evict expired entries (TTL-based)
        let cutoff = (Local::now() - Duration::minutes(self.ttl_minutes)).to_rfc3339();
        conn.execute(
            "DELETE FROM response_cache WHERE created_at <= ?1",
            params![cutoff],
        )?;

        // 2. LRU eviction (Size-based)
        // If we have more than max_entries, delete the oldest entries based on accessed_at.
        #[allow(clippy::cast_possible_wrap)]
        let max = self.max_entries as i64;
        conn.execute(
            "DELETE FROM response_cache WHERE prompt_hash IN (
                SELECT prompt_hash FROM response_cache
                ORDER BY accessed_at ASC
                LIMIT MAX(0, (SELECT COUNT(*) FROM response_cache) - ?1)
            )",
            params![max],
        )?;

        Ok(())
    }

    /// Return cache statistics.
    ///
    /// Returns a tuple of `(total_entries, total_hits, total_tokens_saved)`.
    pub fn stats(&self) -> Result<(usize, u64, u64)> {
        let conn = self.conn.lock();

        // Count total active entries in the cache.
        let count: i64 =
            conn.query_row("SELECT COUNT(*) FROM response_cache", [], |row| row.get(0))?;

        // Sum up total cache hits.
        let hits: i64 = conn.query_row(
            "SELECT COALESCE(SUM(hit_count), 0) FROM response_cache",
            [],
            |row| row.get(0),
        )?;

        // Calculate total tokens saved (token_count * hit_count for each entry).
        let tokens_saved: i64 = conn.query_row(
            "SELECT COALESCE(SUM(token_count * hit_count), 0) FROM response_cache",
            [],
            |row| row.get(0),
        )?;

        #[allow(clippy::cast_sign_loss, clippy::cast_possible_truncation)]
        Ok((count as usize, hits as u64, tokens_saved as u64))
    }

    /// Wipe the entire cache.
    ///
    /// Returns the number of entries deleted.
    pub fn clear(&self) -> Result<usize> {
        let conn = self.conn.lock();

        let affected = conn.execute("DELETE FROM response_cache", [])?;
        Ok(affected)
    }
}

#[cfg(test)]
mod tests {
    // ... (tests remain unchanged)
