use crate::error::Error::MigrationError;
use crate::error::Result;
use include_dir::{Dir, DirEntry, include_dir};
use log::{debug, info};
use rusqlite::{OptionalExtension, Transaction, TransactionBehavior, params};
use sha2::{Digest, Sha384};
use yaak_database::SqlitePool;

static MIGRATIONS_DIR: Dir = include_dir!("$CARGO_MANIFEST_DIR/migrations");

pub fn migrate_db(pool: &SqlitePool) -> Result<()> {
    info!("Running database migrations");

    // Ensure the table exists
    // NOTE: Yaak used to use sqlx for migrations, so we need to mirror that table structure. We
    //  are writing checksum but not verifying because we want to be able to change migrations after
    //  a release in case something breaks.
    pool.get()?.execute(
        "CREATE TABLE IF NOT EXISTS _sqlx_migrations (
            version        BIGINT PRIMARY KEY,
            description    TEXT NOT NULL,
            installed_on   TIMESTAMP default CURRENT_TIMESTAMP NOT NULL,
            success        BOOLEAN                             NOT NULL,
            checksum       BLOB                                NOT NULL,
            execution_time BIGINT                              NOT NULL
        )",
        [],
    )?;

    // Read and sort all .sql files
    let mut entries = MIGRATIONS_DIR
        .entries()
        .into_iter()
        .filter(|e| e.path().extension().map(|ext| ext == "sql").unwrap_or(false))
        .collect::<Vec<_>>();

    // Ensure they're in the correct order
    entries.sort_by_key(|e| e.path());

    // Run each migration in a transaction
    let mut num_migrations = 0;
    let mut ran_migrations = 0;
    for entry in entries {
        num_migrations += 1;
        let conn = pool.get()?;
        // `new_unchecked` takes `&Connection`; see yaak_database::pool for why
        // the pool never hands out `&mut`.
        let mut tx = Transaction::new_unchecked(&conn, TransactionBehavior::Immediate)?;
        match run_migration(entry, &mut tx) {
            Ok(ran) => {
                if ran {
                    ran_migrations += 1;
                }
                tx.commit()?
            }
            Err(e) => {
                let msg = format!(
                    "{} failed with {}",
                    entry.path().file_name().unwrap().to_str().unwrap(),
                    e.to_string()
                );
                tx.rollback()?;
                return Err(MigrationError(msg));
            }
        };
    }

    if ran_migrations == 0 {
        info!("No migrations to run out of {}", num_migrations);
    } else {
        info!("Ran {}/{} migrations", ran_migrations, num_migrations);
    }

    Ok(())
}

fn run_migration(migration_path: &DirEntry, tx: &mut rusqlite::Transaction) -> Result<bool> {
    let start = elapsed_timer();
    let (version, description) = split_migration_filename(migration_path.path().to_str().unwrap())
        .expect("Failed to parse migration filename");

    // Skip if already applied
    let row: Option<i64> = tx
        .query_row("SELECT 1 FROM _sqlx_migrations WHERE version = ?", [version.clone()], |r| {
            r.get(0)
        })
        .optional()?;

    if row.is_some() {
        debug!("Skipping already run migration {description}");
        return Ok(false); // Migration was already run
    }

    let sql =
        migration_path.as_file().unwrap().contents_utf8().expect("Failed to read migration file");
    info!("Applying migration {description}");

    // Split on `;`? → optional depending on how your SQL is structured
    tx.execute_batch(&sql)?;

    let execution_time = start();
    let checksum = sha384_hex_prefixed(sql.as_bytes());

    // NOTE: The success column is never used. It's just there for sqlx compatibility.
    tx.execute(
        "INSERT INTO _sqlx_migrations (version, description, execution_time, checksum, success) VALUES (?, ?, ?, ?, ?)",
        params![version, description, execution_time, checksum, true],
    )?;

    Ok(true)
}

/// Nanoseconds since the timer was started, for the sqlx-compatible
/// `execution_time` column. `Instant` does not exist on `wasm32-unknown-unknown`
/// (there is no monotonic clock to ask), and the column is bookkeeping, so
/// there it reads as zero rather than taking the migrator down with it.
#[cfg(not(target_arch = "wasm32"))]
fn elapsed_timer() -> impl Fn() -> i64 {
    let start = std::time::Instant::now();
    move || start.elapsed().as_nanos() as i64
}

#[cfg(target_arch = "wasm32")]
fn elapsed_timer() -> impl Fn() -> i64 {
    || 0
}

fn split_migration_filename(filename: &str) -> Option<(String, String)> {
    // Remove the .sql extension
    let trimmed = filename.strip_suffix(".sql")?;

    // Split on the first underscore
    let mut parts = trimmed.splitn(2, '_');
    let version = parts.next()?.to_string();
    let description = parts.next()?.to_string();

    Some((version, description))
}

fn sha384_hex_prefixed(input: &[u8]) -> String {
    let mut hasher = Sha384::new();
    hasher.update(input);
    let result = hasher.finalize();

    // Format as 0x... with uppercase hex
    format!("0x{}", hex::encode_upper(result))
}
