//! Forward-only schema migration mechanism.
//!
//! Approach: each migration is a `&'static str` SQL script applied in
//! order. The current schema version is tracked in SQLite's
//! `PRAGMA user_version`. Running the migrator is idempotent — already-
//! applied versions are skipped.
//!
//! Why not an external migration framework? At this stage we want the
//! mechanism in the audit surface to be explainable in ~50 lines and
//! to have no transitive crate dependencies. Production code may
//! promote this to `refinery`, `sqlx::migrate!`, or similar if the
//! complexity warrants.

use rusqlite::{Connection, TransactionBehavior};
use thiserror::Error;
use tracing::info;

/// A single migration step. `version` is monotonically increasing
/// starting at 1. `sql` is applied inside a transaction.
#[derive(Debug, Clone, Copy)]
pub struct Migration {
    pub version: u32,
    pub name: &'static str,
    pub sql: &'static str,
}

#[derive(Debug, Error)]
pub enum MigrationError {
    #[error("sqlite error during migration: {0}")]
    Sqlite(#[from] rusqlite::Error),
    #[error("migrations must be ordered by ascending version; got {got} after {prev}")]
    OutOfOrder { prev: u32, got: u32 },
    #[error("database is at version {db}, which is newer than the highest known migration {max}")]
    FromTheFuture { db: u32, max: u32 },
}

/// Forward-only migrator over a fixed list of `Migration` steps.
pub struct Migrator {
    migrations: Vec<Migration>,
}

impl Migrator {
    pub fn new(migrations: Vec<Migration>) -> Result<Self, MigrationError> {
        let mut prev = 0u32;
        for m in &migrations {
            if m.version <= prev {
                return Err(MigrationError::OutOfOrder { prev, got: m.version });
            }
            prev = m.version;
        }
        Ok(Self { migrations })
    }

    pub fn highest_version(&self) -> u32 {
        self.migrations.last().map(|m| m.version).unwrap_or(0)
    }

    /// Apply every pending migration to `conn`. Idempotent: if the DB
    /// is already at the highest version, this is a no-op. Each
    /// migration is wrapped in an IMMEDIATE transaction; on failure
    /// the transaction is rolled back and the DB version is unchanged.
    pub fn run(&self, conn: &mut Connection) -> Result<u32, MigrationError> {
        let current: u32 =
            conn.query_row("PRAGMA user_version", [], |r| r.get::<_, i64>(0))? as u32;

        if current > self.highest_version() {
            return Err(MigrationError::FromTheFuture {
                db: current,
                max: self.highest_version(),
            });
        }

        let pending: Vec<&Migration> = self
            .migrations
            .iter()
            .filter(|m| m.version > current)
            .collect();

        if pending.is_empty() {
            info!(target: "spike", "no migrations to apply (already at v{current})");
            return Ok(current);
        }

        for m in pending {
            info!(target: "spike", "applying migration v{} {:?}", m.version, m.name);
            let tx = conn.transaction_with_behavior(TransactionBehavior::Immediate)?;
            tx.execute_batch(m.sql)?;
            // PRAGMA user_version cannot be parameterised; the version
            // is from our own static migration list and is therefore
            // safe to interpolate.
            tx.execute_batch(&format!("PRAGMA user_version = {}", m.version))?;
            tx.commit()?;
        }

        let final_v: u32 =
            conn.query_row("PRAGMA user_version", [], |r| r.get::<_, i64>(0))? as u32;
        Ok(final_v)
    }
}

/// Canonical Chanora PoC migration list.
///
/// Each version is intentionally small and self-contained. Migrations
/// in the production crate will live in `migrations/` as separate
/// files; the PoC keeps them inline for visibility.
pub fn canonical_migrations() -> Vec<Migration> {
    vec![
        Migration {
            version: 1,
            name: "initial bookmarks + settings",
            sql: r#"
                CREATE TABLE bookmarks (
                    id            INTEGER PRIMARY KEY AUTOINCREMENT,
                    server_name   TEXT NOT NULL,
                    server_host   TEXT NOT NULL,
                    identity_ref  TEXT NOT NULL,
                    created_at    INTEGER NOT NULL DEFAULT (strftime('%s','now'))
                );
                CREATE UNIQUE INDEX ix_bookmarks_host_identity
                    ON bookmarks(server_host, identity_ref);

                CREATE TABLE settings (
                    key   TEXT PRIMARY KEY,
                    value TEXT NOT NULL
                );
            "#,
        },
        Migration {
            version: 2,
            name: "add bookmarks.nickname",
            sql: r#"
                ALTER TABLE bookmarks ADD COLUMN nickname TEXT;
            "#,
        },
        Migration {
            version: 3,
            name: "add bookmarks.last_connected_at",
            sql: r#"
                ALTER TABLE bookmarks ADD COLUMN last_connected_at INTEGER;
                CREATE INDEX ix_bookmarks_last_connected
                    ON bookmarks(last_connected_at DESC);
            "#,
        },
    ]
}
