#![allow(dead_code)] // Phase 0: types defined now, consumed in Phase 1+

//! Database access layer for the TUI.
//!
//! Provides a read pool (3 connections, round-robin) plus a dedicated writer
//! connection. All connections use WAL mode and busy_timeout for concurrency.
//!
//! The TUI operates read-heavy: parallel queries for dashboard, list views,
//! and prefetch. Writes are rare (TUI-local state: scroll positions, bookmarks).

use std::path::Path;
use std::sync::Mutex;
use std::sync::atomic::{AtomicUsize, Ordering};

use anyhow::{Context, Result};
use rusqlite::Connection;

/// Number of reader connections in the pool.
const READER_COUNT: usize = 3;

// ---------------------------------------------------------------------------
// DbManager
// ---------------------------------------------------------------------------

/// Manages a pool of read-only connections plus a dedicated writer.
///
/// Designed for `Arc<DbManager>` sharing across FrankenTUI's `Cmd::task`
/// background threads. Each reader is individually `Mutex`-protected so
/// concurrent tasks can query different readers without blocking.
pub struct DbManager {
    readers: Vec<Mutex<Connection>>,
    writer: Mutex<Connection>,
    next_reader: AtomicUsize,
}

impl DbManager {
    /// Open a database at `path` with 3 reader + 1 writer connections.
    ///
    /// All connections get WAL mode, 5000ms busy_timeout, and foreign keys.
    /// Reader connections additionally set `query_only = ON` as a safety guard.
    pub fn open(path: &Path) -> Result<Self> {
        let mut readers = Vec::with_capacity(READER_COUNT);
        for i in 0..READER_COUNT {
            let conn =
                open_connection(path).with_context(|| format!("opening reader connection {i}"))?;
            conn.pragma_update(None, "query_only", "ON")
                .context("setting query_only on reader")?;
            readers.push(Mutex::new(conn));
        }

        let writer = open_connection(path).context("opening writer connection")?;

        Ok(Self {
            readers,
            writer: Mutex::new(writer),
            next_reader: AtomicUsize::new(0),
        })
    }

    /// Execute a read-only query against the pool.
    ///
    /// Selects the next reader via round-robin. The connection is borrowed
    /// for the duration of `f` and cannot leak outside.
    pub fn with_reader<F, T>(&self, f: F) -> Result<T>
    where
        F: FnOnce(&Connection) -> Result<T>,
    {
        let idx = self.next_reader.fetch_add(1, Ordering::Relaxed) % READER_COUNT;
        let conn = self.readers[idx].lock().expect("reader mutex poisoned");
        f(&conn)
    }

    /// Execute a write operation against the dedicated writer.
    ///
    /// Serialized via a single `Mutex`. The TUI writes infrequently
    /// (bookmarks, scroll state) so contention is negligible.
    pub fn with_writer<F, T>(&self, f: F) -> Result<T>
    where
        F: FnOnce(&Connection) -> Result<T>,
    {
        let conn = self.writer.lock().expect("writer mutex poisoned");
        f(&conn)
    }
}

// ---------------------------------------------------------------------------
// Connection setup
// ---------------------------------------------------------------------------

/// Open a single SQLite connection with TUI-appropriate pragmas.
///
/// Mirrors lore's `create_connection` pragmas (WAL, busy_timeout, etc.)
/// but skips the sqlite-vec extension registration — the TUI reads standard
/// tables only, never vec0 virtual tables.
fn open_connection(path: &Path) -> Result<Connection> {
    let conn = Connection::open(path).context("opening SQLite database")?;

    conn.pragma_update(None, "journal_mode", "WAL")?;
    conn.pragma_update(None, "synchronous", "NORMAL")?;
    conn.pragma_update(None, "foreign_keys", "ON")?;
    conn.pragma_update(None, "busy_timeout", 5000)?;
    conn.pragma_update(None, "temp_store", "MEMORY")?;

    Ok(conn)
}

// ---------------------------------------------------------------------------
// Tests
// ---------------------------------------------------------------------------

#[cfg(test)]
mod tests {
    use super::*;
    use std::sync::Arc;

