//! Repository pattern over the SQLite schema.
//!
//! `LocalDatabaseRepository` is the single concrete type that owns the
//! `Connection`. Repository traits (`BookmarkRepository`,
//! `SettingsRepository`) describe the API surface the rest of the Rust
//! core depends on. This mirrors the production split where the
//! Chanora core consumes traits rather than the concrete SQLite type
//! (SAD-067, SDD-077).

use std::path::Path;

use rusqlite::{params, Connection, OptionalExtension};
use thiserror::Error;

use crate::migrate::{canonical_migrations, Migrator};

#[derive(Debug, Error)]
pub enum RepoError {
    #[error("sqlite error: {0}")]
    Sqlite(#[from] rusqlite::Error),
    #[error("migration error: {0}")]
    Migration(#[from] crate::migrate::MigrationError),
    #[error("not found")]
    NotFound,
}

// ---------- DTOs ----------

#[derive(Debug, Clone, PartialEq, Eq)]
pub struct Bookmark {
    pub id: i64,
    pub server_name: String,
    pub server_host: String,
    /// Stable reference to a secret in platform secure storage.
    /// **Not** the secret itself (SAD-067, see secure-storage-spike).
    pub identity_ref: String,
    pub nickname: Option<String>,
    /// Unix epoch seconds; `None` if never connected.
    pub last_connected_at: Option<i64>,
    pub created_at: i64,
}

#[derive(Debug, Clone, PartialEq, Eq)]
pub struct Setting {
    pub key: String,
    pub value: String,
}

// ---------- Traits ----------

pub trait BookmarkRepository {
    fn insert(&self, b: NewBookmark<'_>) -> Result<i64, RepoError>;
    fn get(&self, id: i64) -> Result<Bookmark, RepoError>;
    fn list_ordered(&self) -> Result<Vec<Bookmark>, RepoError>;
    fn touch_connected(&self, id: i64, at_epoch_secs: i64) -> Result<(), RepoError>;
    fn delete(&self, id: i64) -> Result<(), RepoError>;
}

pub trait SettingsRepository {
    fn put(&self, key: &str, value: &str) -> Result<(), RepoError>;
    fn get(&self, key: &str) -> Result<Option<String>, RepoError>;
    fn delete(&self, key: &str) -> Result<bool, RepoError>;
}

#[derive(Debug, Clone)]
pub struct NewBookmark<'a> {
    pub server_name: &'a str,
    pub server_host: &'a str,
    pub identity_ref: &'a str,
    pub nickname: Option<&'a str>,
}

// ---------- Concrete repository ----------

pub struct LocalDatabaseRepository {
    conn: Connection,
}

impl LocalDatabaseRepository {
    /// Open (or create) a SQLite database at `path` and bring the
    /// schema up to date via the migrator.
    pub fn open(path: &Path) -> Result<Self, RepoError> {
        let mut conn = Connection::open(path)?;
        Self::pragmas(&conn)?;
        let migrator = Migrator::new(canonical_migrations()).map_err(RepoError::Migration)?;
        migrator.run(&mut conn).map_err(RepoError::Migration)?;
        Ok(Self { conn })
    }

    /// Open an in-memory DB for tests.
    pub fn open_in_memory() -> Result<Self, RepoError> {
        let mut conn = Connection::open_in_memory()?;
        Self::pragmas(&conn)?;
        let migrator = Migrator::new(canonical_migrations()).map_err(RepoError::Migration)?;
        migrator.run(&mut conn).map_err(RepoError::Migration)?;
        Ok(Self { conn })
    }

    fn pragmas(conn: &Connection) -> Result<(), RepoError> {
        // Durability + concurrency defaults that match production
        // expectations for a desktop/mobile embedded DB.
        conn.execute_batch(
            "PRAGMA journal_mode = WAL;
             PRAGMA synchronous  = NORMAL;
             PRAGMA foreign_keys = ON;",
        )?;
        Ok(())
    }

    /// Current schema version (PRAGMA user_version).
    pub fn schema_version(&self) -> Result<u32, RepoError> {
        let v: i64 = self
            .conn
            .query_row("PRAGMA user_version", [], |r| r.get(0))?;
        Ok(v as u32)
    }
}

impl BookmarkRepository for LocalDatabaseRepository {
    fn insert(&self, b: NewBookmark<'_>) -> Result<i64, RepoError> {
        self.conn.execute(
            "INSERT INTO bookmarks (server_name, server_host, identity_ref, nickname)
             VALUES (?1, ?2, ?3, ?4)",
            params![b.server_name, b.server_host, b.identity_ref, b.nickname],
        )?;
        Ok(self.conn.last_insert_rowid())
    }

    fn get(&self, id: i64) -> Result<Bookmark, RepoError> {
        self.conn
            .query_row(
                "SELECT id, server_name, server_host, identity_ref, nickname,
                        last_connected_at, created_at
                   FROM bookmarks WHERE id = ?1",
                params![id],
                row_to_bookmark,
            )
            .optional()?
            .ok_or(RepoError::NotFound)
    }

    fn list_ordered(&self) -> Result<Vec<Bookmark>, RepoError> {
        let mut stmt = self.conn.prepare(
            "SELECT id, server_name, server_host, identity_ref, nickname,
                    last_connected_at, created_at
               FROM bookmarks
              ORDER BY last_connected_at DESC NULLS LAST, server_name ASC",
        )?;
        let rows = stmt.query_map([], row_to_bookmark)?;
        let mut out = Vec::new();
        for r in rows {
            out.push(r?);
        }
        Ok(out)
    }

    fn touch_connected(&self, id: i64, at_epoch_secs: i64) -> Result<(), RepoError> {
        let n = self.conn.execute(
            "UPDATE bookmarks SET last_connected_at = ?2 WHERE id = ?1",
            params![id, at_epoch_secs],
        )?;
        if n == 0 {
            return Err(RepoError::NotFound);
        }
        Ok(())
    }

    fn delete(&self, id: i64) -> Result<(), RepoError> {
        let n = self
            .conn
            .execute("DELETE FROM bookmarks WHERE id = ?1", params![id])?;
        if n == 0 {
            return Err(RepoError::NotFound);
        }
        Ok(())
    }
}

impl SettingsRepository for LocalDatabaseRepository {
    fn put(&self, key: &str, value: &str) -> Result<(), RepoError> {
        self.conn.execute(
            "INSERT INTO settings (key, value) VALUES (?1, ?2)
             ON CONFLICT(key) DO UPDATE SET value = excluded.value",
            params![key, value],
        )?;
        Ok(())
    }

    fn get(&self, key: &str) -> Result<Option<String>, RepoError> {
        Ok(self
            .conn
            .query_row(
                "SELECT value FROM settings WHERE key = ?1",
                params![key],
                |r| r.get(0),
            )
            .optional()?)
    }

    fn delete(&self, key: &str) -> Result<bool, RepoError> {
        let n = self
            .conn
            .execute("DELETE FROM settings WHERE key = ?1", params![key])?;
        Ok(n > 0)
    }
}

fn row_to_bookmark(row: &rusqlite::Row<'_>) -> rusqlite::Result<Bookmark> {
    Ok(Bookmark {
        id: row.get(0)?,
        server_name: row.get(1)?,
        server_host: row.get(2)?,
        identity_ref: row.get(3)?,
        nickname: row.get(4)?,
        last_connected_at: row.get(5)?,
        created_at: row.get(6)?,
    })
}
