use chrono::{Datelike, NaiveDate};
use rusqlite::{Connection, Result as SqlResult, params};
use std::path::Path;

use crate::models::*;

const CURRENT_SCHEMA_VERSION: i32 = 11;

pub struct Database {
    pub conn: Connection,
}

impl Database {
    pub fn open(path: &Path) -> SqlResult<Self> {
        if let Some(parent) = path.parent() {
            std::fs::create_dir_all(parent).ok();
        }
        let conn = Connection::open(path)?;
        let db = Database { conn };
        db.init()?;
        Ok(db)
    }

    pub fn open_in_memory() -> SqlResult<Self> {
        let conn = Connection::open_in_memory()?;
        let db = Database { conn };
        db.init()?;
        Ok(db)
    }

    fn init(&self) -> SqlResult<()> {
        self.conn.execute_batch("PRAGMA journal_mode=WAL;")?;
        self.conn.execute_batch("PRAGMA foreign_keys=ON;")?;
        self.create_tables()?;
        self.migrate()?;
        Ok(())
    }

    fn create_tables(&self) -> SqlResult<()> {
        self.conn.execute_batch(
            "CREATE TABLE IF NOT EXISTS schema_version (
                version INTEGER NOT NULL
            );

            CREATE TABLE IF NOT EXISTS categories (
                id          INTEGER PRIMARY KEY,
                name        TEXT NOT NULL,
                icon        TEXT,
                color       TEXT,
                type        TEXT NOT NULL,
                is_default  INTEGER DEFAULT 0,
                sort_order  INTEGER DEFAULT 0,
                parent_id   INTEGER REFERENCES categories(id)
            );

            CREATE TABLE IF NOT EXISTS transactions (
                id            INTEGER PRIMARY KEY,
                amount        REAL NOT NULL,
                type          TEXT NOT NULL,
                category_id   INTEGER NOT NULL REFERENCES categories(id),
                currency      TEXT NOT NULL DEFAULT 'USD',
                exchange_rate REAL DEFAULT 1.0,
                note          TEXT,
                date          TEXT NOT NULL,
                created_at    TEXT NOT NULL,
                recurring_id  INTEGER REFERENCES recurring_transactions(id),
                payee         TEXT
            );

            CREATE TABLE IF NOT EXISTS budgets (
                id          INTEGER PRIMARY KEY,
                category_id INTEGER NOT NULL REFERENCES categories(id),
                amount      REAL NOT NULL,
                month       TEXT NOT NULL,
                rollover    INTEGER DEFAULT 0,
                UNIQUE(category_id, month)
            );

            CREATE TABLE IF NOT EXISTS recurring_transactions (
                id              INTEGER PRIMARY KEY,
                amount          REAL NOT NULL,
                type            TEXT NOT NULL,
                category_id     INTEGER NOT NULL REFERENCES categories(id),
                currency        TEXT NOT NULL DEFAULT 'USD',
                note            TEXT,
                frequency       TEXT NOT NULL,
                start_date      TEXT NOT NULL,
                end_date        TEXT,
                last_generated  TEXT,
                active          INTEGER DEFAULT 1,
                resume_date     TEXT,
                is_bill         INTEGER DEFAULT 0,
                reminder_days   INTEGER DEFAULT 3,
                subscription_id INTEGER
            );

            CREATE TABLE IF NOT EXISTS exchange_rates (
                base       TEXT NOT NULL,
                target     TEXT NOT NULL,
                rate       REAL NOT NULL,
                fetched_at TEXT NOT NULL,
                PRIMARY KEY (base, target)
            );

            CREATE TABLE IF NOT EXISTS settings (
                key   TEXT PRIMARY KEY,
                value TEXT NOT NULL
            );

            CREATE TABLE IF NOT EXISTS budget_notifications (
                category_id INTEGER NOT NULL,
                month       TEXT NOT NULL,
                threshold   INTEGER NOT NULL,
                notified_at TEXT NOT NULL,
                PRIMARY KEY (category_id, month, threshold)
            );

            CREATE TABLE IF NOT EXISTS tags (
                id   INTEGER PRIMARY KEY,
                name TEXT NOT NULL UNIQUE
            );

            CREATE TABLE IF NOT EXISTS transaction_tags (
                transaction_id INTEGER NOT NULL REFERENCES transactions(id) ON DELETE CASCADE,
                tag_id         INTEGER NOT NULL REFERENCES tags(id) ON DELETE CASCADE,
                PRIMARY KEY (transaction_id, tag_id)
            );

            CREATE TABLE IF NOT EXISTS transaction_splits (
                id             INTEGER PRIMARY KEY,
                transaction_id INTEGER NOT NULL REFERENCES transactions(id) ON DELETE CASCADE,
                category_id    INTEGER NOT NULL REFERENCES categories(id),
                amount         REAL NOT NULL,
                note           TEXT
            );

            CREATE TABLE IF NOT EXISTS transaction_templates (
                id          INTEGER PRIMARY KEY,
                name        TEXT NOT NULL,
                amount      REAL,
                type        TEXT NOT NULL,
                category_id INTEGER NOT NULL REFERENCES categories(id),
                currency    TEXT NOT NULL DEFAULT 'USD',
                payee       TEXT,
                note        TEXT,
                tags        TEXT,
                sort_order  INTEGER DEFAULT 0
            );

            CREATE TABLE IF NOT EXISTS categorization_rules (
                id          INTEGER PRIMARY KEY,
                field       TEXT NOT NULL,
                pattern     TEXT NOT NULL,
                category_id INTEGER NOT NULL REFERENCES categories(id),
                priority    INTEGER DEFAULT 0
            );

            CREATE TABLE IF NOT EXISTS savings_goals (
                id         INTEGER PRIMARY KEY,
                name       TEXT NOT NULL,
                target     REAL NOT NULL,
                saved      REAL DEFAULT 0.0,
                currency   TEXT NOT NULL DEFAULT 'USD',
                deadline   TEXT,
                color      TEXT,
                icon       TEXT,
                created_at TEXT NOT NULL DEFAULT (datetime('now'))
            );

            CREATE TABLE IF NOT EXISTS wishlist_items (
                id          INTEGER PRIMARY KEY,
                name        TEXT NOT NULL,
                amount      REAL NOT NULL,
                category_id INTEGER REFERENCES categories(id),
                url         TEXT,
                note        TEXT,
                priority    INTEGER DEFAULT 0,
                purchased   INTEGER DEFAULT 0,
                created_at  TEXT NOT NULL DEFAULT (datetime('now'))
            );

            CREATE TABLE IF NOT EXISTS subscription_categories (
                id         INTEGER PRIMARY KEY,
                name       TEXT NOT NULL,
                icon       TEXT,
                color      TEXT,
                sort_order INTEGER DEFAULT 0
            );

            CREATE TABLE IF NOT EXISTS subscriptions (
                id           INTEGER PRIMARY KEY,
                name         TEXT NOT NULL,
                amount       REAL NOT NULL,
                currency     TEXT NOT NULL DEFAULT 'USD',
                frequency    TEXT NOT NULL DEFAULT 'monthly',
                category_id  INTEGER NOT NULL REFERENCES subscription_categories(id),
                start_date   TEXT NOT NULL,
                next_due     TEXT NOT NULL,
                active       INTEGER DEFAULT 1,
                note         TEXT,
                url          TEXT,
                recurring_id INTEGER
            );

            CREATE TABLE IF NOT EXISTS credit_cards (
                id INTEGER PRIMARY KEY,
                name TEXT NOT NULL,
                credit_limit REAL,
                statement_close_day INTEGER NOT NULL,
                due_day INTEGER NOT NULL,
                min_payment_pct REAL DEFAULT 2.0,
                current_balance REAL DEFAULT 0.0,
                currency TEXT NOT NULL DEFAULT 'USD',
                color TEXT,
                active INTEGER DEFAULT 1
            );

            CREATE TABLE IF NOT EXISTS streaks (
                id INTEGER PRIMARY KEY,
                streak_type TEXT NOT NULL,
                start_date TEXT NOT NULL,
                end_date TEXT,
                current_count INTEGER DEFAULT 0,
                best_count INTEGER DEFAULT 0
            );

            CREATE TABLE IF NOT EXISTS achievements (
                id INTEGER PRIMARY KEY,
                name TEXT NOT NULL UNIQUE,
                description TEXT NOT NULL,
                earned_at TEXT,
                icon TEXT
            );"
        )?;
        Ok(())
    }

    fn migrate(&self) -> SqlResult<()> {
        let version = self.get_schema_version()?;

        if version == 0 {
            self.seed_default_categories()?;
            self.set_schema_version(1)?;
        }

        if self.get_schema_version()? < 2 {
            self.conn.execute_batch(
                "CREATE TABLE IF NOT EXISTS attachments (
                    id INTEGER PRIMARY KEY AUTOINCREMENT,
                    transaction_id INTEGER NOT NULL REFERENCES transactions(id) ON DELETE CASCADE,
                    filename TEXT NOT NULL,
                    mime_type TEXT NOT NULL,
                    data BLOB NOT NULL,
                    created_at TEXT NOT NULL DEFAULT (datetime('now'))
                );"
            )?;
            self.set_schema_version(2)?;
        }

        if self.get_schema_version()? < 3 {
            // Only add column if it doesn't already exist (CREATE TABLE may include it)
            let has_col: bool = self.conn
                .prepare("PRAGMA table_info(recurring_transactions)")?
                .query_map([], |row| row.get::<_, String>(1))?
                .any(|name| name.map_or(false, |n| n == "resume_date"));
            if !has_col {
                self.conn.execute_batch(
                    "ALTER TABLE recurring_transactions ADD COLUMN resume_date TEXT;"
                )?;
            }
            self.set_schema_version(3)?;
        }

        if self.get_schema_version()? < 4 {
            let has_parent_id: bool = self.conn
                .prepare("PRAGMA table_info(categories)")?
                .query_map([], |row| row.get::<_, String>(1))?
                .any(|name| name.map_or(false, |n| n == "parent_id"));
            if !has_parent_id {
                self.conn.execute_batch(
                    "ALTER TABLE categories ADD COLUMN parent_id INTEGER REFERENCES categories(id);"
                )?;
            }

            let has_payee: bool = self.conn
                .prepare("PRAGMA table_info(transactions)")?
                .query_map([], |row| row.get::<_, String>(1))?
                .any(|name| name.map_or(false, |n| n == "payee"));
            if !has_payee {
                self.conn.execute_batch(
                    "ALTER TABLE transactions ADD COLUMN payee TEXT;"
                )?;
            }

            self.conn.execute_batch(
                "CREATE TABLE IF NOT EXISTS tags (
                    id   INTEGER PRIMARY KEY,
                    name TEXT NOT NULL UNIQUE
                );
                CREATE TABLE IF NOT EXISTS transaction_tags (
                    transaction_id INTEGER NOT NULL REFERENCES transactions(id) ON DELETE CASCADE,
                    tag_id         INTEGER NOT NULL REFERENCES tags(id) ON DELETE CASCADE,
                    PRIMARY KEY (transaction_id, tag_id)
                );"
            )?;
            self.set_schema_version(4)?;
        }

        if self.get_schema_version()? < 5 {
            self.conn.execute_batch(
                "CREATE TABLE IF NOT EXISTS transaction_splits (
                    id             INTEGER PRIMARY KEY,
                    transaction_id INTEGER NOT NULL REFERENCES transactions(id) ON DELETE CASCADE,
                    category_id    INTEGER NOT NULL REFERENCES categories(id),
                    amount         REAL NOT NULL,
                    note           TEXT
                );
                CREATE TABLE IF NOT EXISTS transaction_templates (
                    id          INTEGER PRIMARY KEY,
                    name        TEXT NOT NULL,
                    amount      REAL,
                    type        TEXT NOT NULL,
                    category_id INTEGER NOT NULL REFERENCES categories(id),
                    currency    TEXT NOT NULL DEFAULT 'USD',
                    payee       TEXT,
                    note        TEXT,
                    tags        TEXT,
                    sort_order  INTEGER DEFAULT 0
                );
                CREATE TABLE IF NOT EXISTS categorization_rules (
                    id          INTEGER PRIMARY KEY,
                    field       TEXT NOT NULL,
                    pattern     TEXT NOT NULL,
                    category_id INTEGER NOT NULL REFERENCES categories(id),
                    priority    INTEGER DEFAULT 0
                );"
            )?;
            self.set_schema_version(5)?;
        }

        if self.get_schema_version()? < 6 {
            // Check if rollover column already exists (fresh installs have it in CREATE TABLE)
            let has_rollover: bool = self.conn
                .prepare("SELECT rollover FROM budgets LIMIT 0")
                .is_ok();
            if !has_rollover {
                self.conn.execute_batch(
                    "ALTER TABLE budgets ADD COLUMN rollover INTEGER DEFAULT 0;"
                )?;
            }
            self.set_schema_version(6)?;
        }

        if self.get_schema_version()? < 7 {
            // Add is_bill and reminder_days to recurring_transactions
            let has_is_bill: bool = self.conn
                .prepare("SELECT is_bill FROM recurring_transactions LIMIT 0")
                .is_ok();
            if !has_is_bill {
                self.conn.execute_batch(
                    "ALTER TABLE recurring_transactions ADD COLUMN is_bill INTEGER DEFAULT 0;
                     ALTER TABLE recurring_transactions ADD COLUMN reminder_days INTEGER DEFAULT 3;"
                )?;
            }

            self.conn.execute_batch(
                "CREATE TABLE IF NOT EXISTS savings_goals (
                    id         INTEGER PRIMARY KEY,
                    name       TEXT NOT NULL,
                    target     REAL NOT NULL,
                    saved      REAL DEFAULT 0.0,
                    currency   TEXT NOT NULL DEFAULT 'USD',
                    deadline   TEXT,
                    color      TEXT,
                    icon       TEXT,
                    created_at TEXT NOT NULL DEFAULT (datetime('now'))
                );

                CREATE TABLE IF NOT EXISTS wishlist_items (
                    id          INTEGER PRIMARY KEY,
                    name        TEXT NOT NULL,
                    amount      REAL NOT NULL,
                    category_id INTEGER REFERENCES categories(id),
                    url         TEXT,
                    note        TEXT,
                    priority    INTEGER DEFAULT 0,
                    purchased   INTEGER DEFAULT 0,
                    created_at  TEXT NOT NULL DEFAULT (datetime('now'))
                );"
            )?;
            self.set_schema_version(7)?;
        }

        if self.get_schema_version()? < 8 {
            self.conn.execute_batch(
                "CREATE TABLE IF NOT EXISTS subscription_categories (
                    id         INTEGER PRIMARY KEY,
                    name       TEXT NOT NULL,
                    icon       TEXT,
                    color      TEXT,
                    sort_order INTEGER DEFAULT 0
                );

                CREATE TABLE IF NOT EXISTS subscriptions (
                    id          INTEGER PRIMARY KEY,
                    name        TEXT NOT NULL,
                    amount      REAL NOT NULL,
                    currency    TEXT NOT NULL DEFAULT 'USD',
                    frequency   TEXT NOT NULL DEFAULT 'monthly',
                    category_id INTEGER NOT NULL REFERENCES subscription_categories(id),
                    start_date  TEXT NOT NULL,
                    next_due    TEXT NOT NULL,
                    active      INTEGER DEFAULT 1,
                    note        TEXT,
                    url         TEXT
                );"
            )?;
            self.seed_default_subscription_categories()?;
            self.set_schema_version(8)?;
        }

        if self.get_schema_version()? < 9 {
            self.conn.execute_batch(
                "CREATE TABLE IF NOT EXISTS credit_cards (
                    id INTEGER PRIMARY KEY,
                    name TEXT NOT NULL,
                    credit_limit REAL,
                    statement_close_day INTEGER NOT NULL,
                    due_day INTEGER NOT NULL,
                    min_payment_pct REAL DEFAULT 2.0,
                    current_balance REAL DEFAULT 0.0,
                    currency TEXT NOT NULL DEFAULT 'USD',
                    color TEXT,
                    active INTEGER DEFAULT 1
                );

                CREATE TABLE IF NOT EXISTS streaks (
                    id INTEGER PRIMARY KEY,
                    streak_type TEXT NOT NULL,
                    start_date TEXT NOT NULL,
                    end_date TEXT,
                    current_count INTEGER DEFAULT 0,
                    best_count INTEGER DEFAULT 0
                );

                CREATE TABLE IF NOT EXISTS achievements (
                    id INTEGER PRIMARY KEY,
                    name TEXT NOT NULL UNIQUE,
                    description TEXT NOT NULL,
                    earned_at TEXT,
                    icon TEXT
                );

                INSERT OR IGNORE INTO achievements (name, description, icon) VALUES
                    ('First Transaction', 'Log your first transaction', 'star-symbolic'),
                    ('7-Day No-Spend', 'Go 7 days without spending', 'fire-symbolic'),
                    ('30-Day No-Spend', 'Go 30 days without spending', 'fire-symbolic'),
                    ('Month Under Budget', 'Stay under budget for a full month', 'shield-symbolic'),
                    ('3 Months Under Budget', 'Stay under budget for 3 consecutive months', 'shield-symbolic'),
                    ('First Goal Completed', 'Complete your first savings goal', 'trophy-symbolic'),
                    ('100 Transactions', 'Log 100 transactions', 'medal-symbolic'),
                    ('Budget Streak 6mo', 'Stay under budget for 6 consecutive months', 'crown-symbolic');"
            )?;
            self.set_schema_version(9)?;
        }

        if version < 10 {
            self.conn.execute_batch(
                "INSERT OR IGNORE INTO achievements (name, description, icon) VALUES
                    ('500 Transactions', 'Log 500 transactions', NULL),
                    ('1000 Transactions', 'Log 1000 transactions', NULL),
                    ('First Recurring', 'Set up your first recurring transaction', NULL),
                    ('5 Goals Completed', 'Complete 5 savings goals', NULL),
                    ('Year Under Budget', 'Stay under budget for 12 consecutive months', NULL),
                    ('Big Saver', 'Save over 50% of income in a single month', NULL),
                    ('Category Master', 'Use 10 or more spending categories', NULL),
                    ('Streak Champion', 'Reach a 14-day no-spend streak', NULL);"
            )?;
            self.set_schema_version(10)?;
        }

        if self.get_schema_version()? < 11 {
            let has_sub_id: bool = self.conn
                .prepare("SELECT subscription_id FROM recurring_transactions LIMIT 0")
                .is_ok();
            if !has_sub_id {
                self.conn.execute_batch(
                    "ALTER TABLE recurring_transactions ADD COLUMN subscription_id INTEGER;"
                )?;
            }

            let has_rec_id: bool = self.conn
                .prepare("SELECT recurring_id FROM subscriptions LIMIT 0")
                .is_ok();
            if !has_rec_id {
                self.conn.execute_batch(
                    "ALTER TABLE subscriptions ADD COLUMN recurring_id INTEGER;"
                )?;
            }

            self.set_schema_version(11)?;
        }

        Ok(())
    }

    fn get_schema_version(&self) -> SqlResult<i32> {
        let count: i64 = self.conn.query_row(
            "SELECT COUNT(*) FROM schema_version",
            [],
            |row| row.get(0),
        )?;
        if count == 0 {
            return Ok(0);
        }
        self.conn.query_row(
            "SELECT version FROM schema_version LIMIT 1",
            [],
            |row| row.get(0),
        )
    }

    fn set_schema_version(&self, version: i32) -> SqlResult<()> {
        self.conn.execute("DELETE FROM schema_version", [])?;
        self.conn.execute(
            "INSERT INTO schema_version (version) VALUES (?1)",
            params![version],
        )?;
        Ok(())
    }

    pub fn schema_version(&self) -> SqlResult<i32> {
        self.get_schema_version()
    }

    // -- Transaction CRUD --

    pub fn insert_transaction(&self, txn: &NewTransaction) -> SqlResult<i64> {
        let now = chrono::Utc::now().to_rfc3339();
        self.conn.execute(
            "INSERT INTO transactions (amount, type, category_id, currency, exchange_rate, note, date, created_at, recurring_id, payee)
             VALUES (?1, ?2, ?3, ?4, ?5, ?6, ?7, ?8, ?9, ?10)",
            params![
                txn.amount,
                txn.transaction_type.as_str(),
                txn.category_id,
                txn.currency,
                txn.exchange_rate,
                txn.note,
                txn.date.format("%Y-%m-%d").to_string(),
                now,
                txn.recurring_id,
                txn.payee,
            ],
        )?;
        Ok(self.conn.last_insert_rowid())
    }

    pub fn find_duplicate_transaction(
        &self,
        amount: f64,
        txn_type: TransactionType,
        category_id: i64,
        date: NaiveDate,
    ) -> SqlResult<bool> {
        let count: i64 = self.conn.query_row(
            "SELECT COUNT(*) FROM transactions WHERE amount = ?1 AND type = ?2 AND category_id = ?3 AND date = ?4",
            params![amount, txn_type.as_str(), category_id, date.format("%Y-%m-%d").to_string()],
            |row| row.get(0),
        )?;
        Ok(count > 0)
    }

    pub fn get_transaction(&self, id: i64) -> SqlResult<Transaction> {
        self.conn.query_row(
            "SELECT id, amount, type, category_id, currency, exchange_rate, note, date, created_at, recurring_id, payee
             FROM transactions WHERE id = ?1",
            params![id],
            |row| Self::row_to_transaction(row),
        )
    }

    pub fn update_transaction(&self, txn: &Transaction) -> SqlResult<()> {
        self.conn.execute(
            "UPDATE transactions SET amount=?1, type=?2, category_id=?3, currency=?4,
             exchange_rate=?5, note=?6, date=?7, payee=?8 WHERE id=?9",
            params![
                txn.amount,
                txn.transaction_type.as_str(),
                txn.category_id,
                txn.currency,
                txn.exchange_rate,
                txn.note,
                txn.date.format("%Y-%m-%d").to_string(),
                txn.payee,
                txn.id,
            ],
        )?;
        Ok(())
    }

    pub fn update_transaction_category(&self, id: i64, category_id: i64) -> SqlResult<()> {
        self.conn.execute(
            "UPDATE transactions SET category_id = ?1 WHERE id = ?2",
            params![category_id, id],
        )?;
        Ok(())
    }

    pub fn delete_transaction(&self, id: i64) -> SqlResult<()> {
        self.conn.execute("DELETE FROM transactions WHERE id = ?1", params![id])?;
        Ok(())
    }

    pub fn list_transactions_by_month(&self, year: i32, month: u32) -> SqlResult<Vec<Transaction>> {
        let prefix = format!("{:04}-{:02}", year, month);
        let mut stmt = self.conn.prepare(
            "SELECT id, amount, type, category_id, currency, exchange_rate, note, date, created_at, recurring_id, payee
             FROM transactions WHERE date LIKE ?1 ORDER BY date DESC, id DESC"
        )?;
        let rows = stmt.query_map(params![format!("{}%", prefix)], |row| {
            Self::row_to_transaction(row)
        })?;
        rows.collect()
    }

    pub fn list_recent_transactions(&self, limit: usize) -> SqlResult<Vec<Transaction>> {
        let mut stmt = self.conn.prepare(
            "SELECT id, amount, type, category_id, currency, exchange_rate, note, date, created_at, recurring_id, payee
             FROM transactions ORDER BY date DESC, id DESC LIMIT ?1"
        )?;
        let rows = stmt.query_map(params![limit as i64], |row| {
            Self::row_to_transaction(row)
        })?;
        rows.collect()
    }

    pub fn list_all_transactions(
        &self,
        from: Option<NaiveDate>,
        to: Option<NaiveDate>,
    ) -> SqlResult<Vec<Transaction>> {
        let (sql, params_vec): (&str, Vec<String>) = match (from, to) {
            (Some(f), Some(t)) => (
                "SELECT id, amount, type, category_id, currency, exchange_rate, note, date, created_at, recurring_id, payee
                 FROM transactions WHERE date >= ?1 AND date <= ?2 ORDER BY date ASC, id ASC",
                vec![f.format("%Y-%m-%d").to_string(), t.format("%Y-%m-%d").to_string()],
            ),
            (Some(f), None) => (
                "SELECT id, amount, type, category_id, currency, exchange_rate, note, date, created_at, recurring_id, payee
                 FROM transactions WHERE date >= ?1 ORDER BY date ASC, id ASC",
                vec![f.format("%Y-%m-%d").to_string()],
            ),
            (None, Some(t)) => (
                "SELECT id, amount, type, category_id, currency, exchange_rate, note, date, created_at, recurring_id, payee
                 FROM transactions WHERE date <= ?1 ORDER BY date ASC, id ASC",
                vec![t.format("%Y-%m-%d").to_string()],
            ),
            (None, None) => (
                "SELECT id, amount, type, category_id, currency, exchange_rate, note, date, created_at, recurring_id, payee
                 FROM transactions ORDER BY date ASC, id ASC",
                vec![],
            ),
        };
        let mut stmt = self.conn.prepare(sql)?;
        let param_refs: Vec<&dyn rusqlite::types::ToSql> = params_vec
            .iter()
            .map(|s| s as &dyn rusqlite::types::ToSql)
            .collect();
        let rows = stmt.query_map(param_refs.as_slice(), |row| {
            Self::row_to_transaction(row)
        })?;
        rows.collect()
    }

    fn row_to_transaction(row: &rusqlite::Row) -> SqlResult<Transaction> {
        let type_str: String = row.get(2)?;
        let date_str: String = row.get(7)?;
        Ok(Transaction {
            id: row.get(0)?,
            amount: row.get(1)?,
            transaction_type: TransactionType::from_str(&type_str).unwrap_or(TransactionType::Expense),
            category_id: row.get(3)?,
            currency: row.get(4)?,
            exchange_rate: row.get(5)?,
            note: row.get(6)?,
            date: NaiveDate::parse_from_str(&date_str, "%Y-%m-%d").unwrap_or_default(),
            created_at: row.get(8)?,
            recurring_id: row.get(9)?,
            payee: row.get(10)?,
        })
    }

    // -- Category CRUD --

    pub fn list_categories(&self, txn_type: Option<TransactionType>) -> SqlResult<Vec<Category>> {
        match txn_type {
            Some(t) => {
                let mut stmt = self.conn.prepare(
                    "SELECT id, name, icon, color, type, is_default, sort_order, parent_id
                     FROM categories WHERE type = ?1 ORDER BY sort_order"
                )?;
                let rows = stmt.query_map(params![t.as_str()], |row| Self::row_to_category(row))?;
                rows.collect()
            }
            None => {
                let mut stmt = self.conn.prepare(
                    "SELECT id, name, icon, color, type, is_default, sort_order, parent_id
                     FROM categories ORDER BY type, sort_order"
                )?;
                let rows = stmt.query_map([], |row| Self::row_to_category(row))?;
                rows.collect()
            }
        }
    }

    pub fn get_category(&self, id: i64) -> SqlResult<Category> {
        self.conn.query_row(
            "SELECT id, name, icon, color, type, is_default, sort_order, parent_id
             FROM categories WHERE id = ?1",
            params![id],
            |row| Self::row_to_category(row),
        )
    }

    pub fn insert_category(&self, cat: &NewCategory) -> SqlResult<i64> {
        self.conn.execute(
            "INSERT INTO categories (name, icon, color, type, is_default, sort_order, parent_id)
             VALUES (?1, ?2, ?3, ?4, 0, ?5, ?6)",
            params![
                cat.name,
                cat.icon,
                cat.color,
                cat.transaction_type.as_str(),
                cat.sort_order,
                cat.parent_id,
            ],
        )?;
        Ok(self.conn.last_insert_rowid())
    }

    pub fn update_category(&self, cat: &Category) -> SqlResult<()> {
        self.conn.execute(
            "UPDATE categories SET name=?1, icon=?2, color=?3, sort_order=?4 WHERE id=?5",
            params![cat.name, cat.icon, cat.color, cat.sort_order, cat.id],
        )?;
        Ok(())
    }

    pub fn delete_category(&self, id: i64) -> SqlResult<()> {
        self.conn.execute("DELETE FROM categories WHERE id = ?1", params![id])?;
        Ok(())
    }

    fn row_to_category(row: &rusqlite::Row) -> SqlResult<Category> {
        let type_str: String = row.get(4)?;
        Ok(Category {
            id: row.get(0)?,
            name: row.get(1)?,
            icon: row.get(2)?,
            color: row.get(3)?,
            transaction_type: TransactionType::from_str(&type_str).unwrap_or(TransactionType::Expense),
            is_default: row.get(5)?,
            sort_order: row.get(6)?,
            parent_id: row.get(7)?,
        })
    }

    // -- Aggregation queries --

    pub fn get_monthly_totals_by_category(
        &self,
        year: i32,
        month: u32,
        txn_type: TransactionType,
    ) -> SqlResult<Vec<(Category, f64)>> {
        let prefix = format!("{:04}-{:02}", year, month);
        let mut stmt = self.conn.prepare(
            "SELECT c.id, c.name, c.icon, c.color, c.type, c.is_default, c.sort_order, c.parent_id,
                    SUM(t.amount * t.exchange_rate) as total
             FROM transactions t
             JOIN categories c ON t.category_id = c.id
             WHERE t.date LIKE ?1 AND t.type = ?2
             GROUP BY c.id
             ORDER BY total DESC"
        )?;
        let rows = stmt.query_map(params![format!("{}%", prefix), txn_type.as_str()], |row| {
            let cat = Self::row_to_category(row)?;
            let total: f64 = row.get(8)?;
            Ok((cat, total))
        })?;
        rows.collect()
    }

    pub fn get_monthly_total(
        &self,
        year: i32,
        month: u32,
        txn_type: TransactionType,
    ) -> SqlResult<f64> {
        let prefix = format!("{:04}-{:02}", year, month);
        self.conn.query_row(
            "SELECT COALESCE(SUM(amount * exchange_rate), 0.0)
             FROM transactions WHERE date LIKE ?1 AND type = ?2",
            params![format!("{}%", prefix), txn_type.as_str()],
            |row| row.get(0),
        )
    }

    pub fn get_monthly_total_for_category(
        &self,
        year: i32,
        month: u32,
        category_id: i64,
    ) -> SqlResult<f64> {
        let month_str = format!("{:04}-{:02}", year, month);
        self.conn.query_row(
            "SELECT COALESCE(SUM(amount * exchange_rate), 0) FROM transactions WHERE category_id = ?1 AND date LIKE ?2",
            params![category_id, format!("{}%", month_str)],
            |row| row.get(0),
        )
    }

    pub fn get_daily_totals(
        &self,
        year: i32,
        month: u32,
    ) -> SqlResult<Vec<(NaiveDate, f64, f64)>> {
        let prefix = format!("{:04}-{:02}", year, month);
        let mut stmt = self.conn.prepare(
            "SELECT date,
                    COALESCE(SUM(CASE WHEN type='income' THEN amount * exchange_rate ELSE 0 END), 0.0),
                    COALESCE(SUM(CASE WHEN type='expense' THEN amount * exchange_rate ELSE 0 END), 0.0)
             FROM transactions
             WHERE date LIKE ?1
             GROUP BY date
             ORDER BY date DESC"
        )?;
        let rows = stmt.query_map(params![format!("{}%", prefix)], |row| {
            let date_str: String = row.get(0)?;
            let date = NaiveDate::parse_from_str(&date_str, "%Y-%m-%d").unwrap_or_default();
            let income: f64 = row.get(1)?;
            let expense: f64 = row.get(2)?;
            Ok((date, income, expense))
        })?;
        rows.collect()
    }

    // -- Budgets --

    pub fn set_budget(&self, category_id: i64, month: &str, amount: f64, rollover: bool) -> SqlResult<()> {
        self.conn.execute(
            "INSERT OR REPLACE INTO budgets (category_id, amount, month, rollover) VALUES (?1, ?2, ?3, ?4)",
            params![category_id, amount, month, rollover as i32],
        )?;
        Ok(())
    }

    pub fn get_budget(&self, category_id: i64, month: &str) -> SqlResult<Option<Budget>> {
        match self.conn.query_row(
            "SELECT id, category_id, amount, month, rollover FROM budgets WHERE category_id = ?1 AND month = ?2",
            params![category_id, month],
            |row| {
                let rollover_int: i32 = row.get(4)?;
                Ok(Budget {
                    id: row.get(0)?,
                    category_id: row.get(1)?,
                    amount: row.get(2)?,
                    month: row.get(3)?,
                    rollover: rollover_int != 0,
                })
            },
        ) {
            Ok(b) => Ok(Some(b)),
            Err(rusqlite::Error::QueryReturnedNoRows) => Ok(None),
            Err(e) => Err(e),
        }
    }

    pub fn list_budgets_for_month(&self, month: &str) -> SqlResult<Vec<Budget>> {
        let mut stmt = self.conn.prepare(
            "SELECT id, category_id, amount, month, rollover FROM budgets WHERE month = ?1"
        )?;
        let rows = stmt.query_map(params![month], |row| {
            let rollover_int: i32 = row.get(4)?;
            Ok(Budget {
                id: row.get(0)?,
                category_id: row.get(1)?,
                amount: row.get(2)?,
                month: row.get(3)?,
                rollover: rollover_int != 0,
            })
        })?;
        rows.collect()
    }

    pub fn list_all_budgets(&self) -> SqlResult<Vec<Budget>> {
        let mut stmt = self.conn.prepare(
            "SELECT id, category_id, amount, month, rollover FROM budgets ORDER BY month ASC, id ASC"
        )?;
        let rows = stmt.query_map([], |row| {
            let rollover_int: i32 = row.get(4)?;
            Ok(Budget {
                id: row.get(0)?,
                category_id: row.get(1)?,
                amount: row.get(2)?,
                month: row.get(3)?,
                rollover: rollover_int != 0,
            })
        })?;
        rows.collect()
    }

    pub fn delete_budget(&self, id: i64) -> SqlResult<()> {
        self.conn.execute("DELETE FROM budgets WHERE id = ?1", params![id])?;
        Ok(())
    }

    pub fn get_budget_progress(
        &self,
        category_id: i64,
        month: &str,
    ) -> SqlResult<Option<(f64, f64, f64)>> {
        let budget = match self.get_budget(category_id, month)? {
            Some(b) => b,
            None => return Ok(None),
        };

        let spent: f64 = self.conn.query_row(
            "SELECT COALESCE(SUM(amount * exchange_rate), 0.0) FROM transactions
             WHERE category_id = ?1 AND date LIKE ?2 AND type = 'expense'",
            params![category_id, format!("{}%", month)],
            |row| row.get(0),
        )?;

        let pct = if budget.amount > 0.0 {
            (spent / budget.amount) * 100.0
        } else {
            0.0
        };

        Ok(Some((budget.amount, spent, pct)))
    }

    pub fn copy_budgets(&self, from_month: &str, to_month: &str) -> SqlResult<()> {
        let budgets = self.list_budgets_for_month(from_month)?;
        for b in &budgets {
            self.set_budget(b.category_id, to_month, b.amount, b.rollover)?;
        }
        Ok(())
    }

    /// Get the effective budget including rollover from previous months.
    /// Returns (effective_budget, rollover_amount) where effective_budget = base_budget + rollover.
    pub fn get_budget_with_rollover(
        &self,
        category_id: i64,
        month: &str,
    ) -> SqlResult<Option<(f64, f64)>> {
        let budget = match self.get_budget(category_id, month)? {
            Some(b) => b,
            None => return Ok(None),
        };

        if !budget.rollover {
            return Ok(Some((budget.amount, 0.0)));
        }

        // Walk backwards through previous months accumulating rollover
        let mut rollover = 0.0;
        let mut check_month = prev_month(month);
        for _ in 0..12 {
            // Limit lookback to 12 months
            if let Some(prev_budget) = self.get_budget(category_id, &check_month)? {
                let spent: f64 = self.conn.query_row(
                    "SELECT COALESCE(SUM(amount * exchange_rate), 0.0) FROM transactions
                     WHERE category_id = ?1 AND date LIKE ?2 AND type = 'expense'",
                    params![category_id, format!("{}%", check_month)],
                    |row| row.get(0),
                )?;
                let surplus = prev_budget.amount - spent;
                rollover += surplus;

                if !prev_budget.rollover {
                    break; // Stop if previous month doesn't have rollover enabled
                }
            } else {
                break; // No budget for this month, stop
            }
            check_month = prev_month(&check_month);
        }

        Ok(Some((budget.amount + rollover, rollover)))
    }

    /// Compute the "safe to spend" daily allowance for the current period.
    /// Returns (daily_amount, remaining, days_left).
    pub fn get_safe_to_spend(&self, month: &str) -> SqlResult<(f64, f64, i32)> {
        use chrono::{Datelike, Local, NaiveDate};

        // Total budget for this month
        let total_budget: f64 = self.conn.query_row(
            "SELECT COALESCE(SUM(amount), 0.0) FROM budgets WHERE month = ?1",
            params![month],
            |row| row.get(0),
        )?;

        // Total spent this month
        let total_spent: f64 = self.conn.query_row(
            "SELECT COALESCE(SUM(amount * exchange_rate), 0.0) FROM transactions
             WHERE date LIKE ?1 AND type = 'expense'",
            params![format!("{}%", month)],
            |row| row.get(0),
        )?;

        // Compute days left in the month
        let today = Local::now().date_naive();
        let year: i32 = month[..4].parse().unwrap_or(today.year());
        let mon: u32 = month[5..7].parse().unwrap_or(today.month());

        // Last day of month
        let last_day = if mon == 12 {
            NaiveDate::from_ymd_opt(year + 1, 1, 1)
        } else {
            NaiveDate::from_ymd_opt(year, mon + 1, 1)
        }
        .and_then(|d| d.pred_opt())
        .unwrap_or(today);

        let days_left = (last_day - today).num_days().max(1) as i32;
        let remaining = total_budget - total_spent;
        let safe = if remaining > 0.0 {
            (remaining / days_left as f64) * 0.9
        } else {
            0.0
        };

        Ok((safe, remaining, days_left))
    }

    // -- Recurring Transactions --

    pub fn insert_recurring(&self, r: &NewRecurringTransaction) -> SqlResult<i64> {
        self.conn.execute(
            "INSERT INTO recurring_transactions (amount, type, category_id, currency, note, frequency, start_date, end_date, last_generated, active, is_bill, reminder_days, subscription_id)
             VALUES (?1, ?2, ?3, ?4, ?5, ?6, ?7, ?8, NULL, 1, ?9, ?10, ?11)",
            params![
                r.amount,
                r.transaction_type.as_str(),
                r.category_id,
                r.currency,
                r.note,
                r.frequency.as_str(),
                r.start_date.format("%Y-%m-%d").to_string(),
                r.end_date.map(|d| d.format("%Y-%m-%d").to_string()),
                r.is_bill as i32,
                r.reminder_days,
                r.subscription_id,
            ],
        )?;
        Ok(self.conn.last_insert_rowid())
    }

    pub fn list_recurring(&self, active_only: bool) -> SqlResult<Vec<RecurringTransaction>> {
        let sql = if active_only {
            "SELECT id, amount, type, category_id, currency, note, frequency, start_date, end_date, last_generated, active, resume_date, is_bill, reminder_days, subscription_id
             FROM recurring_transactions WHERE active = 1 ORDER BY id"
        } else {
            "SELECT id, amount, type, category_id, currency, note, frequency, start_date, end_date, last_generated, active, resume_date, is_bill, reminder_days, subscription_id
             FROM recurring_transactions ORDER BY active DESC, id"
        };
        let mut stmt = self.conn.prepare(sql)?;
        let rows = stmt.query_map([], |row| Self::row_to_recurring(row))?;
        rows.collect()
    }

    pub fn get_recurring(&self, id: i64) -> SqlResult<RecurringTransaction> {
        self.conn.query_row(
            "SELECT id, amount, type, category_id, currency, note, frequency, start_date, end_date, last_generated, active, resume_date, is_bill, reminder_days, subscription_id
             FROM recurring_transactions WHERE id = ?1",
            params![id],
            |row| Self::row_to_recurring(row),
        )
    }

    pub fn update_recurring(&self, r: &RecurringTransaction) -> SqlResult<()> {
        self.conn.execute(
            "UPDATE recurring_transactions SET amount=?1, type=?2, category_id=?3, currency=?4,
             note=?5, frequency=?6, start_date=?7, end_date=?8, active=?9, resume_date=?10,
             is_bill=?11, reminder_days=?12, subscription_id=?13 WHERE id=?14",
            params![
                r.amount,
                r.transaction_type.as_str(),
                r.category_id,
                r.currency,
                r.note,
                r.frequency.as_str(),
                r.start_date.format("%Y-%m-%d").to_string(),
                r.end_date.map(|d| d.format("%Y-%m-%d").to_string()),
                r.active,
                r.resume_date.map(|d| d.format("%Y-%m-%d").to_string()),
                r.is_bill,
                r.reminder_days,
                r.subscription_id,
                r.id,
            ],
        )?;
        Ok(())
    }

    pub fn delete_recurring(&self, id: i64) -> SqlResult<()> {
        self.conn.execute("DELETE FROM recurring_transactions WHERE id = ?1", params![id])?;
        Ok(())
    }

    pub fn toggle_recurring_active(&self, id: i64, active: bool) -> SqlResult<()> {
        self.conn.execute(
            "UPDATE recurring_transactions SET active = ?1, resume_date = NULL WHERE id = ?2",
            params![active, id],
        )?;
        // Sync linked subscription
        self.conn.execute(
            "UPDATE subscriptions SET active = ?1 WHERE recurring_id = ?2",
            params![active, id],
        )?;
        Ok(())
    }

    pub fn pause_recurring_with_resume(&self, id: i64, resume_date: Option<NaiveDate>) -> SqlResult<()> {
        self.conn.execute(
            "UPDATE recurring_transactions SET active = 0, resume_date = ?1 WHERE id = ?2",
            params![resume_date.map(|d| d.format("%Y-%m-%d").to_string()), id],
        )?;
        // Sync linked subscription
        self.conn.execute(
            "UPDATE subscriptions SET active = 0 WHERE recurring_id = ?1",
            params![id],
        )?;
        Ok(())
    }

    pub fn auto_resume_recurring(&self, today: NaiveDate) -> SqlResult<u32> {
        let count = self.conn.execute(
            "UPDATE recurring_transactions SET active = 1, resume_date = NULL
             WHERE active = 0 AND resume_date IS NOT NULL AND resume_date <= ?1",
            params![today.format("%Y-%m-%d").to_string()],
        )?;
        Ok(count as u32)
    }

    pub fn update_recurring_last_generated(&self, id: i64, date: NaiveDate) -> SqlResult<()> {
        self.conn.execute(
            "UPDATE recurring_transactions SET last_generated = ?1 WHERE id = ?2",
            params![date.format("%Y-%m-%d").to_string(), id],
        )?;
        Ok(())
    }

    fn row_to_recurring(row: &rusqlite::Row) -> SqlResult<RecurringTransaction> {
        let type_str: String = row.get(2)?;
        let freq_str: String = row.get(6)?;
        let start_str: String = row.get(7)?;
        let end_str: Option<String> = row.get(8)?;
        let last_str: Option<String> = row.get(9)?;
        let resume_str: Option<String> = row.get(11)?;
        let is_bill_int: i32 = row.get(12)?;
        Ok(RecurringTransaction {
            id: row.get(0)?,
            amount: row.get(1)?,
            transaction_type: TransactionType::from_str(&type_str).unwrap_or(TransactionType::Expense),
            category_id: row.get(3)?,
            currency: row.get(4)?,
            note: row.get(5)?,
            frequency: Frequency::from_str(&freq_str).unwrap_or(Frequency::Monthly),
            start_date: NaiveDate::parse_from_str(&start_str, "%Y-%m-%d").unwrap_or_default(),
            end_date: end_str.and_then(|s| NaiveDate::parse_from_str(&s, "%Y-%m-%d").ok()),
            last_generated: last_str.and_then(|s| NaiveDate::parse_from_str(&s, "%Y-%m-%d").ok()),
            active: row.get(10)?,
            resume_date: resume_str.and_then(|s| NaiveDate::parse_from_str(&s, "%Y-%m-%d").ok()),
            is_bill: is_bill_int != 0,
            reminder_days: row.get(13)?,
            subscription_id: row.get(14)?,
        })
    }

    // -- Budget Notifications --

    pub fn has_notification_been_sent(
        &self,
        category_id: i64,
        month: &str,
        threshold: u32,
    ) -> SqlResult<bool> {
        let count: i64 = self.conn.query_row(
            "SELECT COUNT(*) FROM budget_notifications
             WHERE category_id = ?1 AND month = ?2 AND threshold = ?3",
            params![category_id, month, threshold as i64],
            |row| row.get(0),
        )?;
        Ok(count > 0)
    }

    pub fn record_notification(
        &self,
        category_id: i64,
        month: &str,
        threshold: u32,
    ) -> SqlResult<()> {
        let now = chrono::Utc::now().to_rfc3339();
        self.conn.execute(
            "INSERT OR IGNORE INTO budget_notifications (category_id, month, threshold, notified_at)
             VALUES (?1, ?2, ?3, ?4)",
            params![category_id, month, threshold as i64, now],
        )?;
        Ok(())
    }

    pub fn check_budget_thresholds(
        &self,
        category_id: i64,
        month: &str,
    ) -> SqlResult<Vec<u32>> {
        let progress = self.get_budget_progress(category_id, month)?;
        let mut crossed = Vec::new();

        if let Some((_, _, pct)) = progress {
            for threshold in [75, 90, 100] {
                if pct >= threshold as f64
                    && !self.has_notification_been_sent(category_id, month, threshold)?
                {
                    crossed.push(threshold);
                }
            }
        }
        Ok(crossed)
    }

    // -- Savings Goals --

    pub fn insert_goal(
        &self,
        name: &str,
        target: f64,
        currency: &str,
        deadline: Option<NaiveDate>,
        color: Option<&str>,
        icon: Option<&str>,
    ) -> SqlResult<i64> {
        self.conn.execute(
            "INSERT INTO savings_goals (name, target, saved, currency, deadline, color, icon)
             VALUES (?1, ?2, 0.0, ?3, ?4, ?5, ?6)",
            params![
                name,
                target,
                currency,
                deadline.map(|d| d.format("%Y-%m-%d").to_string()),
                color,
                icon,
            ],
        )?;
        Ok(self.conn.last_insert_rowid())
    }

    pub fn list_goals(&self) -> SqlResult<Vec<SavingsGoal>> {
        let mut stmt = self.conn.prepare(
            "SELECT id, name, target, saved, currency, deadline, color, icon, created_at
             FROM savings_goals ORDER BY id"
        )?;
        let rows = stmt.query_map([], |row| {
            let deadline_str: Option<String> = row.get(5)?;
            Ok(SavingsGoal {
                id: row.get(0)?,
                name: row.get(1)?,
                target: row.get(2)?,
                saved: row.get(3)?,
                currency: row.get(4)?,
                deadline: deadline_str.and_then(|s| NaiveDate::parse_from_str(&s, "%Y-%m-%d").ok()),
                color: row.get(6)?,
                icon: row.get(7)?,
                created_at: row.get(8)?,
            })
        })?;
        rows.collect()
    }

    pub fn get_goal(&self, id: i64) -> SqlResult<SavingsGoal> {
        self.conn.query_row(
            "SELECT id, name, target, saved, currency, deadline, color, icon, created_at
             FROM savings_goals WHERE id = ?1",
            params![id],
            |row| {
                let deadline_str: Option<String> = row.get(5)?;
                Ok(SavingsGoal {
                    id: row.get(0)?,
                    name: row.get(1)?,
                    target: row.get(2)?,
                    saved: row.get(3)?,
                    currency: row.get(4)?,
                    deadline: deadline_str.and_then(|s| NaiveDate::parse_from_str(&s, "%Y-%m-%d").ok()),
                    color: row.get(6)?,
                    icon: row.get(7)?,
                    created_at: row.get(8)?,
                })
            },
        )
    }

    pub fn update_goal(
        &self,
        id: i64,
        name: &str,
        target: f64,
        currency: &str,
        deadline: Option<NaiveDate>,
        color: Option<&str>,
        icon: Option<&str>,
    ) -> SqlResult<()> {
        self.conn.execute(
            "UPDATE savings_goals SET name=?1, target=?2, currency=?3, deadline=?4, color=?5, icon=?6
             WHERE id=?7",
            params![
                name,
                target,
                currency,
                deadline.map(|d| d.format("%Y-%m-%d").to_string()),
                color,
                icon,
                id,
            ],
        )?;
        Ok(())
    }

    pub fn delete_goal(&self, id: i64) -> SqlResult<()> {
        self.conn.execute("DELETE FROM savings_goals WHERE id = ?1", params![id])?;
        Ok(())
    }

    pub fn contribute_to_goal(&self, id: i64, amount: f64) -> SqlResult<()> {
        self.conn.execute(
            "UPDATE savings_goals SET saved = saved + ?1 WHERE id = ?2",
            params![amount, id],
        )?;
        Ok(())
    }

    pub fn get_goal_progress(&self, id: i64) -> SqlResult<(f64, f64, f64)> {
        self.conn.query_row(
            "SELECT saved, target FROM savings_goals WHERE id = ?1",
            params![id],
            |row| {
                let saved: f64 = row.get(0)?;
                let target: f64 = row.get(1)?;
                let pct = if target > 0.0 { saved / target * 100.0 } else { 0.0 };
                Ok((saved, target, pct))
            },
        )
    }

    pub fn get_required_monthly(&self, id: i64) -> SqlResult<Option<f64>> {
        let goal = self.get_goal(id)?;
        let remaining = goal.target - goal.saved;
        if remaining <= 0.0 {
            return Ok(Some(0.0));
        }
        match goal.deadline {
            Some(deadline) => {
                let today = chrono::Local::now().date_naive();
                let months_left = (deadline.year() - today.year()) * 12
                    + (deadline.month() as i32 - today.month() as i32);
                if months_left <= 0 {
                    Ok(Some(remaining))
                } else {
                    Ok(Some(remaining / months_left as f64))
                }
            }
            None => Ok(None),
        }
    }

    // -- Wishlist --

    pub fn insert_wishlist_item(
        &self,
        name: &str,
        amount: f64,
        category_id: Option<i64>,
        url: Option<&str>,
        note: Option<&str>,
        priority: i32,
    ) -> SqlResult<i64> {
        self.conn.execute(
            "INSERT INTO wishlist_items (name, amount, category_id, url, note, priority)
             VALUES (?1, ?2, ?3, ?4, ?5, ?6)",
            params![name, amount, category_id, url, note, priority],
        )?;
        Ok(self.conn.last_insert_rowid())
    }

    pub fn list_wishlist(&self, purchased: bool) -> SqlResult<Vec<WishlistItem>> {
        let mut stmt = self.conn.prepare(
            "SELECT id, name, amount, category_id, url, note, priority, purchased, created_at
             FROM wishlist_items WHERE purchased = ?1 ORDER BY priority DESC, id"
        )?;
        let rows = stmt.query_map(params![purchased as i32], |row| {
            let purchased_int: i32 = row.get(7)?;
            Ok(WishlistItem {
                id: row.get(0)?,
                name: row.get(1)?,
                amount: row.get(2)?,
                category_id: row.get(3)?,
                url: row.get(4)?,
                note: row.get(5)?,
                priority: row.get(6)?,
                purchased: purchased_int != 0,
                created_at: row.get(8)?,
            })
        })?;
        rows.collect()
    }

    pub fn mark_wishlist_purchased(&self, id: i64) -> SqlResult<()> {
        self.conn.execute(
            "UPDATE wishlist_items SET purchased = 1 WHERE id = ?1",
            params![id],
        )?;
        Ok(())
    }

    pub fn delete_wishlist_item(&self, id: i64) -> SqlResult<()> {
        self.conn.execute("DELETE FROM wishlist_items WHERE id = ?1", params![id])?;
        Ok(())
    }

    pub fn get_wishlist_total(&self) -> SqlResult<f64> {
        self.conn.query_row(
            "SELECT COALESCE(SUM(amount), 0.0) FROM wishlist_items WHERE purchased = 0",
            [],
            |row| row.get(0),
        )
    }

    // -- Bills --

    pub fn list_upcoming_bills(&self, days_ahead: i32) -> SqlResult<Vec<(RecurringTransaction, NaiveDate)>> {
        let today = chrono::Local::now().date_naive();
        let all_recurring = self.list_recurring(true)?;
        let mut bills = Vec::new();

        for r in all_recurring {
            if !r.is_bill {
                continue;
            }
            if let Some(next) = crate::recurring::next_occurrence(&r, today) {
                let diff = (next - today).num_days();
                if diff >= 0 && diff <= days_ahead as i64 {
                    bills.push((r, next));
                }
            }
        }
        bills.sort_by_key(|(_, date)| *date);
        Ok(bills)
    }

    // -- Subscription Categories --

    fn seed_default_subscription_categories(&self) -> SqlResult<()> {
        let defaults = [
            ("Netflix", "tabler-brand-netflix", "#E50914"),
            ("Spotify", "tabler-brand-spotify", "#1DB954"),
            ("YouTube Premium", "tabler-brand-youtube", "#FF0000"),
            ("Disney+", "tabler-brand-disney", "#113CCF"),
            ("Apple TV+", "tabler-brand-apple", "#555555"),
            ("HBO Max", "tabler-brand-hbo", "#B833FF"),
            ("Amazon Prime", "tabler-brand-amazon", "#FF9900"),
            ("Twitch", "tabler-brand-twitch", "#9146FF"),
            ("Adobe", "tabler-brand-adobe", "#FF0000"),
            ("GitHub", "tabler-brand-github", "#333333"),
            ("Google One", "tabler-brand-google-one", "#4285F4"),
            ("Dropbox", "tabler-brand-dropbox", "#0061FF"),
            ("iCloud", "tabler-cloud", "#3693F3"),
            ("Xbox Game Pass", "tabler-brand-xbox", "#107C10"),
            ("PlayStation Plus", "tabler-device-gamepad", "#003087"),
            ("Nintendo Online", "tabler-device-nintendo", "#E60012"),
            ("Steam", "tabler-brand-steam", "#1B2838"),
            ("Slack", "tabler-brand-slack", "#4A154B"),
            ("Discord Nitro", "tabler-brand-discord", "#5865F2"),
            ("Zoom", "tabler-brand-zoom", "#2D8CFF"),
            ("WhatsApp", "tabler-brand-whatsapp", "#25D366"),
            ("News", "tabler-news", "#444444"),
            ("Fitness", "tabler-barbell", "#FF6B35"),
            ("Other", "tabler-apps", "#95a5a6"),
        ];
        for (i, (name, icon, color)) in defaults.iter().enumerate() {
            self.conn.execute(
                "INSERT INTO subscription_categories (name, icon, color, sort_order) VALUES (?1, ?2, ?3, ?4)",
                params![name, icon, color, i as i32],
            )?;
        }
        Ok(())
    }

    pub fn list_subscription_categories(&self) -> SqlResult<Vec<SubscriptionCategory>> {
        let mut stmt = self.conn.prepare(
            "SELECT id, name, icon, color, sort_order FROM subscription_categories ORDER BY sort_order"
        )?;
        let rows = stmt.query_map([], |row| {
            Ok(SubscriptionCategory {
                id: row.get(0)?,
                name: row.get(1)?,
                icon: row.get(2)?,
                color: row.get(3)?,
                sort_order: row.get(4)?,
            })
        })?;
        rows.collect()
    }

    pub fn get_subscription_category(&self, id: i64) -> SqlResult<SubscriptionCategory> {
        self.conn.query_row(
            "SELECT id, name, icon, color, sort_order FROM subscription_categories WHERE id = ?1",
            params![id],
            |row| {
                Ok(SubscriptionCategory {
                    id: row.get(0)?,
                    name: row.get(1)?,
                    icon: row.get(2)?,
                    color: row.get(3)?,
                    sort_order: row.get(4)?,
                })
            },
        )
    }

    pub fn insert_subscription_category(&self, name: &str, icon: Option<&str>, color: Option<&str>) -> SqlResult<i64> {
        let max_order: i32 = self.conn.query_row(
            "SELECT COALESCE(MAX(sort_order), -1) FROM subscription_categories",
            [],
            |row| row.get(0),
        )?;
        self.conn.execute(
            "INSERT INTO subscription_categories (name, icon, color, sort_order) VALUES (?1, ?2, ?3, ?4)",
            params![name, icon, color, max_order + 1],
        )?;
        Ok(self.conn.last_insert_rowid())
    }

    pub fn update_subscription_category(&self, id: i64, name: &str, icon: Option<&str>, color: Option<&str>) -> SqlResult<()> {
        self.conn.execute(
            "UPDATE subscription_categories SET name = ?1, icon = ?2, color = ?3 WHERE id = ?4",
            params![name, icon, color, id],
        )?;
        Ok(())
    }

    pub fn delete_subscription_category(&self, id: i64) -> SqlResult<()> {
        self.conn.execute("DELETE FROM subscription_categories WHERE id = ?1", params![id])?;
        Ok(())
    }

    // -- Subscriptions --

    fn compute_next_due(start: NaiveDate, frequency: Frequency) -> NaiveDate {
        let today = chrono::Local::now().date_naive();
        let mut due = start;
        while due < today {
            due = match frequency {
                Frequency::Daily => due + chrono::Duration::days(1),
                Frequency::Weekly => due + chrono::Duration::weeks(1),
                Frequency::Biweekly => due + chrono::Duration::weeks(2),
                Frequency::Monthly => {
                    let m = due.month();
                    let y = due.year();
                    if m == 12 {
                        NaiveDate::from_ymd_opt(y + 1, 1, due.day().min(28)).unwrap_or(due + chrono::Duration::days(30))
                    } else {
                        NaiveDate::from_ymd_opt(y, m + 1, due.day().min(28)).unwrap_or(due + chrono::Duration::days(30))
                    }
                }
                Frequency::Yearly => {
                    NaiveDate::from_ymd_opt(due.year() + 1, due.month(), due.day().min(28)).unwrap_or(due + chrono::Duration::days(365))
                }
            };
        }
        due
    }

    fn row_to_subscription(row: &rusqlite::Row) -> rusqlite::Result<Subscription> {
        let freq_str: String = row.get(4)?;
        let start_str: String = row.get(6)?;
        let next_str: String = row.get(7)?;
        Ok(Subscription {
            id: row.get(0)?,
            name: row.get(1)?,
            amount: row.get(2)?,
            currency: row.get(3)?,
            frequency: Frequency::from_str(&freq_str).unwrap_or(Frequency::Monthly),
            category_id: row.get(5)?,
            start_date: NaiveDate::parse_from_str(&start_str, "%Y-%m-%d").unwrap_or_else(|_| chrono::Local::now().date_naive()),
            next_due: NaiveDate::parse_from_str(&next_str, "%Y-%m-%d").unwrap_or_else(|_| chrono::Local::now().date_naive()),
            active: row.get::<_, i32>(8)? != 0,
            note: row.get(9)?,
            url: row.get(10)?,
            recurring_id: row.get(11)?,
        })
    }

    pub fn insert_subscription(&self, sub: &NewSubscription) -> SqlResult<i64> {
        let next_due = Self::compute_next_due(sub.start_date, sub.frequency);
        self.conn.execute(
            "INSERT INTO subscriptions (name, amount, currency, frequency, category_id, start_date, next_due, active, note, url, recurring_id)
             VALUES (?1, ?2, ?3, ?4, ?5, ?6, ?7, 1, ?8, ?9, ?10)",
            params![
                sub.name,
                sub.amount,
                sub.currency,
                sub.frequency.as_str(),
                sub.category_id,
                sub.start_date.format("%Y-%m-%d").to_string(),
                next_due.format("%Y-%m-%d").to_string(),
                sub.note,
                sub.url,
                sub.recurring_id,
            ],
        )?;
        Ok(self.conn.last_insert_rowid())
    }

    pub fn list_subscriptions_v2(&self) -> SqlResult<Vec<Subscription>> {
        let mut stmt = self.conn.prepare(
            "SELECT id, name, amount, currency, frequency, category_id, start_date, next_due, active, note, url, recurring_id
             FROM subscriptions ORDER BY active DESC, name"
        )?;
        let rows = stmt.query_map([], |row| Self::row_to_subscription(row))?;
        rows.collect()
    }

    pub fn update_subscription(&self, sub: &Subscription) -> SqlResult<()> {
        let next_due = Self::compute_next_due(sub.start_date, sub.frequency);
        self.conn.execute(
            "UPDATE subscriptions SET name = ?1, amount = ?2, currency = ?3, frequency = ?4,
             category_id = ?5, start_date = ?6, next_due = ?7, note = ?8, url = ?9,
             recurring_id = ?10
             WHERE id = ?11",
            params![
                sub.name,
                sub.amount,
                sub.currency,
                sub.frequency.as_str(),
                sub.category_id,
                sub.start_date.format("%Y-%m-%d").to_string(),
                next_due.format("%Y-%m-%d").to_string(),
                sub.note,
                sub.url,
                sub.recurring_id,
                sub.id,
            ],
        )?;
        Ok(())
    }

    pub fn delete_subscription(&self, id: i64) -> SqlResult<()> {
        self.conn.execute("DELETE FROM subscriptions WHERE id = ?1", params![id])?;
        Ok(())
    }

    pub fn toggle_subscription_active(&self, id: i64) -> SqlResult<()> {
        self.conn.execute(
            "UPDATE subscriptions SET active = CASE WHEN active = 1 THEN 0 ELSE 1 END WHERE id = ?1",
            params![id],
        )?;
        // Sync linked recurring transaction
        let new_active: bool = self.conn.query_row(
            "SELECT active FROM subscriptions WHERE id = ?1",
            params![id],
            |row| row.get(0),
        )?;
        self.conn.execute(
            "UPDATE recurring_transactions SET active = ?1, resume_date = NULL WHERE subscription_id = ?2",
            params![new_active, id],
        )?;
        Ok(())
    }

    pub fn get_subscription_monthly_total(&self) -> SqlResult<f64> {
        let subs = self.list_subscriptions_v2()?;
        let total = subs.iter()
            .filter(|s| s.active)
            .map(|s| {
                match s.frequency {
                    Frequency::Daily => s.amount * 30.0,
                    Frequency::Weekly => s.amount * 4.33,
                    Frequency::Biweekly => s.amount * 2.17,
                    Frequency::Monthly => s.amount,
                    Frequency::Yearly => s.amount / 12.0,
                }
            }).sum();
        Ok(total)
    }

    pub fn get_subscription_yearly_total(&self) -> SqlResult<f64> {
        let monthly = self.get_subscription_monthly_total()?;
        Ok(monthly * 12.0)
    }

    pub fn find_subscriptions_category_id(&self) -> SqlResult<Option<i64>> {
        let result = self.conn.query_row(
            "SELECT id FROM categories WHERE name = 'Subscriptions' AND type = 'expense' LIMIT 1",
            [],
            |row| row.get(0),
        );
        match result {
            Ok(id) => Ok(Some(id)),
            Err(rusqlite::Error::QueryReturnedNoRows) => Ok(None),
            Err(e) => Err(e),
        }
    }

    pub fn delete_recurring_with_cascade(&self, id: i64) -> SqlResult<()> {
        // Unlink generated transactions so FK doesn't block deletion
        self.conn.execute(
            "UPDATE transactions SET recurring_id = NULL WHERE recurring_id = ?1",
            params![id],
        )?;
        // Delete linked subscriptions
        self.conn.execute(
            "DELETE FROM subscriptions WHERE recurring_id = ?1",
            params![id],
        )?;
        self.conn.execute(
            "DELETE FROM recurring_transactions WHERE id = ?1",
            params![id],
        )?;
        Ok(())
    }

    pub fn delete_subscription_with_cascade(&self, id: i64) -> SqlResult<()> {
        // Unlink generated transactions so FK doesn't block recurring deletion
        self.conn.execute(
            "UPDATE transactions SET recurring_id = NULL WHERE recurring_id IN \
             (SELECT id FROM recurring_transactions WHERE subscription_id = ?1)",
            params![id],
        )?;
        // Delete linked recurring transactions
        self.conn.execute(
            "DELETE FROM recurring_transactions WHERE subscription_id = ?1",
            params![id],
        )?;
        self.conn.execute(
            "DELETE FROM subscriptions WHERE id = ?1",
            params![id],
        )?;
        Ok(())
    }

    pub fn insert_linked_recurring_and_subscription(
        &self,
        rec: &NewRecurringTransaction,
        sub_cat_id: i64,
        name: &str,
    ) -> SqlResult<i64> {
        // Insert the recurring transaction first
        let rec_id = self.insert_recurring(rec)?;

        // Create a matching subscription linked to it
        let sub = NewSubscription {
            name: name.to_string(),
            amount: rec.amount,
            currency: rec.currency.clone(),
            frequency: rec.frequency,
            category_id: sub_cat_id,
            start_date: rec.start_date,
            note: rec.note.clone(),
            url: None,
            recurring_id: Some(rec_id),
        };
        let sub_id = self.insert_subscription(&sub)?;

        // Link back: recurring -> subscription
        self.conn.execute(
            "UPDATE recurring_transactions SET subscription_id = ?1 WHERE id = ?2",
            params![sub_id, rec_id],
        )?;

        Ok(rec_id)
    }

    pub fn insert_linked_subscription_and_recurring(
        &self,
        sub: &NewSubscription,
        expense_cat_id: i64,
    ) -> SqlResult<i64> {
        // Insert the subscription first
        let sub_id = self.insert_subscription(sub)?;

        // Create a matching recurring transaction linked to it
        let rec = NewRecurringTransaction {
            amount: sub.amount,
            transaction_type: TransactionType::Expense,
            category_id: expense_cat_id,
            currency: sub.currency.clone(),
            note: Some(sub.name.clone()),
            frequency: sub.frequency,
            start_date: sub.start_date,
            end_date: None,
            is_bill: true,
            reminder_days: 3,
            subscription_id: Some(sub_id),
        };
        let rec_id = self.insert_recurring(&rec)?;

        // Link back: subscription -> recurring
        self.conn.execute(
            "UPDATE subscriptions SET recurring_id = ?1 WHERE id = ?2",
            params![rec_id, sub_id],
        )?;

        Ok(sub_id)
    }

    // -- Forecasting --

    /// Forecast cash flow for the next N months.
    /// Returns Vec of (month_key, projected_income, projected_expenses, running_balance).
    pub fn forecast_cash_flow(&self, months_ahead: i32) -> SqlResult<Vec<(String, f64, f64, f64)>> {
        let today = chrono::Local::now().date_naive();

        // Get average non-recurring income and expenses over last 3 months
        let mut avg_income = 0.0;
        let mut avg_expense = 0.0;
        let mut months_counted = 0;
        for i in 1..=3 {
            let m = if today.month() as i32 - i <= 0 {
                (today.month() as i32 - i + 12) as u32
            } else {
                (today.month() as i32 - i) as u32
            };
            let y = if today.month() as i32 - i <= 0 {
                today.year() - 1
            } else {
                today.year()
            };
            let inc = self.get_monthly_total(y, m, TransactionType::Income).unwrap_or(0.0);
            let exp = self.get_monthly_total(y, m, TransactionType::Expense).unwrap_or(0.0);
            if inc > 0.0 || exp > 0.0 {
                avg_income += inc;
                avg_expense += exp;
                months_counted += 1;
            }
        }
        if months_counted > 0 {
            avg_income /= months_counted as f64;
            avg_expense /= months_counted as f64;
        }

        // Sum recurring transactions (monthly equivalents)
        let recurring = self.list_recurring(true).unwrap_or_default();
        let mut rec_income = 0.0;
        let mut rec_expense = 0.0;
        for r in &recurring {
            let monthly_amt = match r.frequency {
                Frequency::Daily => r.amount * 30.0,
                Frequency::Weekly => r.amount * 4.33,
                Frequency::Biweekly => r.amount * 2.17,
                Frequency::Monthly => r.amount,
                Frequency::Yearly => r.amount / 12.0,
            };
            match r.transaction_type {
                TransactionType::Income => rec_income += monthly_amt,
                TransactionType::Expense => rec_expense += monthly_amt,
            }
        }

        // Use max of recurring vs historical average for projections
        let proj_income = if rec_income > avg_income { rec_income } else { avg_income };
        let proj_expense = if rec_expense > avg_expense { rec_expense } else { avg_expense };

        // Current month actuals
        let current_income = self.get_monthly_total(today.year(), today.month(), TransactionType::Income).unwrap_or(0.0);
        let current_expense = self.get_monthly_total(today.year(), today.month(), TransactionType::Expense).unwrap_or(0.0);

        let mut results = Vec::new();
        let mut balance = current_income - current_expense;

        for i in 0..months_ahead {
            let month_offset = today.month() as i32 + i;
            let y = today.year() + (month_offset - 1) / 12;
            let m = ((month_offset - 1) % 12 + 1) as u32;
            let key = format!("{:04}-{:02}", y, m);

            if i == 0 {
                results.push((key, current_income, current_expense, balance));
            } else {
                balance += proj_income - proj_expense;
                results.push((key, proj_income, proj_expense, balance));
            }
        }

        Ok(results)
    }

    // -- Insights / Anomaly Detection --

    /// Detect spending anomalies for the given month.
    /// Returns a list of (message, amount, category_id) tuples.
    pub fn detect_anomalies(&self, year: i32, month: u32) -> Vec<(String, f64, Option<i64>)> {
        let mut insights = Vec::new();
        let month_str = format!("{:04}-{:02}", year, month);

        // Get all categories with spending this month
        let cats = self.list_categories(Some(TransactionType::Expense)).unwrap_or_default();

        for cat in &cats {
            let current = self.get_category_total(cat.id, &month_str).unwrap_or(0.0);
            if current <= 0.0 { continue; }

            // 3-month average
            let mut avg_total = 0.0;
            let mut avg_months = 0;
            for i in 1..=3 {
                let pm = if month as i32 - i <= 0 {
                    (month as i32 - i + 12) as u32
                } else {
                    (month as i32 - i) as u32
                };
                let py = if month as i32 - i <= 0 { year - 1 } else { year };
                let prev_key = format!("{:04}-{:02}", py, pm);
                let prev = self.get_category_total(cat.id, &prev_key).unwrap_or(0.0);
                if prev > 0.0 {
                    avg_total += prev;
                    avg_months += 1;
                }
            }

            if avg_months > 0 {
                let avg = avg_total / avg_months as f64;
                if current > avg * 1.5 {
                    let pct = ((current / avg - 1.0) * 100.0) as i32;
                    insights.push((
                        format!("{} is {}% above your 3-month average ({:.0} vs {:.0})", cat.name, pct, current, avg),
                        current - avg,
                        Some(cat.id),
                    ));
                } else if current < avg * 0.5 && avg > 10.0 {
                    let pct = ((1.0 - current / avg) * 100.0) as i32;
                    insights.push((
                        format!("{} is {}% below your 3-month average ({:.0} vs {:.0})", cat.name, pct, current, avg),
                        avg - current,
                        Some(cat.id),
                    ));
                }
            } else {
                // New category - no previous spending
                insights.push((
                    format!("New spending in {} this month ({:.0})", cat.name, current),
                    current,
                    Some(cat.id),
                ));
            }
        }

        // Overall spending comparison
        let total_expense = self.get_monthly_total(year, month, TransactionType::Expense).unwrap_or(0.0);
        let mut avg_total_expense = 0.0;
        let mut avg_months = 0;
        for i in 1..=3 {
            let pm = if month as i32 - i <= 0 { (month as i32 - i + 12) as u32 } else { (month as i32 - i) as u32 };
            let py = if month as i32 - i <= 0 { year - 1 } else { year };
            let prev = self.get_monthly_total(py, pm, TransactionType::Expense).unwrap_or(0.0);
            if prev > 0.0 {
                avg_total_expense += prev;
                avg_months += 1;
            }
        }
        if avg_months > 0 {
            let avg = avg_total_expense / avg_months as f64;
            let change = (total_expense / avg - 1.0) * 100.0;
            if change > 20.0 {
                insights.push((
                    format!("Total spending is up {:.0}% compared to your 3-month average", change),
                    total_expense - avg,
                    None,
                ));
            } else if change < -20.0 {
                insights.push((
                    format!("Total spending is down {:.0}% compared to your 3-month average", change.abs()),
                    avg - total_expense,
                    None,
                ));
            }
        }

        // Sort by deviation amount (largest first) and limit to 5
        insights.sort_by(|a, b| b.1.partial_cmp(&a.1).unwrap_or(std::cmp::Ordering::Equal));
        insights.truncate(5);
        insights
    }

    /// Get total spending for a category in a given month.
    pub fn get_category_total(&self, category_id: i64, month: &str) -> SqlResult<f64> {
        self.conn.query_row(
            "SELECT COALESCE(SUM(amount * exchange_rate), 0.0) FROM transactions
             WHERE category_id = ?1 AND strftime('%Y-%m', date) = ?2 AND type = 'expense'",
            params![category_id, month],
            |row| row.get(0),
        )
    }

    /// Get daily average expenses for a given month (so far).
    pub fn get_daily_average(&self, year: i32, month: u32) -> SqlResult<f64> {
        let month_str = format!("{:04}-{:02}", year, month);
        let total: f64 = self.conn.query_row(
            "SELECT COALESCE(SUM(amount * exchange_rate), 0.0) FROM transactions
             WHERE strftime('%Y-%m', date) = ?1 AND type = 'expense'",
            params![month_str],
            |row| row.get(0),
        )?;

        let today = chrono::Local::now().date_naive();
        let days_elapsed = if year == today.year() && month == today.month() {
            today.day() as f64
        } else {
            // Past month - use full days in month
            let days_in = if month == 12 {
                NaiveDate::from_ymd_opt(year + 1, 1, 1)
            } else {
                NaiveDate::from_ymd_opt(year, month + 1, 1)
            }.and_then(|d| d.pred_opt()).map(|d| d.day()).unwrap_or(30) as f64;
            days_in
        };

        if days_elapsed > 0.0 {
            Ok(total / days_elapsed)
        } else {
            Ok(0.0)
        }
    }

    /// Get weekly average expenses for a given month.
    pub fn get_weekly_average(&self, year: i32, month: u32) -> SqlResult<f64> {
        let daily = self.get_daily_average(year, month)?;
        Ok(daily * 7.0)
    }

    /// Get average monthly spending for a category over the last N months.
    pub fn get_category_monthly_average(&self, category_id: i64, months: i32) -> SqlResult<f64> {
        let today = chrono::Local::now().date_naive();
        let mut total = 0.0;
        let mut counted = 0;
        for i in 1..=months {
            let m = if today.month() as i32 - i <= 0 {
                (today.month() as i32 - i + 12) as u32
            } else {
                (today.month() as i32 - i) as u32
            };
            let y = if today.month() as i32 - i <= 0 { today.year() - 1 } else { today.year() };
            let key = format!("{:04}-{:02}", y, m);
            let amt = self.get_category_total(category_id, &key).unwrap_or(0.0);
            if amt > 0.0 {
                total += amt;
                counted += 1;
            }
        }
        if counted > 0 {
            Ok(total / counted as f64)
        } else {
            Ok(0.0)
        }
    }

    // -- Year-over-Year Comparison --

    /// Compare the same month across multiple years.
    /// Returns Vec of (year, total_income, total_expense).
    pub fn get_yearly_comparison(&self, month_num: u32, years: &[i32]) -> Vec<(i32, f64, f64)> {
        let mut results = Vec::new();
        for &year in years {
            let income = self.get_monthly_total(year, month_num, TransactionType::Income).unwrap_or(0.0);
            let expense = self.get_monthly_total(year, month_num, TransactionType::Expense).unwrap_or(0.0);
            results.push((year, income, expense));
        }
        results
    }

    // -- Custom Date Ranges --

    /// List transactions within a date range.
    pub fn list_transactions_in_range(&self, start: NaiveDate, end: NaiveDate) -> SqlResult<Vec<Transaction>> {
        let start_str = start.format("%Y-%m-%d").to_string();
        let end_str = end.format("%Y-%m-%d").to_string();
        let mut stmt = self.conn.prepare(
            "SELECT id, amount, type, category_id, currency, exchange_rate, note, date, created_at, recurring_id, payee
             FROM transactions WHERE date >= ?1 AND date <= ?2 ORDER BY date DESC, id DESC"
        )?;
        let rows = stmt.query_map(params![start_str, end_str], |row| {
            Ok(Transaction {
                id: row.get(0)?,
                amount: row.get(1)?,
                transaction_type: TransactionType::from_str(&row.get::<_, String>(2)?).unwrap_or(TransactionType::Expense),
                category_id: row.get(3)?,
                currency: row.get(4)?,
                exchange_rate: row.get(5)?,
                note: row.get(6)?,
                date: NaiveDate::parse_from_str(&row.get::<_, String>(7)?, "%Y-%m-%d").unwrap_or_default(),
                created_at: row.get(8)?,
                recurring_id: row.get(9)?,
                payee: row.get(10)?,
            })
        })?;
        rows.collect()
    }

    /// Get income and expense totals within a date range.
    pub fn get_totals_in_range(&self, start: NaiveDate, end: NaiveDate) -> SqlResult<(f64, f64)> {
        let start_str = start.format("%Y-%m-%d").to_string();
        let end_str = end.format("%Y-%m-%d").to_string();
        let income: f64 = self.conn.query_row(
            "SELECT COALESCE(SUM(amount * exchange_rate), 0.0) FROM transactions
             WHERE date >= ?1 AND date <= ?2 AND type = 'income'",
            params![start_str, end_str],
            |row| row.get(0),
        )?;
        let expense: f64 = self.conn.query_row(
            "SELECT COALESCE(SUM(amount * exchange_rate), 0.0) FROM transactions
             WHERE date >= ?1 AND date <= ?2 AND type = 'expense'",
            params![start_str, end_str],
            |row| row.get(0),
        )?;
        Ok((income, expense))
    }

    /// Get daily expense totals within a date range.
    pub fn get_daily_totals_in_range(&self, start: NaiveDate, end: NaiveDate) -> SqlResult<Vec<(NaiveDate, f64)>> {
        let start_str = start.format("%Y-%m-%d").to_string();
        let end_str = end.format("%Y-%m-%d").to_string();
        let mut stmt = self.conn.prepare(
            "SELECT date, COALESCE(SUM(amount * exchange_rate), 0.0) FROM transactions
             WHERE date >= ?1 AND date <= ?2 AND type = 'expense'
             GROUP BY date ORDER BY date"
        )?;
        let rows = stmt.query_map(params![start_str, end_str], |row| {
            let date_str: String = row.get(0)?;
            let amount: f64 = row.get(1)?;
            Ok((NaiveDate::parse_from_str(&date_str, "%Y-%m-%d").unwrap_or_default(), amount))
        })?;
        rows.collect()
    }

    // -- Category History --

    /// Get last N months of total spending for a category.
    pub fn get_category_monthly_history(&self, category_id: i64, months: i32) -> SqlResult<Vec<f64>> {
        let today = chrono::Local::now().date_naive();
        let mut results = Vec::new();
        for i in (0..months).rev() {
            let m = if today.month() as i32 - i <= 0 {
                (today.month() as i32 - i + 12) as u32
            } else {
                (today.month() as i32 - i) as u32
            };
            let y = if today.month() as i32 - i <= 0 { today.year() - 1 } else { today.year() };
            let key = format!("{:04}-{:02}", y, m);
            let amt = self.get_category_total(category_id, &key).unwrap_or(0.0);
            results.push(amt);
        }
        Ok(results)
    }

    // -- Currency Conversion --

    /// Convert amount from one currency to another using cached rates.
    pub fn convert_amount(&self, amount: f64, from: &str, to: &str) -> Option<f64> {
        if from.eq_ignore_ascii_case(to) {
            return Some(amount);
        }
        if let Ok(Some(rate)) = self.get_cached_rate(from, to) {
            Some(amount * rate.rate)
        } else {
            None
        }
    }

    // -- Exchange Rates --

    pub fn get_cached_rate(&self, base: &str, target: &str) -> SqlResult<Option<ExchangeRate>> {
        match self.conn.query_row(
            "SELECT base, target, rate, fetched_at FROM exchange_rates WHERE base = ?1 AND target = ?2",
            params![base.to_lowercase(), target.to_lowercase()],
            |row| {
                Ok(ExchangeRate {
                    base: row.get(0)?,
                    target: row.get(1)?,
                    rate: row.get(2)?,
                    fetched_at: row.get(3)?,
                })
            },
        ) {
            Ok(rate) => Ok(Some(rate)),
            Err(rusqlite::Error::QueryReturnedNoRows) => Ok(None),
            Err(e) => Err(e),
        }
    }

    pub fn cache_rates(&self, base: &str, rates: &std::collections::HashMap<String, f64>) -> SqlResult<()> {
        let now = chrono::Utc::now().to_rfc3339();
        let base_lower = base.to_lowercase();
        let mut stmt = self.conn.prepare(
            "INSERT OR REPLACE INTO exchange_rates (base, target, rate, fetched_at) VALUES (?1, ?2, ?3, ?4)"
        )?;
        for (target, rate) in rates {
            stmt.execute(params![base_lower, target.to_lowercase(), rate, now])?;
        }
        Ok(())
    }

    // -- Settings --

    pub fn get_setting(&self, key: &str) -> SqlResult<Option<String>> {
        match self.conn.query_row(
            "SELECT value FROM settings WHERE key = ?1",
            params![key],
            |row| row.get(0),
        ) {
            Ok(val) => Ok(Some(val)),
            Err(rusqlite::Error::QueryReturnedNoRows) => Ok(None),
            Err(e) => Err(e),
        }
    }

    pub fn set_setting(&self, key: &str, value: &str) -> SqlResult<()> {
        self.conn.execute(
            "INSERT OR REPLACE INTO settings (key, value) VALUES (?1, ?2)",
            params![key, value],
        )?;
        Ok(())
    }

    pub fn execute_raw(&self, sql: &str) -> SqlResult<()> {
        self.conn.execute_batch(sql)?;
        Ok(())
    }

    pub fn get_date_format_string(&self) -> String {
        match self.get_setting("date_format").ok().flatten().as_deref() {
            Some("dmy") => "%d/%m/%Y".to_string(),
            Some("mdy") => "%m/%d/%Y".to_string(),
            _ => "%Y-%m-%d".to_string(),
        }
    }

    pub fn format_date(&self, date: chrono::NaiveDate) -> String {
        date.format(&self.get_date_format_string()).to_string()
    }

    // -- Attachment CRUD --

    pub fn insert_attachment(&self, transaction_id: i64, filename: &str, mime_type: &str, data: &[u8]) -> SqlResult<i64> {
        self.conn.execute(
            "INSERT INTO attachments (transaction_id, filename, mime_type, data) VALUES (?1, ?2, ?3, ?4)",
            params![transaction_id, filename, mime_type, data],
        )?;
        Ok(self.conn.last_insert_rowid())
    }

    pub fn list_attachments(&self, transaction_id: i64) -> SqlResult<Vec<(i64, String, String, Vec<u8>)>> {
        let mut stmt = self.conn.prepare(
            "SELECT id, filename, mime_type, data FROM attachments WHERE transaction_id = ?1"
        )?;
        let rows = stmt.query_map([transaction_id], |row| {
            Ok((row.get(0)?, row.get(1)?, row.get(2)?, row.get(3)?))
        })?;
        rows.collect()
    }

    pub fn delete_attachment(&self, id: i64) -> SqlResult<()> {
        self.conn.execute("DELETE FROM attachments WHERE id = ?1", [id])?;
        Ok(())
    }

    /// Returns the most frequently used category IDs for a transaction type,
    /// based on the last N transactions of that type.
    pub fn get_recent_category_ids(
        &self,
        txn_type: TransactionType,
        limit: u32,
    ) -> SqlResult<Vec<i64>> {
        let mut stmt = self.conn.prepare(
            "SELECT category_id, COUNT(*) as cnt
             FROM transactions WHERE type = ?1
             GROUP BY category_id
             ORDER BY cnt DESC
             LIMIT ?2"
        )?;
        let rows = stmt.query_map(params![txn_type.as_str(), limit], |row| {
            row.get(0)
        })?;
        let mut ids = Vec::new();
        for id in rows {
            ids.push(id?);
        }
        Ok(ids)
    }

    /// Returns (income, expenses) for the month prior to the given one.
    pub fn get_previous_month_totals(
        &self,
        year: i32,
        month: u32,
    ) -> SqlResult<(f64, f64)> {
        let (prev_year, prev_month) = if month == 1 {
            (year - 1, 12u32)
        } else {
            (year, month - 1)
        };
        let income = self.get_monthly_total(prev_year, prev_month, TransactionType::Income)?;
        let expenses = self.get_monthly_total(prev_year, prev_month, TransactionType::Expense)?;
        Ok((income, expenses))
    }

    /// Returns the number of transactions in a given month.
    pub fn get_transaction_count_for_month(
        &self,
        year: i32,
        month: u32,
    ) -> SqlResult<i64> {
        let prefix = format!("{:04}-{:02}", year, month);
        self.conn.query_row(
            "SELECT COUNT(*) FROM transactions WHERE date LIKE ?1",
            params![format!("{}%", prefix)],
            |row| row.get(0),
        )
    }

    /// Returns (transaction_count, category_count, earliest_date, latest_date) for export preview.
    pub fn get_export_stats(&self) -> SqlResult<(i64, i64, Option<String>, Option<String>)> {
        let txn_count: i64 = self.conn.query_row(
            "SELECT COUNT(*) FROM transactions", [], |row| row.get(0),
        )?;
        let cat_count: i64 = self.conn.query_row(
            "SELECT COUNT(DISTINCT category_id) FROM transactions", [], |row| row.get(0),
        )?;
        let earliest: Option<String> = self.conn.query_row(
            "SELECT MIN(date) FROM transactions", [], |row| row.get(0),
        )?;
        let latest: Option<String> = self.conn.query_row(
            "SELECT MAX(date) FROM transactions", [], |row| row.get(0),
        )?;
        Ok((txn_count, cat_count, earliest, latest))
    }

    /// Returns all budget progress for a month.
    /// Each entry: (category_name, spent, budget_amount, percentage, category_id).
    pub fn get_all_budget_progress(
        &self,
        year: i32,
        month: u32,
    ) -> SqlResult<Vec<(String, f64, f64, f64, i64)>> {
        let month_str = format!("{:04}-{:02}", year, month);
        let mut stmt = self.conn.prepare(
            "SELECT c.name, b.amount, b.category_id,
                    COALESCE(SUM(t.amount * t.exchange_rate), 0.0) as spent
             FROM budgets b
             JOIN categories c ON c.id = b.category_id
             LEFT JOIN transactions t ON t.category_id = b.category_id
                  AND t.date LIKE ?1 AND t.type = 'expense'
             WHERE b.month = ?2
             GROUP BY b.category_id"
        )?;
        let prefix = format!("{}%", month_str);
        let rows = stmt.query_map(params![prefix, month_str], |row| {
            let name: String = row.get(0)?;
            let budget: f64 = row.get(1)?;
            let cat_id: i64 = row.get(2)?;
            let spent: f64 = row.get(3)?;
            let pct = if budget > 0.0 { spent / budget * 100.0 } else { 0.0 };
            Ok((name, spent, budget, pct, cat_id))
        })?;
        let mut result = Vec::new();
        for r in rows {
            result.push(r?);
        }
        Ok(result)
    }

    // -- Tags --

    pub fn get_or_create_tag(&self, name: &str) -> SqlResult<i64> {
        let name = name.trim().trim_start_matches('#').to_lowercase();
        self.conn.execute(
            "INSERT OR IGNORE INTO tags (name) VALUES (?1)",
            params![name],
        )?;
        self.conn.query_row(
            "SELECT id FROM tags WHERE name = ?1",
            params![name],
            |row| row.get(0),
        )
    }

    pub fn list_tags(&self) -> SqlResult<Vec<Tag>> {
        let mut stmt = self.conn.prepare("SELECT id, name FROM tags ORDER BY name")?;
        let rows = stmt.query_map([], |row| {
            Ok(Tag { id: row.get(0)?, name: row.get(1)? })
        })?;
        rows.collect()
    }

    pub fn delete_tag(&self, id: i64) -> SqlResult<()> {
        self.conn.execute("DELETE FROM tags WHERE id = ?1", params![id])?;
        Ok(())
    }

    pub fn set_transaction_tags(&self, txn_id: i64, tag_ids: &[i64]) -> SqlResult<()> {
        self.conn.execute(
            "DELETE FROM transaction_tags WHERE transaction_id = ?1",
            params![txn_id],
        )?;
        let mut stmt = self.conn.prepare(
            "INSERT INTO transaction_tags (transaction_id, tag_id) VALUES (?1, ?2)"
        )?;
        for tag_id in tag_ids {
            stmt.execute(params![txn_id, tag_id])?;
        }
        Ok(())
    }

    pub fn get_transaction_tags(&self, txn_id: i64) -> SqlResult<Vec<Tag>> {
        let mut stmt = self.conn.prepare(
            "SELECT t.id, t.name FROM tags t
             JOIN transaction_tags tt ON tt.tag_id = t.id
             WHERE tt.transaction_id = ?1 ORDER BY t.name"
        )?;
        let rows = stmt.query_map(params![txn_id], |row| {
            Ok(Tag { id: row.get(0)?, name: row.get(1)? })
        })?;
        rows.collect()
    }

    pub fn list_tags_for_month(&self, year: i32, month: u32) -> SqlResult<Vec<Tag>> {
        let prefix = format!("{:04}-{:02}", year, month);
        let mut stmt = self.conn.prepare(
            "SELECT DISTINCT t.id, t.name FROM tags t
             JOIN transaction_tags tt ON tt.tag_id = t.id
             JOIN transactions txn ON txn.id = tt.transaction_id
             WHERE txn.date LIKE ?1
             ORDER BY t.name"
        )?;
        let rows = stmt.query_map(params![format!("{}%", prefix)], |row| {
            Ok(Tag { id: row.get(0)?, name: row.get(1)? })
        })?;
        rows.collect()
    }

    // -- Payees --

    pub fn list_payees(&self) -> SqlResult<Vec<String>> {
        let mut stmt = self.conn.prepare(
            "SELECT DISTINCT payee FROM transactions WHERE payee IS NOT NULL AND payee != '' ORDER BY payee"
        )?;
        let rows = stmt.query_map([], |row| row.get(0))?;
        rows.collect()
    }

    // -- Subcategories --

    pub fn list_top_categories(&self, txn_type: TransactionType) -> SqlResult<Vec<Category>> {
        let mut stmt = self.conn.prepare(
            "SELECT id, name, icon, color, type, is_default, sort_order, parent_id
             FROM categories WHERE type = ?1 AND parent_id IS NULL ORDER BY sort_order"
        )?;
        let rows = stmt.query_map(params![txn_type.as_str()], |row| Self::row_to_category(row))?;
        rows.collect()
    }

    pub fn list_subcategories(&self, parent_id: i64) -> SqlResult<Vec<Category>> {
        let mut stmt = self.conn.prepare(
            "SELECT id, name, icon, color, type, is_default, sort_order, parent_id
             FROM categories WHERE parent_id = ?1 ORDER BY sort_order"
        )?;
        let rows = stmt.query_map(params![parent_id], |row| Self::row_to_category(row))?;
        rows.collect()
    }

    // -- Splits --

    pub fn insert_splits(&self, txn_id: i64, splits: &[(i64, f64, Option<String>)]) -> SqlResult<()> {
        let mut stmt = self.conn.prepare(
            "INSERT INTO transaction_splits (transaction_id, category_id, amount, note) VALUES (?1, ?2, ?3, ?4)"
        )?;
        for (cat_id, amount, note) in splits {
            stmt.execute(params![txn_id, cat_id, amount, note])?;
        }
        Ok(())
    }

    pub fn get_splits(&self, txn_id: i64) -> SqlResult<Vec<Split>> {
        let mut stmt = self.conn.prepare(
            "SELECT id, transaction_id, category_id, amount, note FROM transaction_splits WHERE transaction_id = ?1"
        )?;
        let rows = stmt.query_map(params![txn_id], |row| {
            Ok(Split {
                id: row.get(0)?,
                transaction_id: row.get(1)?,
                category_id: row.get(2)?,
                amount: row.get(3)?,
                note: row.get(4)?,
            })
        })?;
        rows.collect()
    }

    pub fn delete_splits(&self, txn_id: i64) -> SqlResult<()> {
        self.conn.execute(
            "DELETE FROM transaction_splits WHERE transaction_id = ?1",
            params![txn_id],
        )?;
        Ok(())
    }

    pub fn has_splits(&self, txn_id: i64) -> SqlResult<bool> {
        let count: i64 = self.conn.query_row(
            "SELECT COUNT(*) FROM transaction_splits WHERE transaction_id = ?1",
            params![txn_id],
            |row| row.get(0),
        )?;
        Ok(count > 0)
    }

    // -- Templates --

    pub fn insert_template(
        &self,
        name: &str,
        amount: Option<f64>,
        txn_type: TransactionType,
        category_id: i64,
        currency: &str,
        payee: Option<&str>,
        note: Option<&str>,
        tags: Option<&str>,
    ) -> SqlResult<i64> {
        self.conn.execute(
            "INSERT INTO transaction_templates (name, amount, type, category_id, currency, payee, note, tags, sort_order)
             VALUES (?1, ?2, ?3, ?4, ?5, ?6, ?7, ?8, 0)",
            params![name, amount, txn_type.as_str(), category_id, currency, payee, note, tags],
        )?;
        Ok(self.conn.last_insert_rowid())
    }

    pub fn list_templates(&self) -> SqlResult<Vec<TransactionTemplate>> {
        let mut stmt = self.conn.prepare(
            "SELECT id, name, amount, type, category_id, currency, payee, note, tags, sort_order
             FROM transaction_templates ORDER BY sort_order, name"
        )?;
        let rows = stmt.query_map([], |row| {
            let type_str: String = row.get(3)?;
            Ok(TransactionTemplate {
                id: row.get(0)?,
                name: row.get(1)?,
                amount: row.get(2)?,
                transaction_type: TransactionType::from_str(&type_str).unwrap_or(TransactionType::Expense),
                category_id: row.get(4)?,
                currency: row.get(5)?,
                payee: row.get(6)?,
                note: row.get(7)?,
                tags: row.get(8)?,
                sort_order: row.get(9)?,
            })
        })?;
        rows.collect()
    }

    pub fn delete_template(&self, id: i64) -> SqlResult<()> {
        self.conn.execute("DELETE FROM transaction_templates WHERE id = ?1", params![id])?;
        Ok(())
    }

    // -- Categorization Rules --

    pub fn insert_rule(
        &self,
        field: &str,
        pattern: &str,
        category_id: i64,
        priority: i32,
    ) -> SqlResult<i64> {
        self.conn.execute(
            "INSERT INTO categorization_rules (field, pattern, category_id, priority) VALUES (?1, ?2, ?3, ?4)",
            params![field, pattern, category_id, priority],
        )?;
        Ok(self.conn.last_insert_rowid())
    }

    pub fn list_rules(&self) -> SqlResult<Vec<CategorizeRule>> {
        let mut stmt = self.conn.prepare(
            "SELECT id, field, pattern, category_id, priority FROM categorization_rules ORDER BY priority DESC, id"
        )?;
        let rows = stmt.query_map([], |row| {
            Ok(CategorizeRule {
                id: row.get(0)?,
                field: row.get(1)?,
                pattern: row.get(2)?,
                category_id: row.get(3)?,
                priority: row.get(4)?,
            })
        })?;
        rows.collect()
    }

    pub fn delete_rule(&self, id: i64) -> SqlResult<()> {
        self.conn.execute("DELETE FROM categorization_rules WHERE id = ?1", params![id])?;
        Ok(())
    }

    pub fn match_category(&self, note: Option<&str>, payee: Option<&str>) -> SqlResult<Option<i64>> {
        let rules = self.list_rules()?;
        for rule in &rules {
            let text = match rule.field.as_str() {
                "note" => note,
                "payee" => payee,
                _ => None,
            };
            if let Some(text) = text {
                if text.to_lowercase().contains(&rule.pattern.to_lowercase()) {
                    return Ok(Some(rule.category_id));
                }
            }
        }
        Ok(None)
    }

    // -- Duplicate detection --

    pub fn find_potential_duplicate_transaction(
        &self,
        amount: f64,
        date: NaiveDate,
        category_id: i64,
    ) -> SqlResult<Option<Transaction>> {
        let prev_day = (date - chrono::Duration::days(1)).format("%Y-%m-%d").to_string();
        let next_day = (date + chrono::Duration::days(1)).format("%Y-%m-%d").to_string();
        match self.conn.query_row(
            "SELECT id, amount, type, category_id, currency, exchange_rate, note, date, created_at, recurring_id, payee
             FROM transactions
             WHERE amount = ?1 AND category_id = ?2 AND date BETWEEN ?3 AND ?4
             ORDER BY id DESC LIMIT 1",
            params![amount, category_id, prev_day, next_day],
            |row| Self::row_to_transaction(row),
        ) {
            Ok(txn) => Ok(Some(txn)),
            Err(rusqlite::Error::QueryReturnedNoRows) => Ok(None),
            Err(e) => Err(e),
        }
    }

    pub fn reset_all_data(&self) -> SqlResult<()> {
        self.conn.execute_batch(
            "DELETE FROM transaction_tags;
             DELETE FROM transaction_splits;
             DELETE FROM attachments;
             DELETE FROM transactions;
             DELETE FROM tags;
             DELETE FROM transaction_templates;
             DELETE FROM categorization_rules;
             DELETE FROM budgets;
             DELETE FROM recurring_transactions;
             DELETE FROM budget_notifications;
             DELETE FROM savings_goals;
             DELETE FROM wishlist_items;
             DELETE FROM categories;
             DELETE FROM exchange_rates;
             DELETE FROM settings;"
        )?;
        self.seed_default_categories()?;
        self.set_setting("schema_version", "1")?;
        Ok(())
    }

    fn seed_default_categories(&self) -> SqlResult<()> {
        let expense_categories = [
            ("Food and Dining", "tabler-burger",         "#e74c3c"),
            ("Groceries",       "tabler-shopping-cart",   "#e67e22"),
            ("Transport",       "tabler-bus",             "#3498db"),
            ("Housing/Rent",    "tabler-home",            "#9b59b6"),
            ("Utilities",       "tabler-bulb",            "#f39c12"),
            ("Entertainment",   "tabler-movie",           "#1abc9c"),
            ("Shopping",        "tabler-shopping-bag",    "#e91e63"),
            ("Health",          "tabler-heartbeat",       "#2ecc71"),
            ("Education",       "tabler-school",          "#00bcd4"),
            ("Subscriptions",   "tabler-device-mobile",   "#ff5722"),
            ("Personal Care",   "tabler-sparkles",        "#795548"),
            ("Gifts",           "tabler-gift",            "#ff9800"),
            ("Travel",          "tabler-plane",           "#607d8b"),
            ("Other",           "tabler-cash",            "#95a5a6"),
        ];

        let income_categories = [
            ("Salary",          "tabler-coins",           "#27ae60"),
            ("Freelance",       "tabler-device-laptop",   "#2980b9"),
            ("Investment",      "tabler-trending-up",     "#8e44ad"),
            ("Gift",            "tabler-gift",            "#f1c40f"),
            ("Refund",          "tabler-arrow-back-up",   "#16a085"),
            ("Other",           "tabler-wallet",          "#7f8c8d"),
        ];

        let mut stmt = self.conn.prepare(
            "INSERT INTO categories (name, icon, color, type, is_default, sort_order)
             VALUES (?1, ?2, ?3, ?4, 1, ?5)"
        )?;

        for (i, (name, icon, color)) in expense_categories.iter().enumerate() {
            stmt.execute(params![name, icon, color, "expense", i as i32])?;
        }

        for (i, (name, icon, color)) in income_categories.iter().enumerate() {
            stmt.execute(params![name, icon, color, "income", i as i32])?;
        }

        Ok(())
    }

    // ---- Credit Cards ----

    pub fn insert_credit_card(&self, card: &crate::models::NewCreditCard) -> SqlResult<i64> {
        self.conn.execute(
            "INSERT INTO credit_cards (name, credit_limit, statement_close_day, due_day, min_payment_pct, currency, color)
             VALUES (?1, ?2, ?3, ?4, ?5, ?6, ?7)",
            params![card.name, card.credit_limit, card.statement_close_day, card.due_day, card.min_payment_pct, card.currency, card.color],
        )?;
        Ok(self.conn.last_insert_rowid())
    }

    pub fn list_credit_cards(&self) -> SqlResult<Vec<crate::models::CreditCard>> {
        let mut stmt = self.conn.prepare(
            "SELECT id, name, credit_limit, statement_close_day, due_day, min_payment_pct, current_balance, currency, color, active
             FROM credit_cards ORDER BY name"
        )?;
        let rows = stmt.query_map([], |row| {
            Ok(crate::models::CreditCard {
                id: row.get(0)?,
                name: row.get(1)?,
                credit_limit: row.get(2)?,
                statement_close_day: row.get(3)?,
                due_day: row.get(4)?,
                min_payment_pct: row.get(5)?,
                current_balance: row.get(6)?,
                currency: row.get(7)?,
                color: row.get(8)?,
                active: row.get::<_, i32>(9)? != 0,
            })
        })?.collect::<SqlResult<Vec<_>>>()?;
        Ok(rows)
    }

    pub fn get_credit_card(&self, id: i64) -> SqlResult<crate::models::CreditCard> {
        self.conn.query_row(
            "SELECT id, name, credit_limit, statement_close_day, due_day, min_payment_pct, current_balance, currency, color, active
             FROM credit_cards WHERE id = ?1",
            params![id],
            |row| {
                Ok(crate::models::CreditCard {
                    id: row.get(0)?,
                    name: row.get(1)?,
                    credit_limit: row.get(2)?,
                    statement_close_day: row.get(3)?,
                    due_day: row.get(4)?,
                    min_payment_pct: row.get(5)?,
                    current_balance: row.get(6)?,
                    currency: row.get(7)?,
                    color: row.get(8)?,
                    active: row.get::<_, i32>(9)? != 0,
                })
            },
        )
    }

    pub fn update_credit_card(&self, card: &crate::models::CreditCard) -> SqlResult<()> {
        self.conn.execute(
            "UPDATE credit_cards SET name=?1, credit_limit=?2, statement_close_day=?3, due_day=?4,
             min_payment_pct=?5, current_balance=?6, currency=?7, color=?8, active=?9
             WHERE id=?10",
            params![card.name, card.credit_limit, card.statement_close_day, card.due_day,
                    card.min_payment_pct, card.current_balance, card.currency, card.color,
                    card.active as i32, card.id],
        )?;
        Ok(())
    }

    pub fn delete_credit_card(&self, id: i64) -> SqlResult<()> {
        self.conn.execute("DELETE FROM credit_cards WHERE id = ?1", params![id])?;
        Ok(())
    }

    pub fn record_card_payment(&self, card_id: i64, amount: f64) -> SqlResult<()> {
        self.conn.execute(
            "UPDATE credit_cards SET current_balance = MAX(0, current_balance - ?1) WHERE id = ?2",
            params![amount, card_id],
        )?;
        Ok(())
    }

    pub fn add_card_charge(&self, card_id: i64, amount: f64) -> SqlResult<()> {
        self.conn.execute(
            "UPDATE credit_cards SET current_balance = current_balance + ?1 WHERE id = ?2",
            params![amount, card_id],
        )?;
        Ok(())
    }

    // ---- Achievements & Streaks ----

    pub fn list_achievements(&self) -> SqlResult<Vec<crate::models::Achievement>> {
        let mut stmt = self.conn.prepare(
            "SELECT id, name, description, earned_at, icon FROM achievements ORDER BY id"
        )?;
        let rows = stmt.query_map([], |row| {
            Ok(crate::models::Achievement {
                id: row.get(0)?,
                name: row.get(1)?,
                description: row.get(2)?,
                earned_at: row.get(3)?,
                icon: row.get(4)?,
            })
        })?.collect::<SqlResult<Vec<_>>>()?;
        Ok(rows)
    }

    pub fn award_achievement(&self, name: &str) -> SqlResult<bool> {
        let now = chrono::Local::now().format("%Y-%m-%d %H:%M:%S").to_string();
        let changed = self.conn.execute(
            "UPDATE achievements SET earned_at = ?1 WHERE name = ?2 AND earned_at IS NULL",
            params![now, name],
        )?;
        Ok(changed > 0)
    }

    pub fn get_no_spend_streak(&self, today: chrono::NaiveDate) -> SqlResult<i32> {
        let mut streak = 0i32;
        let mut day = today;
        loop {
            let day_str = day.format("%Y-%m-%d").to_string();
            let count: i64 = self.conn.query_row(
                "SELECT COUNT(*) FROM transactions WHERE date = ?1 AND type = 'expense'",
                params![day_str],
                |row| row.get(0),
            )?;
            if count > 0 {
                break;
            }
            streak += 1;
            if let Some(prev) = day.pred_opt() {
                day = prev;
            } else {
                break;
            }
            if streak > 365 {
                break;
            }
        }
        Ok(streak)
    }

    pub fn get_total_transaction_count(&self) -> SqlResult<i64> {
        self.conn.query_row("SELECT COUNT(*) FROM transactions", [], |row| row.get(0))
    }

    pub fn check_and_award_achievements(&self, today: chrono::NaiveDate) -> SqlResult<Vec<String>> {
        use chrono::Datelike;
        let mut newly_earned = Vec::new();

        // -- Transaction count milestones --
        let count = self.get_total_transaction_count()?;
        if count >= 1 {
            if self.award_achievement("First Transaction")? {
                newly_earned.push("First Transaction".to_string());
            }
        }
        if count >= 100 {
            if self.award_achievement("100 Transactions")? {
                newly_earned.push("100 Transactions".to_string());
            }
        }
        if count >= 500 {
            if self.award_achievement("500 Transactions")? {
                newly_earned.push("500 Transactions".to_string());
            }
        }
        if count >= 1000 {
            if self.award_achievement("1000 Transactions")? {
                newly_earned.push("1000 Transactions".to_string());
            }
        }

        // -- No-spend streaks --
        let streak = self.get_no_spend_streak(today)?;
        if streak >= 7 {
            if self.award_achievement("7-Day No-Spend")? {
                newly_earned.push("7-Day No-Spend".to_string());
            }
        }
        if streak >= 14 {
            if self.award_achievement("Streak Champion")? {
                newly_earned.push("Streak Champion".to_string());
            }
        }
        if streak >= 30 {
            if self.award_achievement("30-Day No-Spend")? {
                newly_earned.push("30-Day No-Spend".to_string());
            }
        }

        // -- Goals --
        let completed_goals: i64 = self.conn.query_row(
            "SELECT COUNT(*) FROM savings_goals WHERE saved >= target",
            [],
            |row| row.get(0),
        )?;
        if completed_goals >= 1 {
            if self.award_achievement("First Goal Completed")? {
                newly_earned.push("First Goal Completed".to_string());
            }
        }
        if completed_goals >= 5 {
            if self.award_achievement("5 Goals Completed")? {
                newly_earned.push("5 Goals Completed".to_string());
            }
        }

        // -- First Recurring --
        let recurring_count: i64 = self.conn.query_row(
            "SELECT COUNT(*) FROM recurring_transactions",
            [],
            |row| row.get(0),
        )?;
        if recurring_count >= 1 {
            if self.award_achievement("First Recurring")? {
                newly_earned.push("First Recurring".to_string());
            }
        }

        // -- Budget streaks --
        // Count consecutive months under budget going backwards
        let mut budget_streak = 0;
        {
            let mut y = today.year();
            let mut m = if today.month() == 1 { 12 } else { today.month() - 1 };
            if today.month() == 1 { y -= 1; }
            for _ in 0..24 {
                let month_str = format!("{:04}-{:02}", y, m);
                let budgets = self.list_budgets_for_month(&month_str)?;
                if budgets.is_empty() { break; }
                let all_under = budgets.iter().all(|b| {
                    let spent = self.get_category_total(b.category_id, &month_str).unwrap_or(0.0);
                    spent <= b.amount
                });
                if all_under {
                    budget_streak += 1;
                } else {
                    break;
                }
                if m == 1 { y -= 1; m = 12; } else { m -= 1; }
            }
        }
        if budget_streak >= 1 {
            if self.award_achievement("Month Under Budget")? {
                newly_earned.push("Month Under Budget".to_string());
            }
        }
        if budget_streak >= 3 {
            if self.award_achievement("3 Months Under Budget")? {
                newly_earned.push("3 Months Under Budget".to_string());
            }
        }
        if budget_streak >= 6 {
            if self.award_achievement("Budget Streak 6mo")? {
                newly_earned.push("Budget Streak 6mo".to_string());
            }
        }
        if budget_streak >= 12 {
            if self.award_achievement("Year Under Budget")? {
                newly_earned.push("Year Under Budget".to_string());
            }
        }

        // -- Big Saver: save over 50% of income in a single month --
        {
            let (prev_year, prev_month) = if today.month() == 1 {
                (today.year() - 1, 12u32)
            } else {
                (today.year(), today.month() - 1)
            };
            let income = self.get_monthly_total(prev_year, prev_month, crate::models::TransactionType::Income).unwrap_or(0.0);
            let expense = self.get_monthly_total(prev_year, prev_month, crate::models::TransactionType::Expense).unwrap_or(0.0);
            if income > 0.0 && expense < income * 0.5 {
                if self.award_achievement("Big Saver")? {
                    newly_earned.push("Big Saver".to_string());
                }
            }
        }

        // -- Category Master: use 10+ distinct expense categories --
        {
            let distinct_cats: i64 = self.conn.query_row(
                "SELECT COUNT(DISTINCT category_id) FROM transactions WHERE type = 'expense'",
                [],
                |row| row.get(0),
            )?;
            if distinct_cats >= 10 {
                if self.award_achievement("Category Master")? {
                    newly_earned.push("Category Master".to_string());
                }
            }
        }

        Ok(newly_earned)
    }

    // ---- Budget Cycle ----

    pub fn get_budget_cycle_mode(&self) -> crate::models::BudgetCycleMode {
        self.get_setting("budget_cycle_mode")
            .ok().flatten()
            .map(|s| crate::models::BudgetCycleMode::from_str(&s))
            .unwrap_or(crate::models::BudgetCycleMode::Calendar)
    }

    pub fn get_budget_period(&self, ref_date: chrono::NaiveDate) -> (chrono::NaiveDate, chrono::NaiveDate) {
        use chrono::Datelike;
        let mode = self.get_budget_cycle_mode();
        match mode {
            crate::models::BudgetCycleMode::Calendar => {
                let start = chrono::NaiveDate::from_ymd_opt(ref_date.year(), ref_date.month(), 1).unwrap();
                let end = if ref_date.month() == 12 {
                    chrono::NaiveDate::from_ymd_opt(ref_date.year() + 1, 1, 1).unwrap()
                } else {
                    chrono::NaiveDate::from_ymd_opt(ref_date.year(), ref_date.month() + 1, 1).unwrap()
                }.pred_opt().unwrap();
                (start, end)
            }
            crate::models::BudgetCycleMode::Payday => {
                let start_day: u32 = self.get_setting("budget_cycle_start_day")
                    .ok().flatten()
                    .and_then(|s| s.parse().ok())
                    .unwrap_or(1);
                let day = ref_date.day();
                let (start_year, start_month) = if day >= start_day {
                    (ref_date.year(), ref_date.month())
                } else if ref_date.month() == 1 {
                    (ref_date.year() - 1, 12)
                } else {
                    (ref_date.year(), ref_date.month() - 1)
                };
                let clamped_day = start_day.min(days_in_month(start_year, start_month));
                let start = chrono::NaiveDate::from_ymd_opt(start_year, start_month, clamped_day).unwrap();
                let (end_year, end_month) = if start_month == 12 {
                    (start_year + 1, 1)
                } else {
                    (start_year, start_month + 1)
                };
                let end_clamped = start_day.min(days_in_month(end_year, end_month));
                let end_start = chrono::NaiveDate::from_ymd_opt(end_year, end_month, end_clamped).unwrap();
                let end = end_start.pred_opt().unwrap_or(end_start);
                (start, end)
            }
            crate::models::BudgetCycleMode::Rolling => {
                let cycle_days: i64 = self.get_setting("budget_cycle_days")
                    .ok().flatten()
                    .and_then(|s| s.parse().ok())
                    .unwrap_or(30);
                let anchor_str = self.get_setting("budget_cycle_anchor")
                    .ok().flatten()
                    .unwrap_or_else(|| ref_date.format("%Y-%m-%d").to_string());
                let anchor = chrono::NaiveDate::parse_from_str(&anchor_str, "%Y-%m-%d")
                    .unwrap_or(ref_date);
                let diff = (ref_date - anchor).num_days();
                let periods = if diff >= 0 { diff / cycle_days } else { (diff / cycle_days) - 1 };
                let start = anchor + chrono::Duration::days(periods * cycle_days);
                let end = start + chrono::Duration::days(cycle_days - 1);
                (start, end)
            }
        }
    }

    pub fn get_next_budget_period(&self, current_start: chrono::NaiveDate) -> (chrono::NaiveDate, chrono::NaiveDate) {
        use chrono::Datelike;
        let mode = self.get_budget_cycle_mode();
        match mode {
            crate::models::BudgetCycleMode::Calendar => {
                let (ny, nm) = if current_start.month() == 12 {
                    (current_start.year() + 1, 1)
                } else {
                    (current_start.year(), current_start.month() + 1)
                };
                let next = chrono::NaiveDate::from_ymd_opt(ny, nm, 1).unwrap();
                self.get_budget_period(next)
            }
            crate::models::BudgetCycleMode::Payday => {
                let (_, end) = self.get_budget_period(current_start);
                let next = end + chrono::Duration::days(1);
                self.get_budget_period(next)
            }
            crate::models::BudgetCycleMode::Rolling => {
                let cycle_days: i64 = self.get_setting("budget_cycle_days")
                    .ok().flatten()
                    .and_then(|s| s.parse().ok())
                    .unwrap_or(30);
                let next = current_start + chrono::Duration::days(cycle_days);
                self.get_budget_period(next)
            }
        }
    }

    pub fn get_prev_budget_period(&self, current_start: chrono::NaiveDate) -> (chrono::NaiveDate, chrono::NaiveDate) {
        let prev = current_start - chrono::Duration::days(1);
        self.get_budget_period(prev)
    }

    // ---- Monthly Recap ----

    pub fn get_monthly_recap(&self, year: i32, month: u32) -> SqlResult<crate::models::MonthlyRecap> {
        use crate::models::{RecapCategory, MonthlyRecap, TransactionType};

        let total_income = self.get_monthly_total(year, month, TransactionType::Income)?;
        let total_expenses = self.get_monthly_total(year, month, TransactionType::Expense)?;
        let net = total_income - total_expenses;
        let transaction_count = self.get_transaction_count_for_month(year, month)?;

        let expense_by_cat = self.get_monthly_totals_by_category(year, month, TransactionType::Expense)?;
        let total = if total_expenses > 0.0 { total_expenses } else { 1.0 };

        // Previous month for comparison
        let (prev_year, prev_month) = if month == 1 { (year - 1, 12u32) } else { (year, month - 1) };

        let mut categories = Vec::new();
        for (cat, amount) in &expense_by_cat {
            let prev_amount = self.get_monthly_total_for_category(prev_year, prev_month, cat.id).unwrap_or(0.0);
            let change_pct = if prev_amount > 0.0 {
                Some(((amount - prev_amount) / prev_amount) * 100.0)
            } else if *amount > 0.0 {
                None // new category, no comparison
            } else {
                Some(0.0)
            };
            categories.push(RecapCategory {
                category_name: cat.name.clone(),
                category_icon: cat.icon.clone(),
                category_color: cat.color.clone(),
                amount: *amount,
                percentage: (amount / total) * 100.0,
                change_pct,
            });
        }

        Ok(MonthlyRecap {
            total_income,
            total_expenses,
            net,
            transaction_count,
            categories,
        })
    }

    pub fn get_yearly_month_summaries(&self, year: i32) -> SqlResult<Vec<(String, f64, f64)>> {
        use crate::models::TransactionType;
        let mut results = Vec::new();
        for month in 1..=12u32 {
            let income = self.get_monthly_total(year, month, TransactionType::Income).unwrap_or(0.0);
            let expense = self.get_monthly_total(year, month, TransactionType::Expense).unwrap_or(0.0);
            let label = format!("{:04}-{:02}", year, month);
            results.push((label, income, expense));
        }
        Ok(results)
    }

    // ---- Goal Projections ----

    pub fn get_goal_avg_monthly_contribution(&self, goal_id: i64) -> SqlResult<f64> {
        let goal = self.get_goal(goal_id)?;
        if goal.saved <= 0.0 {
            return Ok(0.0);
        }
        let created = chrono::NaiveDate::parse_from_str(
            goal.created_at.split(' ').next().unwrap_or("2026-01-01"),
            "%Y-%m-%d"
        ).unwrap_or_else(|_| chrono::Local::now().date_naive());
        let today = chrono::Local::now().date_naive();
        let months_elapsed = ((today.year() - created.year()) * 12
            + today.month() as i32 - created.month() as i32).max(1) as f64;
        Ok(goal.saved / months_elapsed)
    }
}

