use anyhow::Result;
use rusqlite::Connection;

pub(super) fn migrate(connection: &Connection) -> Result<()> {
    connection.execute_batch(
        "BEGIN;
         CREATE TABLE IF NOT EXISTS meta (
            key TEXT PRIMARY KEY, value TEXT NOT NULL
         );
         CREATE TABLE IF NOT EXISTS items (
            id INTEGER PRIMARY KEY,
            text TEXT NOT NULL,
            note TEXT NOT NULL DEFAULT '',
            priority INTEGER NOT NULL DEFAULT 3 CHECK(priority BETWEEN 1 AND 5),
            when_at TEXT,
            done_at TEXT,
            alarm_at TEXT,
            numeric_value REAL,
            created_at TEXT NOT NULL,
            updated_at TEXT NOT NULL,
            sort_order INTEGER NOT NULL DEFAULT 0,
            discarded INTEGER NOT NULL DEFAULT 0 CHECK(discarded IN (0,1))
         );
         CREATE TABLE IF NOT EXISTS categories (
            id INTEGER PRIMARY KEY,
            name TEXT NOT NULL UNIQUE COLLATE NOCASE,
            parent_id INTEGER REFERENCES categories(id) ON DELETE SET NULL,
            kind TEXT NOT NULL DEFAULT 'standard',
            match_text TEXT NOT NULL DEFAULT '',
            exclusive INTEGER NOT NULL DEFAULT 0 CHECK(exclusive IN (0,1)),
            sort_order INTEGER NOT NULL DEFAULT 0
         );
         CREATE TABLE IF NOT EXISTS item_categories (
            item_id INTEGER NOT NULL REFERENCES items(id) ON DELETE CASCADE,
            category_id INTEGER NOT NULL REFERENCES categories(id) ON DELETE CASCADE,
            assignment TEXT NOT NULL DEFAULT 'explicit',
            PRIMARY KEY(item_id, category_id)
         );
         CREATE TABLE IF NOT EXISTS dependencies (
            item_id INTEGER NOT NULL REFERENCES items(id) ON DELETE CASCADE,
            prerequisite_id INTEGER NOT NULL REFERENCES items(id) ON DELETE CASCADE,
            PRIMARY KEY(item_id, prerequisite_id),
            CHECK(item_id <> prerequisite_id)
         );
         CREATE TABLE IF NOT EXISTS views (
            id INTEGER PRIMARY KEY,
            name TEXT NOT NULL UNIQUE COLLATE NOCASE,
            kind TEXT NOT NULL DEFAULT 'list',
            filter_value TEXT NOT NULL DEFAULT '',
            sort_key TEXT NOT NULL DEFAULT 'manual',
            show_done INTEGER NOT NULL DEFAULT 1,
            sort_order INTEGER NOT NULL DEFAULT 0
         );
         CREATE INDEX IF NOT EXISTS idx_items_when ON items(when_at);
         CREATE INDEX IF NOT EXISTS idx_items_done ON items(done_at);
         CREATE INDEX IF NOT EXISTS idx_item_categories_category ON item_categories(category_id);
         COMMIT;",
    )?;

    let version: i64 = connection.query_row("PRAGMA user_version", [], |row| row.get(0))?;
    if version < 2 {
        add_filter_expression(connection)?;
    }
    connection.execute_batch(
        "CREATE TABLE IF NOT EXISTS view_columns (
                id INTEGER PRIMARY KEY, view_id INTEGER NOT NULL REFERENCES views(id) ON DELETE CASCADE,
                field TEXT NOT NULL, heading TEXT NOT NULL, width INTEGER NOT NULL DEFAULT 20,
                aggregate TEXT NOT NULL DEFAULT 'none', sort_order INTEGER NOT NULL DEFAULT 0);
             CREATE TABLE IF NOT EXISTS view_sections (
                id INTEGER PRIMARY KEY, view_id INTEGER NOT NULL REFERENCES views(id) ON DELETE CASCADE,
                heading TEXT NOT NULL, filter_expr TEXT NOT NULL DEFAULT '', collapsed INTEGER NOT NULL DEFAULT 0,
                sort_order INTEGER NOT NULL DEFAULT 0);
             CREATE TABLE IF NOT EXISTS category_rules (
                id INTEGER PRIMARY KEY, category_id INTEGER NOT NULL REFERENCES categories(id) ON DELETE CASCADE,
                condition_expr TEXT NOT NULL, action_kind TEXT NOT NULL, action_value TEXT NOT NULL DEFAULT '',
                enabled INTEGER NOT NULL DEFAULT 1, sort_order INTEGER NOT NULL DEFAULT 0);",
    )?;
    if version < 2 {
        connection.pragma_update(None, "user_version", 2)?;
    }
    if version < 3 {
        add_recurrence(connection)?;
        connection.pragma_update(None, "user_version", 3)?;
    }
    connection.execute_batch(
        "CREATE TABLE IF NOT EXISTS macros (
            id INTEGER PRIMARY KEY,
            name TEXT NOT NULL UNIQUE COLLATE NOCASE,
            source TEXT NOT NULL,
            key_binding TEXT NOT NULL DEFAULT '',
            sort_order INTEGER NOT NULL DEFAULT 0
         );
         CREATE TABLE IF NOT EXISTS macro_variables (
            name TEXT PRIMARY KEY COLLATE NOCASE,
            value TEXT NOT NULL
         );",
    )?;
    if version < 4 {
        connection.pragma_update(None, "user_version", 4)?;
    }
    if version < 5 {
        add_lotus_values_and_rules(connection)?;
        connection.pragma_update(None, "user_version", 5)?;
    }
    Ok(())
}