    /// Create a temporary database file for testing.
    ///
    /// Uses an atomic counter + thread ID to guarantee unique paths even
    /// when tests run in parallel.
    fn test_db_path() -> std::path::PathBuf {
        use std::sync::atomic::AtomicU64;
        static COUNTER: AtomicU64 = AtomicU64::new(0);
        let n = COUNTER.fetch_add(1, Ordering::Relaxed);
        let dir = std::env::temp_dir().join("lore-tui-tests");
        std::fs::create_dir_all(&dir).expect("create test dir");
        dir.join(format!(
            "test-{}-{:?}-{n}.db",
            std::process::id(),
            std::thread::current().id(),
        ))
    }

    fn create_test_table(conn: &Connection) {
        conn.execute_batch(
            "CREATE TABLE IF NOT EXISTS test_items (id INTEGER PRIMARY KEY, name TEXT);",
        )
        .expect("create test table");
    }

    #[test]
    fn test_dbmanager_opens_successfully() {
        let path = test_db_path();
        let db = DbManager::open(&path).expect("open");
        // Writer creates the test table
        db.with_writer(|conn| {
            create_test_table(conn);
            Ok(())
        })
        .expect("create table via writer");
    }

    #[test]
    fn test_reader_is_query_only() {
        let path = test_db_path();
        let db = DbManager::open(&path).expect("open");

        // Create table via writer first
        db.with_writer(|conn| {
            create_test_table(conn);
            Ok(())
        })
        .unwrap();

        // Attempt INSERT via reader — should fail
        let result = db.with_reader(|conn| {
            conn.execute("INSERT INTO test_items (name) VALUES ('boom')", [])
                .map_err(|e| anyhow::anyhow!(e))?;
            Ok(())
        });
        assert!(result.is_err(), "reader should reject writes");
    }

    #[test]
    fn test_writer_allows_mutations() {
        let path = test_db_path();
        let db = DbManager::open(&path).expect("open");

        db.with_writer(|conn| {
            create_test_table(conn);
            conn.execute("INSERT INTO test_items (name) VALUES ('hello')", [])?;
            let count: i64 = conn.query_row("SELECT COUNT(*) FROM test_items", [], |r| r.get(0))?;
            assert_eq!(count, 1);
            Ok(())
        })
        .expect("writer should allow mutations");
    }

    #[test]
    fn test_round_robin_rotates_readers() {
        let path = test_db_path();
        let db = DbManager::open(&path).expect("open");

        // Call with_reader 6 times — should cycle through readers 0,1,2,0,1,2
        for expected_cycle in 0..2 {
            for expected_idx in 0..READER_COUNT {
                let current = db.next_reader.load(Ordering::Relaxed);
                assert_eq!(
                    current % READER_COUNT,
                    (expected_cycle * READER_COUNT + expected_idx) % READER_COUNT,
                );
                db.with_reader(|_conn| Ok(())).unwrap();
            }
        }
    }

    #[test]
    fn test_reader_can_read_writer_data() {
        let path = test_db_path();
        let db = DbManager::open(&path).expect("open");

        db.with_writer(|conn| {
            create_test_table(conn);
            conn.execute("INSERT INTO test_items (name) VALUES ('visible')", [])?;
            Ok(())
        })
        .unwrap();

        let name: String = db
            .with_reader(|conn| {
                let n: String =
                    conn.query_row("SELECT name FROM test_items WHERE id = 1", [], |r| r.get(0))?;
                Ok(n)
            })
            .expect("reader should see writer's data");

        assert_eq!(name, "visible");
    }

    #[test]
    fn test_dbmanager_is_send_sync() {
        fn assert_send_sync<T: Send + Sync>() {}
        assert_send_sync::<DbManager>();
    }

    #[test]
    fn test_concurrent_reads() {
        let path = test_db_path();
        let db = Arc::new(DbManager::open(&path).expect("open"));

        db.with_writer(|conn| {
            create_test_table(conn);
            for i in 0..10 {
                conn.execute(
                    "INSERT INTO test_items (name) VALUES (?1)",
                    [format!("item-{i}")],
                )?;
            }
            Ok(())
        })
        .unwrap();

        let mut handles = Vec::new();
        for _ in 0..6 {
            let db = Arc::clone(&db);
            handles.push(std::thread::spawn(move || {
                db.with_reader(|conn| {
                    let count: i64 =
                        conn.query_row("SELECT COUNT(*) FROM test_items", [], |r| r.get(0))?;
                    assert_eq!(count, 10);
                    Ok(())
                })
                .expect("concurrent read should succeed");
            }));
        }

        for h in handles {
            h.join().expect("thread should not panic");
        }
    }
}