fn days_in_month(year: i32, month: u32) -> u32 {
    if month == 12 {
        chrono::NaiveDate::from_ymd_opt(year + 1, 1, 1)
    } else {
        chrono::NaiveDate::from_ymd_opt(year, month + 1, 1)
    }
    .and_then(|d| d.pred_opt())
    .map(|d| d.day())
    .unwrap_or(30)
}

/// Returns the previous month string in "YYYY-MM" format.
fn prev_month(month: &str) -> String {
    let parts: Vec<&str> = month.split('-').collect();
    if parts.len() != 2 {
        return month.to_string();
    }
    let year: i32 = parts[0].parse().unwrap_or(2026);
    let mon: u32 = parts[1].parse().unwrap_or(1);
    if mon <= 1 {
        format!("{:04}-{:02}", year - 1, 12)
    } else {
        format!("{:04}-{:02}", year, mon - 1)
    }
}

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

    #[test]
    fn test_init_creates_tables() {
        let db = Database::open_in_memory().unwrap();
        let count: i64 = db.conn.query_row(
            "SELECT COUNT(*) FROM categories",
            [],
            |row| row.get(0),
        ).unwrap();
        assert_eq!(count, 20); // 14 expense + 6 income
    }

    #[test]
    fn test_default_expense_categories() {
        let db = Database::open_in_memory().unwrap();
        let count: i64 = db.conn.query_row(
            "SELECT COUNT(*) FROM categories WHERE type = 'expense'",
            [],
            |row| row.get(0),
        ).unwrap();
        assert_eq!(count, 14);
    }

    #[test]
    fn test_default_income_categories() {
        let db = Database::open_in_memory().unwrap();
        let count: i64 = db.conn.query_row(
            "SELECT COUNT(*) FROM categories WHERE type = 'income'",
            [],
            |row| row.get(0),
        ).unwrap();
        assert_eq!(count, 6);
    }

    #[test]
    fn test_schema_version_set() {
        let db = Database::open_in_memory().unwrap();
        assert_eq!(db.schema_version().unwrap(), CURRENT_SCHEMA_VERSION);
    }

    #[test]
    fn test_all_tables_exist() {
        let db = Database::open_in_memory().unwrap();
        let tables = [
            "categories", "transactions", "budgets",
            "recurring_transactions", "exchange_rates",
            "settings", "budget_notifications", "schema_version",
            "tags", "transaction_tags", "transaction_splits",
            "transaction_templates", "categorization_rules",
            "savings_goals", "wishlist_items",
        ];
        for table in tables {
            let exists: bool = db.conn.query_row(
                "SELECT COUNT(*) > 0 FROM sqlite_master WHERE type='table' AND name=?1",
                params![table],
                |row| row.get(0),
            ).unwrap();
            assert!(exists, "Table '{}' should exist", table);
        }
    }

    #[test]
    fn test_categories_have_icons_and_colors() {
        let db = Database::open_in_memory().unwrap();
        let missing: i64 = db.conn.query_row(
            "SELECT COUNT(*) FROM categories WHERE icon IS NULL OR color IS NULL",
            [],
            |row| row.get(0),
        ).unwrap();
        assert_eq!(missing, 0, "All default categories should have icon and color");
    }

    fn make_expense(db: &Database) -> i64 {
        let cats = db.list_categories(Some(TransactionType::Expense)).unwrap();
        let cat = &cats[0];
        let txn = NewTransaction {
            amount: 12.50,
            transaction_type: TransactionType::Expense,
            category_id: cat.id,
            currency: "USD".to_string(),
            exchange_rate: 1.0,
            note: Some("Lunch".to_string()),
            date: NaiveDate::from_ymd_opt(2026, 3, 1).unwrap(),
            recurring_id: None,
            payee: None,
        };
        db.insert_transaction(&txn).unwrap()
    }

    fn make_income(db: &Database) -> i64 {
        let cats = db.list_categories(Some(TransactionType::Income)).unwrap();
        let cat = &cats[0];
        let txn = NewTransaction {
            amount: 3000.0,
            transaction_type: TransactionType::Income,
            category_id: cat.id,
            currency: "USD".to_string(),
            exchange_rate: 1.0,
            note: Some("Salary".to_string()),
            date: NaiveDate::from_ymd_opt(2026, 3, 1).unwrap(),
            recurring_id: None,
            payee: None,
        };
        db.insert_transaction(&txn).unwrap()
    }

    #[test]
    fn test_insert_and_get_transaction() {
        let db = Database::open_in_memory().unwrap();
        let id = make_expense(&db);
        let txn = db.get_transaction(id).unwrap();
        assert_eq!(txn.amount, 12.50);
        assert_eq!(txn.transaction_type, TransactionType::Expense);
        assert_eq!(txn.note.as_deref(), Some("Lunch"));
        assert_eq!(txn.date, NaiveDate::from_ymd_opt(2026, 3, 1).unwrap());
    }

    #[test]
    fn test_update_transaction() {
        let db = Database::open_in_memory().unwrap();
        let id = make_expense(&db);
        let mut txn = db.get_transaction(id).unwrap();
        txn.amount = 15.75;
        txn.note = Some("Dinner".to_string());
        db.update_transaction(&txn).unwrap();
        let updated = db.get_transaction(id).unwrap();
        assert_eq!(updated.amount, 15.75);
        assert_eq!(updated.note.as_deref(), Some("Dinner"));
    }

    #[test]
    fn test_delete_transaction() {
        let db = Database::open_in_memory().unwrap();
        let id = make_expense(&db);
        db.delete_transaction(id).unwrap();
        assert!(db.get_transaction(id).is_err());
    }

    #[test]
    fn test_list_transactions_by_month() {
        let db = Database::open_in_memory().unwrap();
        make_expense(&db);
        make_income(&db);
        let march = db.list_transactions_by_month(2026, 3).unwrap();
        assert_eq!(march.len(), 2);
        let feb = db.list_transactions_by_month(2026, 2).unwrap();
        assert_eq!(feb.len(), 0);
    }

    #[test]
    fn test_list_recent_transactions() {
        let db = Database::open_in_memory().unwrap();
        make_expense(&db);
        make_income(&db);
        let recent = db.list_recent_transactions(1).unwrap();
        assert_eq!(recent.len(), 1);
        let all = db.list_recent_transactions(10).unwrap();
        assert_eq!(all.len(), 2);
    }

    #[test]
    fn test_list_categories_by_type() {
        let db = Database::open_in_memory().unwrap();
        let expense = db.list_categories(Some(TransactionType::Expense)).unwrap();
        assert_eq!(expense.len(), 14);
        let income = db.list_categories(Some(TransactionType::Income)).unwrap();
        assert_eq!(income.len(), 6);
        let all = db.list_categories(None).unwrap();
        assert_eq!(all.len(), 20);
    }

    #[test]
    fn test_insert_custom_category() {
        let db = Database::open_in_memory().unwrap();
        let cat = NewCategory {
            name: "Pets".to_string(),
            icon: Some("\u{1f436}".to_string()),
            color: Some("#a0522d".to_string()),
            transaction_type: TransactionType::Expense,
            sort_order: 99,
            parent_id: None,
        };
        let id = db.insert_category(&cat).unwrap();
        let fetched = db.get_category(id).unwrap();
        assert_eq!(fetched.name, "Pets");
        assert!(!fetched.is_default);
    }

    #[test]
    fn test_monthly_totals_by_category() {
        let db = Database::open_in_memory().unwrap();
        make_expense(&db);
        make_expense(&db);
        let totals = db.get_monthly_totals_by_category(2026, 3, TransactionType::Expense).unwrap();
        assert_eq!(totals.len(), 1);
        assert_eq!(totals[0].1, 25.0); // 12.50 * 2
    }

    #[test]
    fn test_monthly_total() {
        let db = Database::open_in_memory().unwrap();
        make_expense(&db);
        make_income(&db);
        let expenses = db.get_monthly_total(2026, 3, TransactionType::Expense).unwrap();
        assert_eq!(expenses, 12.50);
        let income = db.get_monthly_total(2026, 3, TransactionType::Income).unwrap();
        assert_eq!(income, 3000.0);
    }

    #[test]
    fn test_daily_totals() {
        let db = Database::open_in_memory().unwrap();
        make_expense(&db);
        make_income(&db);
        let daily = db.get_daily_totals(2026, 3).unwrap();
        assert_eq!(daily.len(), 1);
        let (date, inc, exp) = &daily[0];
        assert_eq!(*date, NaiveDate::from_ymd_opt(2026, 3, 1).unwrap());
        assert_eq!(*inc, 3000.0);
        assert_eq!(*exp, 12.50);
    }

    #[test]
    fn test_settings_crud() {
        let db = Database::open_in_memory().unwrap();
        assert_eq!(db.get_setting("base_currency").unwrap(), None);
        db.set_setting("base_currency", "EUR").unwrap();
        assert_eq!(db.get_setting("base_currency").unwrap(), Some("EUR".to_string()));
        db.set_setting("base_currency", "GBP").unwrap();
        assert_eq!(db.get_setting("base_currency").unwrap(), Some("GBP".to_string()));
    }

    #[test]
    fn test_idempotent_init() {
        let db = Database::open_in_memory().unwrap();
        // Re-init should not duplicate categories
        db.init().unwrap();
        let count: i64 = db.conn.query_row(
            "SELECT COUNT(*) FROM categories",
            [],
            |row| row.get(0),
        ).unwrap();
        assert_eq!(count, 20);
    }

    #[test]
    fn test_set_and_get_budget() {
        let db = Database::open_in_memory().unwrap();
        let cats = db.list_categories(Some(TransactionType::Expense)).unwrap();
        let cat_id = cats[0].id;

        db.set_budget(cat_id, "2026-03", 500.0, false).unwrap();
        let budget = db.get_budget(cat_id, "2026-03").unwrap().unwrap();
        assert_eq!(budget.category_id, cat_id);
        assert!((budget.amount - 500.0).abs() < 0.01);
        assert_eq!(budget.month, "2026-03");
    }

    #[test]
    fn test_set_budget_updates_existing() {
        let db = Database::open_in_memory().unwrap();
        let cats = db.list_categories(Some(TransactionType::Expense)).unwrap();
        let cat_id = cats[0].id;

        db.set_budget(cat_id, "2026-03", 500.0, false).unwrap();
        db.set_budget(cat_id, "2026-03", 750.0, false).unwrap();
        let budget = db.get_budget(cat_id, "2026-03").unwrap().unwrap();
        assert!((budget.amount - 750.0).abs() < 0.01);
    }

    #[test]
    fn test_list_budgets_for_month() {
        let db = Database::open_in_memory().unwrap();
        let cats = db.list_categories(Some(TransactionType::Expense)).unwrap();

        db.set_budget(cats[0].id, "2026-03", 500.0, false).unwrap();
        db.set_budget(cats[1].id, "2026-03", 300.0, false).unwrap();
        db.set_budget(cats[0].id, "2026-04", 600.0, false).unwrap();

        let march = db.list_budgets_for_month("2026-03").unwrap();
        assert_eq!(march.len(), 2);

        let april = db.list_budgets_for_month("2026-04").unwrap();
        assert_eq!(april.len(), 1);
    }

    #[test]
    fn test_delete_budget() {
        let db = Database::open_in_memory().unwrap();
        let cats = db.list_categories(Some(TransactionType::Expense)).unwrap();
        let cat_id = cats[0].id;

        db.set_budget(cat_id, "2026-03", 500.0, false).unwrap();
        let budget = db.get_budget(cat_id, "2026-03").unwrap().unwrap();
        db.delete_budget(budget.id).unwrap();
        assert!(db.get_budget(cat_id, "2026-03").unwrap().is_none());
    }

    #[test]
    fn test_budget_progress() {
        let db = Database::open_in_memory().unwrap();
        let cats = db.list_categories(Some(TransactionType::Expense)).unwrap();
        let cat_id = cats[0].id;

        db.set_budget(cat_id, "2026-03", 500.0, false).unwrap();

        // Add some expenses
        let txn = NewTransaction {
            amount: 200.0,
            transaction_type: TransactionType::Expense,
            category_id: cat_id,
            currency: "USD".to_string(),
            exchange_rate: 1.0,
            note: None,
            date: NaiveDate::from_ymd_opt(2026, 3, 15).unwrap(),
            recurring_id: None,
            payee: None,
        };
        db.insert_transaction(&txn).unwrap();

        let (budget_amt, spent, pct) = db.get_budget_progress(cat_id, "2026-03").unwrap().unwrap();
        assert!((budget_amt - 500.0).abs() < 0.01);
        assert!((spent - 200.0).abs() < 0.01);
        assert!((pct - 40.0).abs() < 0.01);
    }

    #[test]
    fn test_budget_progress_no_budget() {
        let db = Database::open_in_memory().unwrap();
        let cats = db.list_categories(Some(TransactionType::Expense)).unwrap();
        assert!(db.get_budget_progress(cats[0].id, "2026-03").unwrap().is_none());
    }

    #[test]
    fn test_copy_budgets() {
        let db = Database::open_in_memory().unwrap();
        let cats = db.list_categories(Some(TransactionType::Expense)).unwrap();

        db.set_budget(cats[0].id, "2026-03", 500.0, false).unwrap();
        db.set_budget(cats[1].id, "2026-03", 300.0, false).unwrap();

        db.copy_budgets("2026-03", "2026-04").unwrap();

        let april = db.list_budgets_for_month("2026-04").unwrap();
        assert_eq!(april.len(), 2);
    }
}