fn add_lotus_values_and_rules(connection: &Connection) -> Result<()> {
    connection.execute_batch(
        "BEGIN;
         ALTER TABLE item_categories RENAME TO old_item_categories;
         CREATE TABLE item_categories (
            item_id INTEGER NOT NULL REFERENCES items(id) ON DELETE CASCADE,
            category_id INTEGER NOT NULL REFERENCES categories(id) ON DELETE CASCADE,
            assignment TEXT NOT NULL CHECK(assignment IN ('explicit','automatic','conditional','excluded')),
            PRIMARY KEY(item_id, category_id, assignment)
         );
         INSERT INTO item_categories(item_id,category_id,assignment)
            SELECT item_id,category_id,assignment FROM old_item_categories;
         DROP TABLE old_item_categories;
         CREATE INDEX idx_item_categories_category ON item_categories(category_id);
         CREATE TABLE item_values (
            item_id INTEGER NOT NULL REFERENCES items(id) ON DELETE CASCADE,
            category_id INTEGER NOT NULL REFERENCES categories(id) ON DELETE CASCADE,
            numeric_value REAL,
            date_value TEXT,
            assignment TEXT NOT NULL DEFAULT 'explicit'
                CHECK(assignment IN ('explicit','automatic','conditional')),
            PRIMARY KEY(item_id, category_id),
            CHECK((numeric_value IS NULL) OR (date_value IS NULL))
         );
         CREATE TABLE rule_action_runs (
            rule_id INTEGER NOT NULL REFERENCES category_rules(id) ON DELETE CASCADE,
            item_id INTEGER NOT NULL REFERENCES items(id) ON DELETE CASCADE,
            PRIMARY KEY(rule_id,item_id)
         );
         CREATE TABLE rule_conflicts (
            item_id INTEGER PRIMARY KEY REFERENCES items(id) ON DELETE CASCADE,
            message TEXT NOT NULL,
            updated_at TEXT NOT NULL
         );
         COMMIT;",
    )?;
    for (column, definition) in [
        (
            "category_id",
            "INTEGER REFERENCES categories(id) ON DELETE SET NULL",
        ),
        ("number_label", "TEXT NOT NULL DEFAULT ''"),
        ("decimals", "INTEGER NOT NULL DEFAULT 2"),
        ("decimal_separator", "TEXT NOT NULL DEFAULT '.'"),
        ("thousands_separator", "TEXT NOT NULL DEFAULT 'comma'"),
        ("negative_style", "TEXT NOT NULL DEFAULT 'minus'"),
        ("percent_total", "INTEGER NOT NULL DEFAULT 0"),
        ("alignment", "TEXT NOT NULL DEFAULT 'left'"),
    ] {
        if !column_exists(connection, "view_columns", column)? {
            connection.execute_batch(&format!(
                "ALTER TABLE view_columns ADD COLUMN {column} {definition};"
            ))?;
        }
    }
    connection.execute_batch(
         "UPDATE view_columns
            SET category_id=(SELECT id FROM categories WHERE kind='numeric' ORDER BY id LIMIT 1),
                field='numeric'
          WHERE field='value';
         INSERT INTO categories(name,kind,sort_order)
            SELECT 'Value','numeric',(SELECT COALESCE(MAX(sort_order),0)+1 FROM categories)
             WHERE EXISTS(SELECT 1 FROM items WHERE numeric_value IS NOT NULL)
               AND NOT EXISTS(SELECT 1 FROM categories WHERE kind='numeric')
               AND NOT EXISTS(SELECT 1 FROM categories WHERE name='Value' COLLATE NOCASE);
         INSERT INTO categories(name,kind,sort_order)
            SELECT 'Numeric Value','numeric',(SELECT COALESCE(MAX(sort_order),0)+1 FROM categories)
             WHERE EXISTS(SELECT 1 FROM items WHERE numeric_value IS NOT NULL)
               AND NOT EXISTS(SELECT 1 FROM categories WHERE kind='numeric');
         UPDATE view_columns
            SET category_id=(SELECT id FROM categories WHERE kind='numeric' ORDER BY id LIMIT 1)
          WHERE field='numeric' AND category_id IS NULL;
         INSERT OR IGNORE INTO item_values(item_id,category_id,numeric_value,assignment)
            SELECT i.id,
                   COALESCE(
                     (SELECT ic.category_id FROM item_categories ic JOIN categories c ON c.id=ic.category_id
                       WHERE ic.item_id=i.id AND ic.assignment<>'excluded' AND c.kind='numeric' LIMIT 1),
                     (SELECT id FROM categories WHERE kind='numeric' ORDER BY id LIMIT 1)
                   ),
                   i.numeric_value,'explicit'
              FROM items i WHERE i.numeric_value IS NOT NULL
                AND EXISTS(SELECT 1 FROM categories WHERE kind='numeric');
         INSERT OR IGNORE INTO item_categories(item_id,category_id,assignment)
            SELECT item_id,category_id,'explicit' FROM item_values;
         UPDATE items SET numeric_value=NULL
          WHERE numeric_value IS NOT NULL AND EXISTS(
            SELECT 1 FROM item_values WHERE item_values.item_id=items.id
          );
         UPDATE category_rules SET action_kind='none'
          WHERE action_kind='assign' AND TRIM(action_value)='';
         INSERT OR IGNORE INTO rule_action_runs(rule_id,item_id)
            SELECT r.id,positive.item_id FROM category_rules r
            JOIN item_categories positive ON positive.category_id=r.category_id
             AND positive.assignment<>'excluded'
           WHERE r.action_kind<>'none' AND NOT EXISTS(
             SELECT 1 FROM item_categories excluded
              WHERE excluded.item_id=positive.item_id
                AND excluded.category_id=positive.category_id
                AND excluded.assignment='excluded'
           );",
    )?;
    Ok(())
}

