use super::{
    error::DatabaseError,
    tables::{SCHEMA_VERSION, Table},
};
use rusqlite::Connection;

fn get_user_version(conn: &Connection) -> Result<u32, DatabaseError> {
    Ok(conn.pragma_query_value(None, "user_version", |row| row.get(0))?)
}

fn set_user_version(conn: &Connection, version: u32) -> Result<(), DatabaseError> {
    conn.pragma_update(None, "user_version", version)?;
    Ok(())
}

fn get_column_names(conn: &Connection, table: &str) -> Result<Vec<String>, DatabaseError> {
    let mut stmt = conn.prepare(&format!("PRAGMA table_info('{table}')"))?;
    let names = stmt
        .query_map([], |row| row.get::<_, String>(1))?
        .collect::<Result<Vec<_>, _>>()?;
    Ok(names)
}

/// Get column names for a table in the attached old_db.
/// Uses `PRAGMA old_db.table_info('table_name')` which is the correct syntax
/// for querying attached database schemas.
fn get_old_column_names(conn: &Connection, table: &str) -> Result<Vec<String>, DatabaseError> {
    let mut stmt = conn.prepare(&format!("PRAGMA old_db.table_info('{table}')"))?;
    let names = stmt
        .query_map([], |row| row.get::<_, String>(1))?
        .collect::<Result<Vec<_>, _>>()?;
    Ok(names)
}

fn has_any_table(conn: &Connection, tables: &[Table]) -> Result<bool, DatabaseError> {
    for table in tables {
        if conn.table_exists(None, table.name)? {
            return Ok(true);
        }
    }
    Ok(false)
}

fn schemas_differ(
    conn: &Connection,
    pristine: &Connection,
    tables: &[Table],
) -> Result<bool, DatabaseError> {
    for table in tables {
        if !conn.table_exists(None, table.name)? {
            return Ok(true);
        }
        let old_cols = get_column_names(conn, table.name)?;
        let new_cols = get_column_names(pristine, table.name)?;
        if old_cols != new_cols {
            return Ok(true);
        }
    }
    Ok(false)
}

fn create_pristine(tables: &[Table]) -> Result<Connection, DatabaseError> {
    let conn = Connection::open_in_memory()?;
    for table in tables {
        conn.execute(table.schema, [])?;
        for &index in table.indexes {
            conn.execute(index, [])?;
        }
        for &trigger in table.triggers {
            conn.execute(trigger, [])?;
        }
    }
    Ok(conn)
}

fn perform_migration(db_path: &str, tables: &[Table]) -> Result<Connection, DatabaseError> {
    log::info!("schema migration started");

    let pristine = create_pristine(tables)?;
    pristine.execute("PRAGMA foreign_keys = OFF", [])?;
    pristine.execute(&format!("ATTACH DATABASE '{}' AS old_db", db_path), [])?;

    let tx = pristine.unchecked_transaction()?;

    for table in tables {
        // Check if the table exists in the old database
        let exists: bool = tx.query_row(
            "SELECT COUNT(*) > 0 FROM old_db.sqlite_master WHERE type='table' AND name=?1",
            [table.name],
            |row| row.get(0),
        )?;
        if !exists {
            log::debug!("table {} is new, skipping copy", table.name);
            continue;
        }

        let old_cols = get_old_column_names(&tx, table.name)?;
        let new_cols = get_column_names(&tx, table.name)?;

        // Compute intersection preserving new schema order
        let common: Vec<&str> = new_cols
            .iter()
            .filter(|c| old_cols.iter().any(|o| o == *c))
            .map(|c| c.as_str())
            .collect();

        if common.is_empty() {
            log::debug!("table {} has no common columns, skipping", table.name);
            continue;
        }

        let cols = common.join(", ");
        let sql = format!(
            "INSERT INTO main.{name} ({cols}) SELECT {cols} FROM old_db.{name}",
            name = table.name,
            cols = cols,
        );
        let count = tx.execute(&sql, [])?;
        log::info!("migrated {} rows in table {}", count, table.name);
    }

    tx.commit()?;

    // Validate foreign key integrity
    pristine.execute("PRAGMA foreign_keys = ON", [])?;
    let fk_errors: Vec<String> = {
        let mut stmt = pristine.prepare("PRAGMA foreign_key_check")?;
        let errors = stmt
            .query_map([], |row| {
                let table: String = row.get(0)?;
                let rowid: i64 = row.get(1)?;
                let parent: String = row.get(2)?;
                Ok(format!("{table} rowid={rowid} -> {parent}"))
            })?
            .collect::<Result<Vec<_>, _>>()?;
        errors
    };
    if !fk_errors.is_empty() {
        return Err(DatabaseError::Migration(format!(
            "foreign key violations after migration: {:?}",
            fk_errors
        )));
    }

    set_user_version(&pristine, SCHEMA_VERSION)?;

    pristine.execute("DETACH DATABASE old_db", [])?;

    // Write in-memory DB to disk via backup API
    let migrating_path = format!("{db_path}.migrating");
    let backup_path = format!("{db_path}.backup");

    {
        let mut dest = Connection::open(&migrating_path)?;
        let backup = rusqlite::backup::Backup::new(&pristine, &mut dest)?;
        backup.run_to_completion(100, std::time::Duration::ZERO, None)?;
    }
    drop(pristine);

    // Atomic swap: old -> backup, migrating -> main
    std::fs::rename(db_path, &backup_path)?;
    std::fs::rename(&migrating_path, db_path)?;

    log::info!("schema migration complete, backup at {backup_path}");

    let conn = Connection::open(db_path)?;
    Ok(conn)
}

pub fn migrate_if_needed(db_path: &str, tables: &[Table]) -> Result<Connection, DatabaseError> {
    let conn = Connection::open(db_path)?;
    let version = get_user_version(&conn)?;

    if version == SCHEMA_VERSION {
        log::debug!("schema version {version} is current, no migration needed");
        return Ok(conn);
    }

    if version == 0 {
        if !has_any_table(&conn, tables)? {
            // Fresh database — stamp version and let tables::init create everything
            set_user_version(&conn, SCHEMA_VERSION)?;
            log::debug!("fresh database, stamped schema version {SCHEMA_VERSION}");
            return Ok(conn);
        }

        let pristine = create_pristine(tables)?;
        if !schemas_differ(&conn, &pristine, tables)? {
            // Existing DB with matching schema, just needs version stamp
            set_user_version(&conn, SCHEMA_VERSION)?;
            log::info!("existing database schema matches, stamped version {SCHEMA_VERSION}");
            return Ok(conn);
        }
    }

    // Close the connection before migration (it will ATTACH the file)
    drop(conn);
    perform_migration(db_path, tables)
}