fn add_filter_expression(connection: &Connection) -> Result<()> {
    if !column_exists(connection, "views", "filter_expr")? {
        connection
            .execute_batch("ALTER TABLE views ADD COLUMN filter_expr TEXT NOT NULL DEFAULT '';")?;
    }
    Ok(())
}

fn add_recurrence(connection: &Connection) -> Result<()> {
    if !column_exists(connection, "items", "recurrence")? {
        connection
            .execute_batch("ALTER TABLE items ADD COLUMN recurrence TEXT NOT NULL DEFAULT '';")?;
    }
    Ok(())
}

fn column_exists(connection: &Connection, table: &str, column: &str) -> Result<bool> {
    let mut statement = connection.prepare(&format!("PRAGMA table_info({table})"))?;
    let names = statement
        .query_map([], |row| row.get::<_, String>(1))?
        .collect::<rusqlite::Result<Vec<_>>>()?;
    Ok(names.iter().any(|name| name == column))
}

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

    #[test]
    fn version_four_numbers_migrate_without_losing_values_or_provenance() {
        let connection = Connection::open_in_memory().unwrap();
        connection
            .execute_batch(
                "PRAGMA foreign_keys=ON;
                 CREATE TABLE items(id INTEGER PRIMARY KEY,text TEXT NOT NULL,note TEXT NOT NULL DEFAULT '',priority INTEGER NOT NULL DEFAULT 3,when_at TEXT,done_at TEXT,alarm_at TEXT,numeric_value REAL,created_at TEXT NOT NULL,updated_at TEXT NOT NULL,sort_order INTEGER NOT NULL DEFAULT 0,discarded INTEGER NOT NULL DEFAULT 0,recurrence TEXT NOT NULL DEFAULT '');
                 CREATE TABLE categories(id INTEGER PRIMARY KEY,name TEXT NOT NULL UNIQUE,parent_id INTEGER,kind TEXT NOT NULL DEFAULT 'standard',match_text TEXT NOT NULL DEFAULT '',exclusive INTEGER NOT NULL DEFAULT 0,sort_order INTEGER NOT NULL DEFAULT 0);
                 CREATE TABLE item_categories(item_id INTEGER NOT NULL REFERENCES items(id),category_id INTEGER NOT NULL REFERENCES categories(id),assignment TEXT NOT NULL DEFAULT 'explicit',PRIMARY KEY(item_id,category_id));
                 CREATE TABLE views(id INTEGER PRIMARY KEY,name TEXT NOT NULL UNIQUE,kind TEXT NOT NULL DEFAULT 'list',filter_value TEXT NOT NULL DEFAULT '',sort_key TEXT NOT NULL DEFAULT 'manual',show_done INTEGER NOT NULL DEFAULT 1,sort_order INTEGER NOT NULL DEFAULT 0,filter_expr TEXT NOT NULL DEFAULT '');
                 CREATE TABLE view_columns(id INTEGER PRIMARY KEY,view_id INTEGER NOT NULL REFERENCES views(id),field TEXT NOT NULL,heading TEXT NOT NULL,width INTEGER NOT NULL DEFAULT 20,aggregate TEXT NOT NULL DEFAULT 'none',sort_order INTEGER NOT NULL DEFAULT 0);
                 INSERT INTO categories(id,name,kind) VALUES(1,'Hours','numeric');
                 INSERT INTO items(id,text,numeric_value,created_at,updated_at) VALUES(1,'Legacy',42.25,'now','now');
                 INSERT INTO item_categories VALUES(1,1,'explicit');
                 INSERT INTO views(id,name) VALUES(1,'Legacy');
                 INSERT INTO view_columns(view_id,field,heading) VALUES(1,'value','Hours');
                 PRAGMA user_version=4;",
            )
            .unwrap();

        migrate(&connection).unwrap();

        assert_eq!(
            connection
                .query_row(
                    "SELECT numeric_value FROM item_values WHERE item_id=1 AND category_id=1",
                    [],
                    |row| row.get::<_, f64>(0),
                )
                .unwrap(),
            42.25
        );
        assert!(
            connection
                .query_row("SELECT numeric_value FROM items WHERE id=1", [], |row| {
                    row.get::<_, Option<f64>>(0)
                })
                .unwrap()
                .is_none()
        );
        connection
            .execute("INSERT INTO item_categories VALUES(1,1,'conditional')", [])
            .unwrap();
        assert_eq!(
            connection
                .query_row(
                    "SELECT COUNT(*) FROM item_categories WHERE item_id=1 AND category_id=1",
                    [],
                    |row| row.get::<_, i64>(0),
                )
                .unwrap(),
            2
        );
    }
}
