use adw::prelude::*;
use chrono::{Datelike, Local};
use gtk::{gdk, gio, glib};
use outlay_core::backup;
use outlay_core::db::Database;
use outlay_core::exchange::ExchangeRateService;
use outlay_core::export_csv;
use outlay_core::export_json;
use outlay_core::export_ofx;
use outlay_core::export_pdf;
use outlay_core::export_qif;
use outlay_core::import_csv;
use outlay_core::import_json;
use outlay_core::import_pdf;
use outlay_core::models::{Category, NewCategory, NewTransaction, TransactionType};
use std::cell::RefCell;
use std::rc::Rc;

use crate::icon_theme;

pub struct SettingsView {
    pub container: gtk::Box,
    on_data_reset: Rc<RefCell<Option<Box<dyn Fn()>>>>,
}

impl SettingsView {
    pub fn new(db: Rc<Database>, app: &adw::Application) -> Self {
        let container = gtk::Box::new(gtk::Orientation::Vertical, 0);
        let toast_overlay = adw::ToastOverlay::new();

        let settings_stack = gtk::Stack::new();
        settings_stack.set_transition_type(gtk::StackTransitionType::SlideLeftRight);

        // Currency group
        let currency_group = adw::PreferencesGroup::builder()
            .title("CURRENCY")
            .build();

        let base_currency = db
            .get_setting("base_currency")
            .ok()
            .flatten()
            .unwrap_or_else(|| "USD".to_string());

        let currencies = ExchangeRateService::supported_currencies();
        let currency_labels: Vec<String> = currencies
            .iter()
            .map(|(code, name)| format!("{} - {}", code, name))
            .collect();
        let currency_label_refs: Vec<&str> = currency_labels.iter().map(|s| s.as_str()).collect();
        let currency_model = gtk::StringList::new(&currency_label_refs);
        let currency_codes: Vec<String> = currencies.iter().map(|(c, _)| c.to_string()).collect();
        let base_idx = currency_codes
            .iter()
            .position(|c| c.eq_ignore_ascii_case(&base_currency))
            .unwrap_or(0);

        let currency_row = adw::ComboRow::builder()
            .title("Base Currency")
            .subtitle("Used for reports and budget calculations")
            .model(&currency_model)
            .selected(base_idx as u32)
            .build();

        {
            let db_ref = db.clone();
            let codes = currency_codes.clone();
            let toast_ref = toast_overlay.clone();
            currency_row.connect_selected_notify(move |row| {
                let idx = row.selected() as usize;
                if let Some(code) = codes.get(idx) {
                    match db_ref.set_setting("base_currency", code) {
                        Ok(()) => {
                            let toast = adw::Toast::new(&format!("Base currency set to {}", code));
                            toast_ref.add_toast(toast);
                        }
                        Err(e) => {
                            let toast = adw::Toast::new(&format!("Error: {}", e));
                            toast_ref.add_toast(toast);
                        }
                    }
                }
            });
        }

        currency_group.add(&currency_row);

        // Secondary display currency
        let mut secondary_labels: Vec<String> = vec!["None".to_string()];
        secondary_labels.extend(currency_labels.iter().cloned());
        let secondary_label_refs: Vec<&str> = secondary_labels.iter().map(|s| s.as_str()).collect();
        let secondary_model = gtk::StringList::new(&secondary_label_refs);

        let secondary_currency = db
            .get_setting("secondary_currency")
            .ok()
            .flatten()
            .unwrap_or_default();
        let secondary_idx = if secondary_currency.is_empty() {
            0
        } else {
            currency_codes
                .iter()
                .position(|c| c.eq_ignore_ascii_case(&secondary_currency))
                .map(|i| i + 1)
                .unwrap_or(0)
        };

        let secondary_row = adw::ComboRow::builder()
            .title("Secondary Display Currency")
            .subtitle("Show totals in a second currency alongside your base")
            .model(&secondary_model)
            .selected(secondary_idx as u32)
            .build();

        {
            let db_ref = db.clone();
            let codes = currency_codes.clone();
            let toast_ref = toast_overlay.clone();
            secondary_row.connect_selected_notify(move |row| {
                let idx = row.selected() as usize;
                let value = if idx == 0 { "" } else { codes.get(idx - 1).map(|s| s.as_str()).unwrap_or("") };
                match db_ref.set_setting("secondary_currency", value) {
                    Ok(()) => {
                        if value.is_empty() {
                            toast_ref.add_toast(adw::Toast::new("Secondary currency cleared"));
                        } else {
                            toast_ref.add_toast(adw::Toast::new(&format!("Secondary currency set to {}", value)));
                        }
                    }
                    Err(e) => toast_ref.add_toast(adw::Toast::new(&format!("Error: {}", e))),
                }
            });
        }

        currency_group.add(&secondary_row);

        // Appearance group
        let appearance_group = adw::PreferencesGroup::builder()
            .title("APPEARANCE")
            .build();

        let theme_labels = ["System", "Light", "Dark"];
        let theme_model = gtk::StringList::new(&theme_labels);

        let current_theme = db
            .get_setting("theme")
            .ok()
            .flatten()
            .unwrap_or_else(|| "system".to_string());
        let theme_idx = match current_theme.as_str() {
            "light" => 1,
            "dark" => 2,
            _ => 0,
        };

        let theme_row = adw::ComboRow::builder()
            .title("Theme")
            .model(&theme_model)
            .selected(theme_idx)
            .build();

        {
            let db_ref = db.clone();
            theme_row.connect_selected_notify(move |row| {
                let theme = match row.selected() {
                    1 => "light",
                    2 => "dark",
                    _ => "system",
                };
                db_ref.set_setting("theme", theme).ok();
                Self::apply_theme(theme);
            });
        }

        // Date format
        let today = chrono::Local::now().date_naive();
        let date_format_options = [
            format!("{} (YYYY-MM-DD)", today.format("%Y-%m-%d")),
            format!("{} (DD/MM/YYYY)", today.format("%d/%m/%Y")),
            format!("{} (MM/DD/YYYY)", today.format("%m/%d/%Y")),
        ];
        let date_format_refs: Vec<&str> = date_format_options.iter().map(|s| s.as_str()).collect();
        let date_format_model = gtk::StringList::new(&date_format_refs);
        let date_format_row = adw::ComboRow::builder()
            .title("Date Format")
            .model(&date_format_model)
            .build();
        // Prevent truncation of the date format display
        let date_factory = gtk::SignalListItemFactory::new();
        date_factory.connect_setup(|_, item| {
            let item = item.downcast_ref::<gtk::ListItem>().unwrap();
            let label = gtk::Label::new(None);
            label.set_halign(gtk::Align::Start);
            label.set_ellipsize(gtk::pango::EllipsizeMode::None);
            item.set_child(Some(&label));
        });
        date_factory.connect_bind(|_, item| {
            let item = item.downcast_ref::<gtk::ListItem>().unwrap();
            let string_obj = item.item().and_downcast::<gtk::StringObject>().unwrap();
            let label = item.child().and_downcast::<gtk::Label>().unwrap();
            label.set_label(&string_obj.string());
        });
        date_format_row.set_factory(Some(&date_factory));
        date_format_row.set_list_factory(Some(&date_factory));

        let current_date_fmt = db.get_setting("date_format")
            .ok().flatten().unwrap_or_else(|| "ymd".to_string());
        let date_fmt_idx = match current_date_fmt.as_str() {
            "dmy" => 1,
            "mdy" => 2,
            _ => 0,
        };
        date_format_row.set_selected(date_fmt_idx);

        {
            let db_ref = db.clone();
            let toast_ref = toast_overlay.clone();
            date_format_row.connect_selected_notify(move |row| {
                let val = match row.selected() {
                    1 => "dmy",
                    2 => "mdy",
                    _ => "ymd",
                };
                if let Ok(()) = db_ref.set_setting("date_format", val) {
                    let toast = adw::Toast::new("Date format updated - reopen views to see changes");
                    toast_ref.add_toast(toast);
                }
            });
        }

        appearance_group.add(&theme_row);
        appearance_group.add(&date_format_row);

        // Apply current theme on load
        Self::apply_theme(&current_theme);

        // Categories group
        let categories_group = adw::PreferencesGroup::builder()
            .title("CATEGORIES")
            .build();

        let expense_expander = adw::ExpanderRow::builder()
            .title("Expense Categories")
            .build();
        Self::populate_category_expander(
            &db, &expense_expander, TransactionType::Expense, &toast_overlay,
        );
        categories_group.add(&expense_expander);

        let income_expander = adw::ExpanderRow::builder()
            .title("Income Categories")
            .build();
        Self::populate_category_expander(
            &db, &income_expander, TransactionType::Income, &toast_overlay,
        );
        categories_group.add(&income_expander);

        let add_cat_btn = gtk::Button::with_label("Add Category");
        add_cat_btn.add_css_class("pill");
        add_cat_btn.set_halign(gtk::Align::Center);
        add_cat_btn.set_margin_top(8);

        {
            let db_ref = db.clone();
            let expense_ref = expense_expander.clone();
            let income_ref = income_expander.clone();
            let toast_ref = toast_overlay.clone();
            add_cat_btn.connect_clicked(move |btn| {
                Self::show_add_category_dialog(
                    btn, &db_ref, &expense_ref, &income_ref, &toast_ref,
                );
            });
        }

        // Export group
        let export_group = adw::PreferencesGroup::builder()
            .title("EXPORT")
            .build();

        let export_csv_row = adw::ActionRow::builder()
            .title("Export CSV")
            .subtitle("Export all transactions as CSV file")
            .activatable(true)
            .build();
        export_csv_row.add_suffix(&gtk::Image::from_icon_name("outlay-export"));

        let export_json_row = adw::ActionRow::builder()
            .title("Export JSON")
            .subtitle("Export all data as JSON file")
            .activatable(true)
            .build();
        export_json_row.add_suffix(&gtk::Image::from_icon_name("outlay-export"));

        let export_qif_row = adw::ActionRow::builder()
            .title("Export QIF")
            .subtitle("Quicken interchange format")
            .activatable(true)
            .build();
        export_qif_row.add_suffix(&gtk::Image::from_icon_name("outlay-export"));

        let export_ofx_row = adw::ActionRow::builder()
            .title("Export OFX")
            .subtitle("Open financial exchange format")
            .activatable(true)
            .build();
        export_ofx_row.add_suffix(&gtk::Image::from_icon_name("outlay-export"));

        let export_pdf_row = adw::ActionRow::builder()
            .title("Export PDF Report")
            .subtitle("Generate a monthly PDF report")
            .activatable(true)
            .build();
        export_pdf_row.add_suffix(&gtk::Image::from_icon_name("outlay-export"));

        export_group.add(&export_csv_row);
        export_group.add(&export_json_row);
        export_group.add(&export_qif_row);
        export_group.add(&export_ofx_row);
        export_group.add(&export_pdf_row);

        // Import group
        let import_group = adw::PreferencesGroup::builder()
            .title("IMPORT")
            .build();

        let import_csv_row = adw::ActionRow::builder()
            .title("Import CSV")
            .subtitle("Import transactions from a CSV file")
            .activatable(true)
            .build();
        import_csv_row.add_suffix(&gtk::Image::from_icon_name("outlay-import"));

        let import_json_row = adw::ActionRow::builder()
            .title("Import JSON")
            .subtitle("Import data from a JSON export")
            .activatable(true)
            .build();
        import_json_row.add_suffix(&gtk::Image::from_icon_name("outlay-import"));

        let import_qif_row = adw::ActionRow::builder()
            .title("Import QIF")
            .subtitle("Import from Quicken interchange format")
            .activatable(true)
            .build();
        import_qif_row.add_suffix(&gtk::Image::from_icon_name("outlay-import"));

        let import_ofx_row = adw::ActionRow::builder()
            .title("Import OFX")
            .subtitle("Import from open financial exchange format")
            .activatable(true)
            .build();
        import_ofx_row.add_suffix(&gtk::Image::from_icon_name("outlay-import"));

        let import_pdf_row = adw::ActionRow::builder()
            .title("Import PDF Statement")
            .subtitle("Parse transactions from a bank/credit card PDF statement")
            .activatable(true)
            .build();
        import_pdf_row.add_suffix(&gtk::Image::from_icon_name("outlay-import"));

        import_group.add(&import_csv_row);
        import_group.add(&import_json_row);
        import_group.add(&import_qif_row);
        import_group.add(&import_ofx_row);
        import_group.add(&import_pdf_row);

        // Backup group
        let backup_group = adw::PreferencesGroup::builder()
            .title("BACKUP")
            .build();

        let backup_row = adw::ActionRow::builder()
            .title("Full Backup")
            .subtitle("Create a backup of all data")
            .activatable(true)
            .build();
        backup_row.add_suffix(&gtk::Image::from_icon_name("outlay-export"));

        let restore_row = adw::ActionRow::builder()
            .title("Restore from Backup")
            .subtitle("Restore data from a .outlay backup file")
            .activatable(true)
            .build();
        restore_row.add_suffix(&gtk::Image::from_icon_name("outlay-import"));

        backup_group.add(&backup_row);
        backup_group.add(&restore_row);

        // Wire export buttons
        {
            let db_ref = db.clone();
            let toast_ref = toast_overlay.clone();
            export_csv_row.connect_activated(move |row| {
                Self::export_csv_action(row, &db_ref, &toast_ref);
            });
        }
        {
            let db_ref = db.clone();
            let toast_ref = toast_overlay.clone();
            export_json_row.connect_activated(move |row| {
                Self::export_json_action(row, &db_ref, &toast_ref);
            });
        }
        {
            let db_ref = db.clone();
            let toast_ref = toast_overlay.clone();
            export_qif_row.connect_activated(move |row| {
                Self::export_qif_action(row, &db_ref, &toast_ref);
            });
        }
        {
            let db_ref = db.clone();
            let toast_ref = toast_overlay.clone();
            export_ofx_row.connect_activated(move |row| {
                Self::export_ofx_action(row, &db_ref, &toast_ref);
            });
        }
        {
            let db_ref = db.clone();
            let toast_ref = toast_overlay.clone();
            export_pdf_row.connect_activated(move |row| {
                Self::export_pdf_action(row, &db_ref, &toast_ref);
            });
        }
        {
            let db_ref = db.clone();
            let toast_ref = toast_overlay.clone();
            backup_row.connect_activated(move |row| {
                Self::backup_action(row, &db_ref, &toast_ref);
            });
        }
        {
            let toast_ref = toast_overlay.clone();
            let app_ref = app.clone();
            restore_row.connect_activated(move |row| {
                Self::restore_action(row, &toast_ref, &app_ref);
            });
        }
        {
            let db_ref = db.clone();
            let toast_ref = toast_overlay.clone();
            import_csv_row.connect_activated(move |row| {
                Self::import_csv_action(row, &db_ref, &toast_ref);
            });
        }
        {
            let db_ref = db.clone();
            let toast_ref = toast_overlay.clone();
            import_json_row.connect_activated(move |row| {
                Self::import_json_action(row, &db_ref, &toast_ref);
            });
        }
        {
            let db_ref = db.clone();
            let toast_ref = toast_overlay.clone();
            import_qif_row.connect_activated(move |row| {
                Self::import_qif_action(row, &db_ref, &toast_ref);
            });
        }
        {
            let db_ref = db.clone();
            let toast_ref = toast_overlay.clone();
            import_ofx_row.connect_activated(move |row| {
                Self::import_ofx_action(row, &db_ref, &toast_ref);
            });
        }
        {
            let db_ref = db.clone();
            let toast_ref = toast_overlay.clone();
            import_pdf_row.connect_activated(move |row| {
                Self::import_pdf_action(row, &db_ref, &toast_ref);
            });
        }

        // Automatic backup group
        let auto_backup_group = adw::PreferencesGroup::builder()
            .title("AUTOMATIC BACKUPS")
            .description("Automatically back up your database on a schedule")
            .build();

        let auto_backup_enabled = db.get_setting("auto_backup_enabled")
            .ok().flatten().unwrap_or_else(|| "0".to_string()) == "1";

        let auto_backup_toggle = adw::SwitchRow::builder()
            .title("Enable Automatic Backups")
            .active(auto_backup_enabled)
            .build();

        let freq_model = gtk::StringList::new(&["Daily", "Weekly", "Monthly"]);
        let auto_backup_freq = adw::ComboRow::builder()
            .title("Backup Frequency")
            .model(&freq_model)
            .sensitive(auto_backup_enabled)
            .build();

        let current_freq = db.get_setting("auto_backup_frequency")
            .ok().flatten().unwrap_or_else(|| "weekly".to_string());
        let freq_idx = match current_freq.as_str() {
            "daily" => 0,
            "monthly" => 2,
            _ => 1,
        };
        auto_backup_freq.set_selected(freq_idx);

        let retention_row = adw::SpinRow::builder()
            .title("Keep Last N Backups")
            .subtitle("Older backups will be deleted automatically")
            .sensitive(auto_backup_enabled)
            .build();
        let retention_adj = gtk::Adjustment::new(
            db.get_setting("auto_backup_retention")
                .ok().flatten()
                .and_then(|v| v.parse::<f64>().ok())
                .unwrap_or(5.0),
            1.0, 50.0, 1.0, 5.0, 0.0,
        );
        retention_row.set_adjustment(Some(&retention_adj));

        let auto_backup_dir = db.get_setting("auto_backup_dir")
            .ok().flatten()
            .unwrap_or_default();
        let dir_subtitle = if auto_backup_dir.is_empty() {
            "Default: ~/.local/share/outlay/backups".to_string()
        } else {
            auto_backup_dir.clone()
        };
        let dir_row = adw::ActionRow::builder()
            .title("Backup Directory")
            .subtitle(&dir_subtitle)
            .activatable(true)
            .build();
        dir_row.add_suffix(&gtk::Image::from_icon_name("folder-symbolic"));

        let last_auto_backup = db.get_setting("auto_backup_last")
            .ok().flatten().unwrap_or_else(|| "Never".to_string());
        let last_backup_row = adw::ActionRow::builder()
            .title("Last Automatic Backup")
            .subtitle(&last_auto_backup)
            .build();

        let backup_now_btn = gtk::Button::with_label("Back Up Now");
        backup_now_btn.add_css_class("pill");
        backup_now_btn.set_halign(gtk::Align::Center);
        backup_now_btn.set_margin_top(8);

        auto_backup_group.add(&auto_backup_toggle);
        auto_backup_group.add(&auto_backup_freq);
        auto_backup_group.add(&retention_row);
        auto_backup_group.add(&dir_row);
        auto_backup_group.add(&last_backup_row);

        // Auto-backup toggle handler
        {
            let db_ref = db.clone();
            let freq_ref = auto_backup_freq.clone();
            let retention_ref = retention_row.clone();
            auto_backup_toggle.connect_active_notify(move |row| {
                let val = if row.is_active() { "1" } else { "0" };
                let _ = db_ref.set_setting("auto_backup_enabled", val);
                freq_ref.set_sensitive(row.is_active());
                retention_ref.set_sensitive(row.is_active());
            });
        }

        // Frequency change handler
        {
            let db_ref = db.clone();
            auto_backup_freq.connect_selected_notify(move |row| {
                let val = match row.selected() {
                    0 => "daily",
                    2 => "monthly",
                    _ => "weekly",
                };
                let _ = db_ref.set_setting("auto_backup_frequency", val);
            });
        }

        // Retention change handler
        {
            let db_ref = db.clone();
            retention_adj.connect_value_changed(move |adj| {
                let val = adj.value() as i32;
                let _ = db_ref.set_setting("auto_backup_retention", &val.to_string());
            });
        }

        // Directory chooser handler
        {
            let db_ref = db.clone();
            let dir_row_ref = dir_row.clone();
            let toast_ref = toast_overlay.clone();
            dir_row.connect_activated(move |row| {
                let dialog = gtk::FileDialog::builder()
                    .title("Choose Backup Directory")
                    .build();
                let window = row.root().and_then(|r| r.downcast::<gtk::Window>().ok());
                let db_save = db_ref.clone();
                let row_save = dir_row_ref.clone();
                let toast_save = toast_ref.clone();
                dialog.select_folder(window.as_ref(), gio::Cancellable::NONE, move |result| {
                    if let Ok(file) = result {
                        if let Some(path) = file.path() {
                            let path_str = path.to_string_lossy().to_string();
                            let _ = db_save.set_setting("auto_backup_dir", &path_str);
                            row_save.set_subtitle(&path_str);
                            let toast = adw::Toast::new(&format!("Backup directory set to {}", path_str));
                            toast_save.add_toast(toast);
                        }
                    }
                });
            });
        }

        // Back up now handler
        {
            let db_ref = db.clone();
            let toast_ref = toast_overlay.clone();
            let last_row_ref = last_backup_row.clone();
            backup_now_btn.connect_clicked(move |_| {
                let backup_dir = db_ref.get_setting("auto_backup_dir")
                    .ok().flatten()
                    .unwrap_or_default();
                let dir = if backup_dir.is_empty() {
                    glib::user_data_dir().join("outlay").join("backups")
                } else {
                    std::path::PathBuf::from(&backup_dir)
                };
                match Self::run_auto_backup(&db_ref, &dir) {
                    Ok(path) => {
                        let now_str = Local::now().format("%Y-%m-%d %H:%M").to_string();
                        last_row_ref.set_subtitle(&now_str);
                        let toast = adw::Toast::new(&format!("Backup saved to {}", path.display()));
                        toast_ref.add_toast(toast);
                    }
                    Err(e) => {
                        let toast = adw::Toast::new(&format!("Backup error: {}", e));
                        toast_ref.add_toast(toast);
                    }
                }
            });
        }

        // Reset group
        let reset_group = adw::PreferencesGroup::builder()
            .title("DANGER ZONE")
            .build();

        let reset_row = adw::ActionRow::builder()
            .title("Reset All Data")
            .subtitle("Delete all transactions, budgets, and settings")
            .activatable(true)
            .build();
        reset_row.add_css_class("error");

        let on_data_reset: Rc<RefCell<Option<Box<dyn Fn()>>>> = Rc::new(RefCell::new(None));
        {
            let db_ref = db.clone();
            let toast_ref = toast_overlay.clone();
            let reset_cb = on_data_reset.clone();
            reset_row.connect_activated(move |row| {
                Self::reset_action(row, &db_ref, &toast_ref, &reset_cb);
            });
        }

        reset_group.add(&reset_row);

        // -- Auto-categorization rules --
        let rules_group = adw::PreferencesGroup::builder()
            .title("AUTO-CATEGORIZATION RULES")
            .description("Automatically assign categories based on note or payee text")
            .build();

        Self::populate_rules(&db, &rules_group, &toast_overlay);

        let add_rule_btn = gtk::Button::with_label("Add Rule");
        add_rule_btn.add_css_class("pill");
        add_rule_btn.set_halign(gtk::Align::Center);
        add_rule_btn.set_margin_top(8);
        {
            let db_ref = db.clone();
            let group_ref = rules_group.clone();
            let toast_ref = toast_overlay.clone();
            add_rule_btn.connect_clicked(move |btn| {
                Self::show_add_rule_dialog(btn, &db_ref, &group_ref, &toast_ref);
            });
        }

        // -- Transaction templates --
        let templates_group = adw::PreferencesGroup::builder()
            .title("TRANSACTION TEMPLATES")
            .description("Save frequent transactions for quick entry")
            .build();

        Self::populate_templates(&db, &templates_group, &toast_overlay);

        let add_template_btn = gtk::Button::with_label("Add Template");
        add_template_btn.add_css_class("pill");
        add_template_btn.set_halign(gtk::Align::Center);
        add_template_btn.set_margin_top(8);
        {
            let db_ref = db.clone();
            let group_ref = templates_group.clone();
            let toast_ref = toast_overlay.clone();
            add_template_btn.connect_clicked(move |btn| {
                Self::show_add_template_dialog(btn, &db_ref, &group_ref, &toast_ref);
            });
        }

        // -- Budget settings --
        let budget_group = adw::PreferencesGroup::builder()
            .title("BUDGET SETTINGS")
            .description("Configure budget periods and notifications")
            .build();

        let period_model = gtk::StringList::new(&["Monthly", "Weekly", "Biweekly"]);
        let period_row = adw::ComboRow::builder()
            .title("Budget Period")
            .model(&period_model)
            .build();

        let current_period = db.get_setting("budget_period")
            .ok().flatten().unwrap_or_else(|| "monthly".to_string());
        let period_idx = match current_period.as_str() {
            "weekly" => 1,
            "biweekly" => 2,
            _ => 0,
        };
        period_row.set_selected(period_idx);

        let week_start_model = gtk::StringList::new(&[
            "Monday", "Tuesday", "Wednesday", "Thursday", "Friday", "Saturday", "Sunday",
        ]);
        let week_start_row = adw::ComboRow::builder()
            .title("Week Starts On")
            .model(&week_start_model)
            .build();

        let current_week_start = db.get_setting("week_start_day")
            .ok().flatten().unwrap_or_else(|| "monday".to_string());
        let week_idx = match current_week_start.as_str() {
            "tuesday" => 1,
            "wednesday" => 2,
            "thursday" => 3,
            "friday" => 4,
            "saturday" => 5,
            "sunday" => 6,
            _ => 0,
        };
        week_start_row.set_selected(week_idx);
        week_start_row.set_visible(current_period == "weekly" || current_period == "biweekly");

        let notifications_row = adw::SwitchRow::builder()
            .title("Budget Notifications")
            .subtitle("Alert when approaching or exceeding budget limits")
            .build();
        let notif_enabled = db.get_setting("budget_notifications")
            .ok().flatten().map(|s| s == "1").unwrap_or(false);
        notifications_row.set_active(notif_enabled);

        let recurring_notif_row = adw::SwitchRow::builder()
            .title("Recurring Transaction Notifications")
            .subtitle("Notify when recurring transactions are auto-generated")
            .build();
        let recurring_notif_enabled = db.get_setting("notify_recurring")
            .ok().flatten().map(|s| s == "1").unwrap_or(true);
        recurring_notif_row.set_active(recurring_notif_enabled);

        budget_group.add(&period_row);
        budget_group.add(&week_start_row);
        budget_group.add(&notifications_row);
        budget_group.add(&recurring_notif_row);

        // Budget period change handler
        {
            let db_ref = db.clone();
            let week_row = week_start_row.clone();
            period_row.connect_selected_notify(move |row| {
                let val = match row.selected() {
                    1 => "weekly",
                    2 => "biweekly",
                    _ => "monthly",
                };
                let _ = db_ref.set_setting("budget_period", val);
                week_row.set_visible(val == "weekly" || val == "biweekly");
            });
        }

        // Week start change handler
        {
            let db_ref = db.clone();
            week_start_row.connect_selected_notify(move |row| {
                let val = match row.selected() {
                    1 => "tuesday",
                    2 => "wednesday",
                    3 => "thursday",
                    4 => "friday",
                    5 => "saturday",
                    6 => "sunday",
                    _ => "monday",
                };
                let _ = db_ref.set_setting("week_start_day", val);
            });
        }

        // Notifications toggle handler
        {
            let db_ref = db.clone();
            notifications_row.connect_active_notify(move |row| {
                let val = if row.is_active() { "1" } else { "0" };
                let _ = db_ref.set_setting("budget_notifications", val);
            });
        }
        {
            let db_ref = db.clone();
            recurring_notif_row.connect_active_notify(move |row| {
                let val = if row.is_active() { "1" } else { "0" };
                let _ = db_ref.set_setting("notify_recurring", val);
            });
        }

        // Subscription categories group
        let sub_cat_group = adw::PreferencesGroup::builder()
            .title("SUBSCRIPTION CATEGORIES")
            .build();

        let sub_cat_expander = adw::ExpanderRow::builder()
            .title("Subscription Categories")
            .build();
        Self::populate_subscription_categories(&db, &sub_cat_expander, &toast_overlay);
        sub_cat_group.add(&sub_cat_expander);

        let add_sub_cat_btn = gtk::Button::with_label("Add Subscription Category");
        add_sub_cat_btn.add_css_class("pill");
        add_sub_cat_btn.set_halign(gtk::Align::Center);
        add_sub_cat_btn.set_margin_top(8);
        {
            let db_ref = db.clone();
            let expander_ref = sub_cat_expander.clone();
            let toast_ref = toast_overlay.clone();
            add_sub_cat_btn.connect_clicked(move |btn| {
                Self::show_add_subscription_category_dialog(
                    btn, &db_ref, &expander_ref, &toast_ref,
                );
            });
        }

        // Back button helper
        let make_back = |stack: &gtk::Stack| -> gtk::Button {
            let btn = gtk::Button::new();
            btn.add_css_class("flat");
            let content = gtk::Box::new(gtk::Orientation::Horizontal, 6);
            content.append(&gtk::Image::from_icon_name("go-previous-symbolic"));
            content.append(&gtk::Label::new(Some("Settings")));
            btn.set_child(Some(&content));
            btn.set_halign(gtk::Align::Start);
            let s = stack.clone();
            btn.connect_clicked(move |_| {
                s.set_visible_child_name("root");
            });
            btn
        };

        // ===== Root page =====
        let root_inner = gtk::Box::new(gtk::Orientation::Vertical, 20);
        root_inner.set_margin_top(20);
        root_inner.set_margin_bottom(20);

        let root_group = adw::PreferencesGroup::new();

        let nav_general = adw::ActionRow::builder()
            .title("General")
            .subtitle("Currency, appearance, budget settings")
            .activatable(true)
            .build();
        nav_general.add_prefix(&gtk::Image::from_icon_name("emblem-system-symbolic"));
        nav_general.add_suffix(&gtk::Image::from_icon_name("go-next-symbolic"));

        let nav_categories = adw::ActionRow::builder()
            .title("Categories")
            .subtitle("Manage categories, rules, and templates")
            .activatable(true)
            .build();
        nav_categories.add_prefix(&gtk::Image::from_icon_name("view-list-symbolic"));
        nav_categories.add_suffix(&gtk::Image::from_icon_name("go-next-symbolic"));

        let nav_import_export = adw::ActionRow::builder()
            .title("Import / Export")
            .subtitle("Export data or import from files")
            .activatable(true)
            .build();
        nav_import_export.add_prefix(&gtk::Image::from_icon_name("document-send-symbolic"));
        nav_import_export.add_suffix(&gtk::Image::from_icon_name("go-next-symbolic"));

        let nav_backup = adw::ActionRow::builder()
            .title("Backup")
            .subtitle("Back up, restore, and manage data")
            .activatable(true)
            .build();
        nav_backup.add_prefix(&gtk::Image::from_icon_name("drive-harddisk-symbolic"));
        nav_backup.add_suffix(&gtk::Image::from_icon_name("go-next-symbolic"));

        root_group.add(&nav_general);
        root_group.add(&nav_categories);
        root_group.add(&nav_import_export);
        root_group.add(&nav_backup);

        root_inner.append(&root_group);

        let root_clamp = adw::Clamp::new();
        root_clamp.set_maximum_size(700);
        root_clamp.set_margin_start(16);
        root_clamp.set_margin_end(16);
        root_clamp.set_child(Some(&root_inner));

        let root_scroll = gtk::ScrolledWindow::builder()
            .hscrollbar_policy(gtk::PolicyType::Never)
            .vexpand(true)
            .child(&root_clamp)
            .build();
        settings_stack.add_named(&root_scroll, Some("root"));

        // ===== General sub-page =====
        let general_inner = gtk::Box::new(gtk::Orientation::Vertical, 20);
        general_inner.set_margin_top(20);
        general_inner.set_margin_bottom(20);
        general_inner.prepend(&make_back(&settings_stack));
        general_inner.append(&currency_group);
        general_inner.append(&appearance_group);
        general_inner.append(&budget_group);

        let general_clamp = adw::Clamp::new();
        general_clamp.set_maximum_size(700);
        general_clamp.set_margin_start(16);
        general_clamp.set_margin_end(16);
        general_clamp.set_child(Some(&general_inner));

        let general_scroll = gtk::ScrolledWindow::builder()
            .hscrollbar_policy(gtk::PolicyType::Never)
            .vexpand(true)
            .child(&general_clamp)
            .build();
        settings_stack.add_named(&general_scroll, Some("general"));

        // ===== Categories sub-page =====
        let categories_inner = gtk::Box::new(gtk::Orientation::Vertical, 20);
        categories_inner.set_margin_top(20);
        categories_inner.set_margin_bottom(20);
        categories_inner.prepend(&make_back(&settings_stack));
        categories_inner.append(&categories_group);
        categories_inner.append(&add_cat_btn);
        categories_inner.append(&sub_cat_group);
        categories_inner.append(&add_sub_cat_btn);
        categories_inner.append(&rules_group);
        categories_inner.append(&add_rule_btn);
        categories_inner.append(&templates_group);
        categories_inner.append(&add_template_btn);

        let categories_clamp = adw::Clamp::new();
        categories_clamp.set_maximum_size(700);
        categories_clamp.set_margin_start(16);
        categories_clamp.set_margin_end(16);
        categories_clamp.set_child(Some(&categories_inner));

        let categories_scroll = gtk::ScrolledWindow::builder()
            .hscrollbar_policy(gtk::PolicyType::Never)
            .vexpand(true)
            .child(&categories_clamp)
            .build();
        settings_stack.add_named(&categories_scroll, Some("categories"));

        // ===== Import / Export sub-page =====
        let ie_inner = gtk::Box::new(gtk::Orientation::Vertical, 20);
        ie_inner.set_margin_top(20);
        ie_inner.set_margin_bottom(20);
        ie_inner.prepend(&make_back(&settings_stack));
        ie_inner.append(&export_group);
        ie_inner.append(&import_group);

        let ie_clamp = adw::Clamp::new();
        ie_clamp.set_maximum_size(700);
        ie_clamp.set_margin_start(16);
        ie_clamp.set_margin_end(16);
        ie_clamp.set_child(Some(&ie_inner));

        let ie_scroll = gtk::ScrolledWindow::builder()
            .hscrollbar_policy(gtk::PolicyType::Never)
            .vexpand(true)
            .child(&ie_clamp)
            .build();
        settings_stack.add_named(&ie_scroll, Some("import_export"));

        // ===== Backup sub-page =====
        let backup_inner = gtk::Box::new(gtk::Orientation::Vertical, 20);
        backup_inner.set_margin_top(20);
        backup_inner.set_margin_bottom(20);
        backup_inner.prepend(&make_back(&settings_stack));
        backup_inner.append(&backup_group);
        backup_inner.append(&auto_backup_group);
        backup_inner.append(&backup_now_btn);
        backup_inner.append(&reset_group);

        let backup_clamp = adw::Clamp::new();
        backup_clamp.set_maximum_size(700);
        backup_clamp.set_margin_start(16);
        backup_clamp.set_margin_end(16);
        backup_clamp.set_child(Some(&backup_inner));

        let backup_scroll = gtk::ScrolledWindow::builder()
            .hscrollbar_policy(gtk::PolicyType::Never)
            .vexpand(true)
            .child(&backup_clamp)
            .build();
        settings_stack.add_named(&backup_scroll, Some("backup"));

        // ===== Wire navigation rows =====
        {
            let s = settings_stack.clone();
            nav_general.connect_activated(move |_| {
                s.set_visible_child_name("general");
            });
        }
        {
            let s = settings_stack.clone();
            nav_categories.connect_activated(move |_| {
                s.set_visible_child_name("categories");
            });
        }
        {
            let s = settings_stack.clone();
            nav_import_export.connect_activated(move |_| {
                s.set_visible_child_name("import_export");
            });
        }
        {
            let s = settings_stack.clone();
            nav_backup.connect_activated(move |_| {
                s.set_visible_child_name("backup");
            });
        }

        toast_overlay.set_child(Some(&settings_stack));
        container.append(&toast_overlay);

        SettingsView { container, on_data_reset }
    }

    pub fn set_on_data_reset<F: Fn() + 'static>(&self, cb: F) {
        *self.on_data_reset.borrow_mut() = Some(Box::new(cb));
    }

    fn apply_theme(theme: &str) {
        let style_manager = adw::StyleManager::default();
        match theme {
            "light" => style_manager.set_color_scheme(adw::ColorScheme::ForceLight),
            "dark" => style_manager.set_color_scheme(adw::ColorScheme::ForceDark),
            _ => style_manager.set_color_scheme(adw::ColorScheme::Default),
        }
    }

    fn populate_category_expander(
        db: &Rc<Database>,
        expander: &adw::ExpanderRow,
        txn_type: TransactionType,
        toast_overlay: &adw::ToastOverlay,
    ) {
        // Remove existing category rows by walking the widget tree
        // Only remove rows we tagged with "cat-row-*" to avoid touching internal widgets
        fn collect_tagged_rows(widget: &gtk::Widget, rows: &mut Vec<adw::ActionRow>) {
            let mut child = widget.first_child();
            while let Some(c) = child {
                if c.widget_name().starts_with("cat-row-") {
                    if let Some(ar) = c.downcast_ref::<adw::ActionRow>() {
                        rows.push(ar.clone());
                    }
                }
                collect_tagged_rows(&c, rows);
                child = c.next_sibling();
            }
        }
        let mut to_remove = Vec::new();
        collect_tagged_rows(expander.upcast_ref(), &mut to_remove);
        for row in &to_remove {
            expander.remove(row);
        }

        let cats = db.list_categories(Some(txn_type)).unwrap_or_default();
        for cat in &cats {
            let row = adw::ActionRow::builder()
                .title(&cat.name)
                .activatable(true)
                .build();
            row.set_widget_name(&format!("cat-row-{}", cat.id));

            // Drag handle (leftmost prefix)
            let handle = gtk::Image::from_icon_name("list-drag-handle-symbolic");
            handle.set_pixel_size(16);
            handle.set_opacity(0.75);
            row.add_prefix(&handle);

            let icon_name = icon_theme::resolve_category_icon(&cat.icon, &cat.color);
            if let Some(name) = &icon_name {
                let icon = gtk::Image::from_icon_name(name);
                icon.set_pixel_size(24);
                row.add_prefix(&icon);
            }

            // Edit on click
            {
                let cat_clone = cat.clone();
                let db_ref = db.clone();
                let expander_ref = expander.clone();
                let toast_ref = toast_overlay.clone();
                row.connect_activated(move |row| {
                    Self::show_edit_category_dialog(
                        row, &cat_clone, &db_ref, &expander_ref, txn_type, &toast_ref,
                    );
                });
            }

            // Drag-to-reorder
            {
                let cat_id = cat.id;

                let drag_source = gtk::DragSource::new();
                drag_source.set_actions(gdk::DragAction::MOVE);
                drag_source.connect_prepare(move |_, _, _| {
                    Some(gdk::ContentProvider::for_value(&cat_id.to_value()))
                });
                let row_ref = row.clone();
                drag_source.connect_drag_begin(move |src, _| {
                    let paintable = gtk::WidgetPaintable::new(Some(&row_ref));
                    src.set_icon(Some(&paintable), 0, 0);
                });
                handle.add_controller(drag_source);

                let drop_target = gtk::DropTarget::new(i64::static_type(), gdk::DragAction::MOVE);
                let db_ref = db.clone();
                let expander_ref = expander.clone();
                let toast_ref = toast_overlay.clone();
                let target_cat_id = cat.id;
                drop_target.connect_drop(move |_, value, _, _| {
                    if let Ok(source_id) = value.get::<i64>() {
                        if source_id != target_cat_id {
                            let mut cats = db_ref.list_categories(Some(txn_type)).unwrap_or_default();
                            let src_idx = cats.iter().position(|c| c.id == source_id);
                            let tgt_idx = cats.iter().position(|c| c.id == target_cat_id);
                            if let (Some(si), Some(ti)) = (src_idx, tgt_idx) {
                                let removed = cats.remove(si);
                                cats.insert(ti, removed);
                                for (i, c) in cats.iter_mut().enumerate() {
                                    c.sort_order = i as i32;
                                    let _ = db_ref.update_category(c);
                                }
                                Self::populate_category_expander(
                                    &db_ref, &expander_ref, txn_type, &toast_ref,
                                );
                            }
                        }
                        true
                    } else {
                        false
                    }
                });
                row.add_controller(drop_target);
            }

            if !cat.is_default {
                let delete_btn = gtk::Button::from_icon_name("outlay-delete");
                delete_btn.add_css_class("flat");
                delete_btn.set_valign(gtk::Align::Center);
                delete_btn.set_tooltip_text(Some("Delete category"));

                let cat_id = cat.id;
                let db_ref = db.clone();
                let expander_ref = expander.clone();
                let toast_ref = toast_overlay.clone();
                delete_btn.connect_clicked(move |btn| {
                    let alert = adw::AlertDialog::new(
                        Some("Delete this category?"),
                        Some("Transactions using this category will not be deleted."),
                    );
                    alert.add_response("cancel", "Cancel");
                    alert.add_response("delete", "Delete");
                    alert.set_response_appearance("delete", adw::ResponseAppearance::Destructive);
                    alert.set_default_response(Some("cancel"));
                    alert.set_close_response("cancel");

                    let db_del = db_ref.clone();
                    let exp_del = expander_ref.clone();
                    let toast_del = toast_ref.clone();
                    alert.connect_response(None, move |_, response| {
                        if response == "delete" {
                            match db_del.delete_category(cat_id) {
                                Ok(()) => {
                                    let toast = adw::Toast::new("Category deleted");
                                    toast_del.add_toast(toast);
                                    Self::populate_category_expander(
                                        &db_del, &exp_del, txn_type, &toast_del,
                                    );
                                }
                                Err(e) => {
                                    let toast = adw::Toast::new(&format!("Error: {}", e));
                                    toast_del.add_toast(toast);
                                }
                            }
                        }
                    });
                    alert.present(Some(btn));
                });

                row.add_suffix(&delete_btn);
            }

            let chevron = gtk::Image::from_icon_name("go-next-symbolic");
            row.add_suffix(&chevron);

            expander.add_row(&row);
        }
    }

    fn show_edit_category_dialog(
        parent: &adw::ActionRow,
        cat: &Category,
        db: &Rc<Database>,
        expander: &adw::ExpanderRow,
        txn_type: TransactionType,
        toast_overlay: &adw::ToastOverlay,
    ) {
        let dialog = adw::Dialog::builder()
            .title("Edit Category")
            .content_width(400)
            .content_height(350)
            .build();

        let toolbar = adw::ToolbarView::new();
        toolbar.add_top_bar(&adw::HeaderBar::new());

        let content = gtk::Box::new(gtk::Orientation::Vertical, 16);
        content.set_margin_top(16);
        content.set_margin_bottom(16);
        content.set_margin_start(16);
        content.set_margin_end(16);

        let form = adw::PreferencesGroup::new();

        let name_row = adw::EntryRow::builder()
            .title("Name")
            .text(&cat.name)
            .build();
        form.add(&name_row);

        let selected_icon: Rc<RefCell<Option<String>>> = Rc::new(RefCell::new(cat.icon.clone()));

        let initial_icon = icon_theme::resolve_category_icon(&cat.icon, &cat.color)
            .unwrap_or_else(|| "outlay-list".to_string());
        let icon_preview = gtk::Image::builder()
            .icon_name(&initial_icon)
            .pixel_size(24)
            .build();

        let icon_subtitle = cat.icon.as_deref().unwrap_or("None selected");
        let icon_row = adw::ActionRow::builder()
            .title("Icon")
            .subtitle(icon_subtitle)
            .activatable(true)
            .build();
        icon_row.add_prefix(&icon_preview);
        let chevron = gtk::Image::from_icon_name("go-next-symbolic");
        icon_row.add_suffix(&chevron);

        {
            let selected_icon_ref = selected_icon.clone();
            let icon_preview_ref = icon_preview.clone();
            let icon_row_ref = icon_row.clone();
            let dialog_ref = dialog.clone();
            icon_row.connect_activated(move |_| {
                Self::show_icon_picker_dialog(
                    &dialog_ref,
                    &selected_icon_ref,
                    &icon_preview_ref,
                    &icon_row_ref,
                );
            });
        }
        form.add(&icon_row);

        let current_color = cat.color.as_deref()
            .and_then(|hex| {
                let hex = hex.trim_start_matches('#');
                if hex.len() != 6 { return None; }
                let r = u8::from_str_radix(&hex[0..2], 16).ok()? as f32 / 255.0;
                let g = u8::from_str_radix(&hex[2..4], 16).ok()? as f32 / 255.0;
                let b = u8::from_str_radix(&hex[4..6], 16).ok()? as f32 / 255.0;
                Some(gdk::RGBA::new(r, g, b, 1.0))
            })
            .unwrap_or_else(|| gdk::RGBA::new(0.204, 0.596, 0.859, 1.0));
        let color_dialog = gtk::ColorDialog::new();
        let color_button = gtk::ColorDialogButton::new(Some(color_dialog));
        color_button.set_rgba(&current_color);
        color_button.set_valign(gtk::Align::Center);

        let color_row = adw::ActionRow::builder()
            .title("Color")
            .build();
        color_row.add_suffix(&color_button);
        form.add(&color_row);

        let save_btn = gtk::Button::with_label("Save");
        save_btn.add_css_class("suggested-action");
        save_btn.add_css_class("pill");
        save_btn.set_halign(gtk::Align::Center);

        content.append(&form);
        content.append(&save_btn);
        toolbar.set_content(Some(&content));
        dialog.set_child(Some(&toolbar));

        {
            let cat_id = cat.id;
            let sort_order = cat.sort_order;
            let is_default = cat.is_default;
            let db_ref = db.clone();
            let dialog_ref = dialog.clone();
            let expander_ref = expander.clone();
            let toast_ref = toast_overlay.clone();
            let selected_icon_ref = selected_icon.clone();
            save_btn.connect_clicked(move |_| {
                let name = name_row.text().to_string();
                if name.trim().is_empty() {
                    let toast = adw::Toast::new("Please enter a category name");
                    toast_ref.add_toast(toast);
                    return;
                }

                let icon = selected_icon_ref.borrow().clone();
                let color = Some(Self::rgba_to_hex(&color_button.rgba()));

                let updated = Category {
                    id: cat_id,
                    name: name.trim().to_string(),
                    icon,
                    color,
                    transaction_type: txn_type,
                    is_default,
                    sort_order,
                    parent_id: None,
                };

                match db_ref.update_category(&updated) {
                    Ok(()) => {
                        dialog_ref.close();
                        let toast = adw::Toast::new("Category updated");
                        toast_ref.add_toast(toast);
                        Self::populate_category_expander(
                            &db_ref, &expander_ref, txn_type, &toast_ref,
                        );
                    }
                    Err(e) => {
                        let toast = adw::Toast::new(&format!("Error: {}", e));
                        toast_ref.add_toast(toast);
                    }
                }
            });
        }

        dialog.present(Some(parent));
    }

    fn rgba_to_hex(rgba: &gdk::RGBA) -> String {
        let r = (rgba.red() * 255.0).round() as u8;
        let g = (rgba.green() * 255.0).round() as u8;
        let b = (rgba.blue() * 255.0).round() as u8;
        format!("#{:02x}{:02x}{:02x}", r, g, b)
    }

    fn show_add_category_dialog(
        parent: &gtk::Button,
        db: &Rc<Database>,
        expense_expander: &adw::ExpanderRow,
        income_expander: &adw::ExpanderRow,
        toast_overlay: &adw::ToastOverlay,
    ) {
        let dialog = adw::Dialog::builder()
            .title("Add Category")
            .content_width(400)
            .content_height(420)
            .build();

        let toolbar = adw::ToolbarView::new();
        toolbar.add_top_bar(&adw::HeaderBar::new());

        let content = gtk::Box::new(gtk::Orientation::Vertical, 16);
        content.set_margin_top(16);
        content.set_margin_bottom(16);
        content.set_margin_start(16);
        content.set_margin_end(16);

        let form = adw::PreferencesGroup::new();

        let name_row = adw::EntryRow::builder()
            .title("Name")
            .build();
        form.add(&name_row);

        // Icon row - ActionRow that opens icon picker
        let selected_icon: Rc<RefCell<Option<String>>> = Rc::new(RefCell::new(None));

        let icon_preview = gtk::Image::builder()
            .icon_name("outlay-list")
            .pixel_size(24)
            .build();

        let icon_row = adw::ActionRow::builder()
            .title("Icon")
            .subtitle("None selected")
            .activatable(true)
            .build();
        icon_row.add_prefix(&icon_preview);

        let chevron = gtk::Image::from_icon_name("go-next-symbolic");
        icon_row.add_suffix(&chevron);

        {
            let selected_icon_ref = selected_icon.clone();
            let icon_preview_ref = icon_preview.clone();
            let icon_row_ref = icon_row.clone();
            let dialog_ref = dialog.clone();
            icon_row.connect_activated(move |_| {
                Self::show_icon_picker_dialog(
                    &dialog_ref,
                    &selected_icon_ref,
                    &icon_preview_ref,
                    &icon_row_ref,
                );
            });
        }

        form.add(&icon_row);

        // Color row - with ColorDialogButton
        let default_color = gdk::RGBA::new(0.204, 0.596, 0.859, 1.0); // #3498db
        let color_dialog = gtk::ColorDialog::new();
        let color_button = gtk::ColorDialogButton::new(Some(color_dialog));
        color_button.set_rgba(&default_color);
        color_button.set_valign(gtk::Align::Center);

        let color_row = adw::ActionRow::builder()
            .title("Color")
            .build();
        color_row.add_suffix(&color_button);

        form.add(&color_row);

        // Type row
        let type_labels = ["Expense", "Income"];
        let type_model = gtk::StringList::new(&type_labels);
        let type_row = adw::ComboRow::builder()
            .title("Type")
            .model(&type_model)
            .build();
        form.add(&type_row);

        let save_btn = gtk::Button::with_label("Save");
        save_btn.add_css_class("suggested-action");
        save_btn.add_css_class("pill");
        save_btn.set_halign(gtk::Align::Center);

        content.append(&form);
        content.append(&save_btn);
        toolbar.set_content(Some(&content));
        dialog.set_child(Some(&toolbar));

        {
            let db_ref = db.clone();
            let dialog_ref = dialog.clone();
            let expense_ref = expense_expander.clone();
            let income_ref = income_expander.clone();
            let toast_ref = toast_overlay.clone();
            let selected_icon_ref = selected_icon.clone();
            save_btn.connect_clicked(move |_| {
                let name = name_row.text().to_string();
                if name.trim().is_empty() {
                    let toast = adw::Toast::new("Please enter a category name");
                    toast_ref.add_toast(toast);
                    return;
                }

                let icon = selected_icon_ref.borrow().clone();
                let color = Some(Self::rgba_to_hex(&color_button.rgba()));

                let txn_type = if type_row.selected() == 0 {
                    TransactionType::Expense
                } else {
                    TransactionType::Income
                };

                let new_cat = NewCategory {
                    name: name.trim().to_string(),
                    icon,
                    color,
                    transaction_type: txn_type,
                    sort_order: 100,
                    parent_id: None,
                };

                match db_ref.insert_category(&new_cat) {
                    Ok(_) => {
                        dialog_ref.close();
                        let toast = adw::Toast::new("Category added");
                        toast_ref.add_toast(toast);
                        Self::populate_category_expander(
                            &db_ref, &expense_ref, TransactionType::Expense, &toast_ref,
                        );
                        Self::populate_category_expander(
                            &db_ref, &income_ref, TransactionType::Income, &toast_ref,
                        );
                    }
                    Err(e) => {
                        let toast = adw::Toast::new(&format!("Error: {}", e));
                        toast_ref.add_toast(toast);
                    }
                }
            });
        }

        dialog.present(Some(parent));
    }

    fn show_icon_picker_dialog(
        parent: &adw::Dialog,
        selected_icon: &Rc<RefCell<Option<String>>>,
        icon_preview: &gtk::Image,
        icon_row: &adw::ActionRow,
    ) {
        let picker = adw::Dialog::builder()
            .title("Choose Icon")
            .content_width(480)
            .content_height(520)
            .build();

        let toolbar = adw::ToolbarView::new();
        toolbar.add_top_bar(&adw::HeaderBar::new());

        let vbox = gtk::Box::new(gtk::Orientation::Vertical, 8);
        vbox.set_margin_start(12);
        vbox.set_margin_end(12);
        vbox.set_margin_top(8);
        vbox.set_margin_bottom(8);

        let search = gtk::SearchEntry::builder()
            .placeholder_text("Search icons...")
            .build();
        vbox.append(&search);

        let flow = gtk::FlowBox::builder()
            .homogeneous(true)
            .min_children_per_line(6)
            .max_children_per_line(12)
            .selection_mode(gtk::SelectionMode::None)
            .build();
        flow.add_css_class("icon-picker-grid");

        let icons = icon_theme::list_tabler_icons();
        for icon_name in &icons {
            let image = gtk::Image::builder()
                .icon_name(icon_name)
                .pixel_size(24)
                .build();

            let display_name = icon_name.strip_prefix("tabler-").unwrap_or(icon_name);
            let btn = gtk::Button::builder()
                .child(&image)
                .tooltip_text(display_name)
                .build();
            btn.add_css_class("flat");
            btn.add_css_class("icon-picker-btn");

            let picker_ref = picker.clone();
            let selected_ref = selected_icon.clone();
            let preview_ref = icon_preview.clone();
            let row_ref = icon_row.clone();
            let name = icon_name.clone();
            btn.connect_clicked(move |_| {
                *selected_ref.borrow_mut() = Some(name.clone());
                preview_ref.set_icon_name(Some(&name));
                row_ref.set_subtitle(&name);
                picker_ref.close();
            });

            let child = gtk::FlowBoxChild::new();
            child.set_child(Some(&btn));
            child.set_widget_name(icon_name);
            flow.append(&child);
        }

        // Search filtering
        {
            let flow_ref = flow.clone();
            search.connect_search_changed(move |entry| {
                let query = entry.text().to_string().to_lowercase();
                let flow_filter = flow_ref.clone();
                flow_filter.set_filter_func(move |child| {
                    if query.is_empty() {
                        return true;
                    }
                    child.widget_name().to_lowercase().contains(&query)
                });
            });
        }

        let scroll = gtk::ScrolledWindow::builder()
            .hscrollbar_policy(gtk::PolicyType::Never)
            .vexpand(true)
            .child(&flow)
            .build();

        vbox.append(&scroll);
        toolbar.set_content(Some(&vbox));
        picker.set_child(Some(&toolbar));
        picker.present(Some(parent));
    }

    fn export_csv_action(
        row: &adw::ActionRow,
        db: &Rc<Database>,
        toast_overlay: &adw::ToastOverlay,
    ) {
        // 6.1: Export preview dialog
        let (txn_count, cat_count, earliest, latest) = db.get_export_stats().unwrap_or((0, 0, None, None));
        if txn_count == 0 {
            let toast = adw::Toast::new("No transactions to export");
            toast_overlay.add_toast(toast);
            return;
        }
        let date_range = match (&earliest, &latest) {
            (Some(e), Some(l)) => format!("{} to {}", e, l),
            _ => "Unknown".to_string(),
        };
        let est_size = txn_count as f64 * 100.0 / 1024.0;

        let alert = adw::AlertDialog::new(
            Some("Export preview"),
            Some(&format!(
                "Date range: {}\nTransactions: {}\nCategories: {}\nEstimated size: ~{:.0} KB",
                date_range, txn_count, cat_count, est_size
            )),
        );
        alert.add_response("cancel", "Cancel");
        alert.add_response("export", "Export");
        alert.set_response_appearance("export", adw::ResponseAppearance::Suggested);
        alert.set_default_response(Some("export"));
        alert.set_close_response("cancel");

        let db_ref = db.clone();
        let toast_ref = toast_overlay.clone();
        let row_ref = row.clone();
        alert.connect_response(None, move |_, response| {
            if response != "export" { return; }

            let filter = gtk::FileFilter::new();
            filter.add_pattern("*.csv");
            filter.set_name(Some("CSV files"));
            let filters = gio::ListStore::new::<gtk::FileFilter>();
            filters.append(&filter);

            let dialog = gtk::FileDialog::builder()
                .title("Export CSV")
                .initial_name("outlay-export.csv")
                .default_filter(&filter)
                .filters(&filters)
                .build();

            let db_save = db_ref.clone();
            let toast_save = toast_ref.clone();
            let window = row_ref.root().and_then(|r| r.downcast::<gtk::Window>().ok());
            dialog.save(window.as_ref(), gio::Cancellable::NONE, move |result: Result<gio::File, glib::Error>| {
                if let Ok(file) = result {
                    if let Some(path) = file.path() {
                        match std::fs::File::create(&path) {
                            Ok(f) => {
                                match export_csv::export_transactions_csv(&db_save, f, None, None) {
                                    Ok(count) => {
                                        let display_path = path.display();
                                        let toast = adw::Toast::new(&format!(
                                            "Exported {} transactions to {}",
                                            count, display_path
                                        ));
                                        toast_save.add_toast(toast);
                                    }
                                    Err(e) => {
                                        let toast = adw::Toast::new(&format!("Export error: {}", e));
                                        toast_save.add_toast(toast);
                                    }
                                }
                            }
                            Err(e) => {
                                let toast = adw::Toast::new(&format!("File error: {}", e));
                                toast_save.add_toast(toast);
                            }
                        }
                    }
                }
            });
        });
        alert.present(Some(row));
    }

    fn export_json_action(
        row: &adw::ActionRow,
        db: &Rc<Database>,
        toast_overlay: &adw::ToastOverlay,
    ) {
        // 6.1: Export preview dialog
        let (txn_count, cat_count, earliest, latest) = db.get_export_stats().unwrap_or((0, 0, None, None));
        if txn_count == 0 {
            let toast = adw::Toast::new("No transactions to export");
            toast_overlay.add_toast(toast);
            return;
        }
        let date_range = match (&earliest, &latest) {
            (Some(e), Some(l)) => format!("{} to {}", e, l),
            _ => "Unknown".to_string(),
        };
        let est_size = txn_count as f64 * 200.0 / 1024.0;

        let alert = adw::AlertDialog::new(
            Some("Export preview"),
            Some(&format!(
                "Date range: {}\nTransactions: {}\nCategories: {}\nEstimated size: ~{:.0} KB",
                date_range, txn_count, cat_count, est_size
            )),
        );
        alert.add_response("cancel", "Cancel");
        alert.add_response("export", "Export");
        alert.set_response_appearance("export", adw::ResponseAppearance::Suggested);
        alert.set_default_response(Some("export"));
        alert.set_close_response("cancel");

        let db_ref = db.clone();
        let toast_ref = toast_overlay.clone();
        let row_ref = row.clone();
        alert.connect_response(None, move |_, response| {
            if response != "export" { return; }

            let filter = gtk::FileFilter::new();
            filter.add_pattern("*.json");
            filter.set_name(Some("JSON files"));
            let filters = gio::ListStore::new::<gtk::FileFilter>();
            filters.append(&filter);

            let dialog = gtk::FileDialog::builder()
                .title("Export JSON")
                .initial_name("outlay-export.json")
                .default_filter(&filter)
                .filters(&filters)
                .build();

            let db_save = db_ref.clone();
            let toast_save = toast_ref.clone();
            let window = row_ref.root().and_then(|r| r.downcast::<gtk::Window>().ok());
            dialog.save(window.as_ref(), gio::Cancellable::NONE, move |result: Result<gio::File, glib::Error>| {
                if let Ok(file) = result {
                    if let Some(path) = file.path() {
                        match std::fs::File::create(&path) {
                            Ok(f) => match export_json::export_json(&db_save, f) {
                                Ok(data) => {
                                    let display_path = path.display();
                                    let toast = adw::Toast::new(&format!(
                                        "Exported {} transactions to {}",
                                        data.transactions.len(), display_path
                                    ));
                                    toast_save.add_toast(toast);
                                }
                                Err(e) => {
                                    let toast = adw::Toast::new(&format!("Export error: {}", e));
                                    toast_save.add_toast(toast);
                                }
                            },
                            Err(e) => {
                                let toast = adw::Toast::new(&format!("File error: {}", e));
                                toast_save.add_toast(toast);
                            }
                        }
                    }
                }
            });
        });
        alert.present(Some(row));
    }

    fn export_pdf_action(
        row: &adw::ActionRow,
        db: &Rc<Database>,
        toast_overlay: &adw::ToastOverlay,
    ) {
        let dialog = adw::Dialog::builder()
            .title("Export PDF Report")
            .content_width(340)
            .content_height(200)
            .build();

        let toolbar = adw::ToolbarView::new();
        toolbar.add_top_bar(&adw::HeaderBar::new());

        let content = gtk::Box::new(gtk::Orientation::Vertical, 16);
        content.set_margin_top(16);
        content.set_margin_bottom(16);
        content.set_margin_start(16);
        content.set_margin_end(16);

        let today = Local::now().date_naive();
        let year = Rc::new(std::cell::Cell::new(today.year()));
        let month = Rc::new(std::cell::Cell::new(today.month()));

        let nav = gtk::Box::new(gtk::Orientation::Horizontal, 12);
        nav.set_halign(gtk::Align::Center);

        let prev_btn = gtk::Button::from_icon_name("go-previous-symbolic");
        prev_btn.add_css_class("flat");
        prev_btn.add_css_class("circular");
        prev_btn.set_tooltip_text(Some("Previous month"));

        let month_names = [
            "January", "February", "March", "April", "May", "June",
            "July", "August", "September", "October", "November", "December",
        ];
        let label = gtk::Label::new(Some(&format!(
            "{} {}", month_names[(today.month() - 1) as usize], today.year()
        )));
        label.add_css_class("title-3");

        let next_btn = gtk::Button::from_icon_name("go-next-symbolic");
        next_btn.add_css_class("flat");
        next_btn.add_css_class("circular");
        next_btn.set_tooltip_text(Some("Next month"));

        {
            let y = year.clone();
            let m = month.clone();
            let lbl = label.clone();
            prev_btn.connect_clicked(move |_| {
                let mut mm = m.get();
                let mut yy = y.get();
                if mm == 1 { mm = 12; yy -= 1; } else { mm -= 1; }
                m.set(mm);
                y.set(yy);
                lbl.set_text(&format!("{} {}", month_names[(mm - 1) as usize], yy));
            });
        }
        {
            let y = year.clone();
            let m = month.clone();
            let lbl = label.clone();
            next_btn.connect_clicked(move |_| {
                let mut mm = m.get();
                let mut yy = y.get();
                if mm == 12 { mm = 1; yy += 1; } else { mm += 1; }
                m.set(mm);
                y.set(yy);
                lbl.set_text(&format!("{} {}", month_names[(mm - 1) as usize], yy));
            });
        }

        nav.append(&prev_btn);
        nav.append(&label);
        nav.append(&next_btn);
        content.append(&nav);

        let export_btn = gtk::Button::with_label("Export");
        export_btn.add_css_class("suggested-action");
        export_btn.add_css_class("pill");
        export_btn.set_halign(gtk::Align::Center);
        export_btn.set_margin_top(8);

        let db_ref = db.clone();
        let toast_ref = toast_overlay.clone();
        let dialog_ref = dialog.clone();
        let parent_row = row.clone();
        export_btn.connect_clicked(move |_| {
            dialog_ref.close();

            let sel_year = year.get();
            let sel_month = month.get();

            let base_currency = db_ref
                .get_setting("base_currency")
                .ok()
                .flatten()
                .unwrap_or_else(|| "USD".to_string());

            let filter = gtk::FileFilter::new();
            filter.add_pattern("*.pdf");
            filter.set_name(Some("PDF files"));

            let filters = gio::ListStore::new::<gtk::FileFilter>();
            filters.append(&filter);

            let default_name = format!(
                "outlay-report-{:04}-{:02}.pdf",
                sel_year, sel_month
            );
            let file_dialog = gtk::FileDialog::builder()
                .title("Export PDF Report")
                .initial_name(&default_name)
                .default_filter(&filter)
                .filters(&filters)
                .build();

            let db_save = db_ref.clone();
            let toast_save = toast_ref.clone();
            let window = parent_row.root().and_then(|r| r.downcast::<gtk::Window>().ok());
            file_dialog.save(window.as_ref(), gio::Cancellable::NONE, move |result: Result<gio::File, glib::Error>| {
                if let Ok(file) = result {
                    if let Some(path) = file.path() {
                        match export_pdf::generate_monthly_report(
                            &db_save,
                            sel_year,
                            sel_month,
                            &base_currency,
                            &path,
                        ) {
                            Ok(()) => {
                                let display_path = path.display();
                                let toast = adw::Toast::new(&format!(
                                    "PDF report exported to {}", display_path
                                ));
                                toast_save.add_toast(toast);
                            }
                            Err(e) => {
                                let toast = adw::Toast::new(&format!("PDF error: {}", e));
                                toast_save.add_toast(toast);
                            }
                        }
                    }
                }
            });
        });

        content.append(&export_btn);
        toolbar.set_content(Some(&content));
        dialog.set_child(Some(&toolbar));
        dialog.present(Some(row));
    }

    fn export_qif_action(
        row: &adw::ActionRow,
        db: &Rc<Database>,
        toast_overlay: &adw::ToastOverlay,
    ) {
        let filter = gtk::FileFilter::new();
        filter.add_pattern("*.qif");
        filter.set_name(Some("QIF files"));

        let filters = gio::ListStore::new::<gtk::FileFilter>();
        filters.append(&filter);

        let today = Local::now().date_naive();
        let default_name = format!("outlay-{}.qif", today.format("%Y-%m-%d"));
        let dialog = gtk::FileDialog::builder()
            .title("Export QIF")
            .initial_name(&default_name)
            .default_filter(&filter)
            .filters(&filters)
            .build();

        let db_ref = db.clone();
        let toast_ref = toast_overlay.clone();
        let window = row.root().and_then(|r| r.downcast::<gtk::Window>().ok());
        dialog.save(window.as_ref(), gio::Cancellable::NONE, move |result: Result<gio::File, glib::Error>| {
            if let Ok(file) = result {
                if let Some(path) = file.path() {
                    match std::fs::File::create(&path) {
                        Ok(mut f) => match export_qif::export_qif(&db_ref, &mut f, None, None) {
                            Ok(count) => {
                                let toast = adw::Toast::new(&format!(
                                    "Exported {} transactions to {}", count, path.display()
                                ));
                                toast_ref.add_toast(toast);
                            }
                            Err(e) => {
                                let toast = adw::Toast::new(&format!("QIF export error: {}", e));
                                toast_ref.add_toast(toast);
                            }
                        },
                        Err(e) => {
                            let toast = adw::Toast::new(&format!("File error: {}", e));
                            toast_ref.add_toast(toast);
                        }
                    }
                }
            }
        });
    }

    fn export_ofx_action(
        row: &adw::ActionRow,
        db: &Rc<Database>,
        toast_overlay: &adw::ToastOverlay,
    ) {
        let filter = gtk::FileFilter::new();
        filter.add_pattern("*.ofx");
        filter.set_name(Some("OFX files"));

        let filters = gio::ListStore::new::<gtk::FileFilter>();
        filters.append(&filter);

        let today = Local::now().date_naive();
        let default_name = format!("outlay-{}.ofx", today.format("%Y-%m-%d"));
        let dialog = gtk::FileDialog::builder()
            .title("Export OFX")
            .initial_name(&default_name)
            .default_filter(&filter)
            .filters(&filters)
            .build();

        let db_ref = db.clone();
        let toast_ref = toast_overlay.clone();
        let window = row.root().and_then(|r| r.downcast::<gtk::Window>().ok());
        dialog.save(window.as_ref(), gio::Cancellable::NONE, move |result: Result<gio::File, glib::Error>| {
            if let Ok(file) = result {
                if let Some(path) = file.path() {
                    match std::fs::File::create(&path) {
                        Ok(mut f) => match export_ofx::export_ofx(&db_ref, &mut f, None, None) {
                            Ok(count) => {
                                let toast = adw::Toast::new(&format!(
                                    "Exported {} transactions to {}", count, path.display()
                                ));
                                toast_ref.add_toast(toast);
                            }
                            Err(e) => {
                                let toast = adw::Toast::new(&format!("OFX export error: {}", e));
                                toast_ref.add_toast(toast);
                            }
                        },
                        Err(e) => {
                            let toast = adw::Toast::new(&format!("File error: {}", e));
                            toast_ref.add_toast(toast);
                        }
                    }
                }
            }
        });
    }

    fn run_auto_backup(db: &Database, backup_dir: &std::path::Path) -> Result<std::path::PathBuf, String> {
        std::fs::create_dir_all(backup_dir).map_err(|e| format!("Could not create backup directory: {}", e))?;

        let now = Local::now();
        let filename = format!("outlay-auto-{}.db", now.format("%Y-%m-%d-%H%M%S"));
        let backup_path = backup_dir.join(&filename);

        // Use SQLite backup API via VACUUM INTO
        let path_str = backup_path.to_string_lossy();
        db.execute_raw(&format!("VACUUM INTO '{}'", path_str.replace('\'', "''")))
            .map_err(|e| format!("Backup failed: {}", e))?;

        let _ = db.set_setting("auto_backup_last", &now.format("%Y-%m-%d %H:%M").to_string());
        let _ = db.set_setting("last_backup_date", &now.format("%Y-%m-%d").to_string());

        let retention = db.get_setting("auto_backup_retention")
            .ok().flatten()
            .and_then(|v| v.parse::<usize>().ok())
            .unwrap_or(5);

        let mut backups: Vec<_> = std::fs::read_dir(backup_dir)
            .map_err(|e| format!("Could not read backup dir: {}", e))?
            .filter_map(|e| e.ok())
            .filter(|e| {
                e.file_name().to_string_lossy().starts_with("outlay-auto-")
                    && e.file_name().to_string_lossy().ends_with(".db")
            })
            .collect();

        backups.sort_by_key(|e| e.file_name());

        if backups.len() > retention {
            let to_delete = backups.len() - retention;
            for entry in backups.iter().take(to_delete) {
                let _ = std::fs::remove_file(entry.path());
            }
        }

        Ok(backup_path)
    }

    pub fn check_and_run_auto_backup(db: &Database) -> Option<std::path::PathBuf> {
        let enabled = db.get_setting("auto_backup_enabled")
            .ok().flatten().unwrap_or_default();
        if enabled != "1" {
            return None;
        }

        let frequency = db.get_setting("auto_backup_frequency")
            .ok().flatten().unwrap_or_else(|| "weekly".to_string());
        let last_backup = db.get_setting("auto_backup_last").ok().flatten();

        let today = Local::now().date_naive();
        let should_backup = match &last_backup {
            Some(date_str) => {
                // Parse "YYYY-MM-DD HH:MM" or "YYYY-MM-DD"
                let date_part = date_str.split(' ').next().unwrap_or(date_str);
                if let Ok(last) = chrono::NaiveDate::parse_from_str(date_part, "%Y-%m-%d") {
                    let days = (today - last).num_days();
                    match frequency.as_str() {
                        "daily" => days >= 1,
                        "monthly" => days >= 30,
                        _ => days >= 7, // weekly
                    }
                } else {
                    true
                }
            }
            None => true,
        };

        if !should_backup {
            return None;
        }

        let backup_dir_setting = db.get_setting("auto_backup_dir").ok().flatten().unwrap_or_default();
        let backup_dir = if backup_dir_setting.is_empty() {
            gtk::glib::user_data_dir().join("outlay").join("backups")
        } else {
            std::path::PathBuf::from(&backup_dir_setting)
        };

        Self::run_auto_backup(db, &backup_dir).ok()
    }

    fn backup_action(
        row: &adw::ActionRow,
        db: &Rc<Database>,
        toast_overlay: &adw::ToastOverlay,
    ) {
        let filter = gtk::FileFilter::new();
        filter.add_pattern("*.outlay");
        filter.set_name(Some("Outlay backup files"));

        let filters = gio::ListStore::new::<gtk::FileFilter>();
        filters.append(&filter);

        let today = Local::now().date_naive();
        let default_name = format!(
            "outlay-backup-{}.outlay",
            today.format("%Y-%m-%d")
        );
        let dialog = gtk::FileDialog::builder()
            .title("Create Backup")
            .initial_name(&default_name)
            .default_filter(&filter)
            .filters(&filters)
            .build();

        let db_ref = db.clone();
        let toast_ref = toast_overlay.clone();
        let window = row.root().and_then(|r| r.downcast::<gtk::Window>().ok());
        dialog.save(window.as_ref(), gio::Cancellable::NONE, move |result: Result<gio::File, glib::Error>| {
            if let Ok(file) = result {
                if let Some(path) = file.path() {
                    match backup::create_backup(&db_ref, &path) {
                        Ok(meta) => {
                            let today = Local::now().date_naive().format("%Y-%m-%d").to_string();
                            let _ = db_ref.set_setting("last_backup_date", &today);
                            let toast = adw::Toast::new(&format!(
                                "Backup created ({} transactions) at {}",
                                meta.transaction_count, path.display()
                            ));
                            toast_ref.add_toast(toast);
                        }
                        Err(e) => {
                            let toast = adw::Toast::new(&format!("Backup error: {}", e));
                            toast_ref.add_toast(toast);
                        }
                    }
                }
            }
        });
    }

    fn restore_action(
        row: &adw::ActionRow,
        toast_overlay: &adw::ToastOverlay,
        app: &adw::Application,
    ) {
        let filter = gtk::FileFilter::new();
        filter.add_pattern("*.outlay");
        filter.set_name(Some("Outlay backup files"));

        let filters = gio::ListStore::new::<gtk::FileFilter>();
        filters.append(&filter);

        let dialog = gtk::FileDialog::builder()
            .title("Restore from Backup")
            .default_filter(&filter)
            .filters(&filters)
            .build();

        let toast_ref = toast_overlay.clone();
        let app_ref = app.clone();
        let window = row.root().and_then(|r| r.downcast::<gtk::Window>().ok());
        dialog.open(window.as_ref(), gio::Cancellable::NONE, move |result: Result<gio::File, glib::Error>| {
            if let Ok(file) = result {
                if let Some(path) = file.path() {
                    // Read meta first
                    match backup::read_backup_meta(&path) {
                        Ok(meta) => {
                            let msg = format!(
                                "This backup contains {} transactions and {} categories.\nCreated on {}.\n\nRestore will replace all current data.",
                                meta.transaction_count,
                                meta.category_count,
                                meta.export_date,
                            );
                            let alert = adw::AlertDialog::new(
                                Some("Restore from Backup?"),
                                Some(&msg),
                            );
                            alert.add_response("cancel", "Cancel");
                            alert.add_response("restore", "Restore");
                            alert.set_response_appearance(
                                "restore",
                                adw::ResponseAppearance::Destructive,
                            );
                            alert.set_default_response(Some("cancel"));
                            alert.set_close_response("cancel");

                            let toast_alert = toast_ref.clone();
                            let app_alert = app_ref.clone();
                            let path_clone = path.to_path_buf();
                            alert.connect_response(None, move |_, response| {
                                if response == "restore" {
                                    let db_path = glib::user_data_dir()
                                        .join("outlay")
                                        .join("outlay.db");
                                    match backup::restore_backup(&path_clone, &db_path) {
                                        Ok(_) => {
                                            let toast = adw::Toast::new(
                                                "Backup restored. Please restart the app.",
                                            );
                                            toast_alert.add_toast(toast);
                                            // Close app after short delay
                                            let app_quit = app_alert.clone();
                                            glib::timeout_add_local_once(
                                                std::time::Duration::from_secs(2),
                                                move || {
                                                    app_quit.quit();
                                                },
                                            );
                                        }
                                        Err(e) => {
                                            let toast = adw::Toast::new(&format!(
                                                "Restore error: {}",
                                                e
                                            ));
                                            toast_alert.add_toast(toast);
                                        }
                                    }
                                }
                            });

                            if let Some(w) = app_ref.active_window() {
                                alert.present(Some(&w));
                            }
                        }
                        Err(e) => {
                            let toast =
                                adw::Toast::new(&format!("Invalid backup file: {}", e));
                            toast_ref.add_toast(toast);
                        }
                    }
                }
            }
        });
    }

    fn reset_action(
        row: &adw::ActionRow,
        db: &Rc<Database>,
        toast_overlay: &adw::ToastOverlay,
        on_reset: &Rc<RefCell<Option<Box<dyn Fn()>>>>,
    ) {
        let alert = adw::AlertDialog::new(
            Some("Reset All Data?"),
            Some("This will permanently delete all transactions, budgets, recurring transactions, and settings. This cannot be undone."),
        );
        alert.add_response("cancel", "Cancel");
        alert.add_response("reset", "Reset Everything");
        alert.set_response_appearance("reset", adw::ResponseAppearance::Destructive);
        alert.set_default_response(Some("cancel"));
        alert.set_close_response("cancel");

        let db_ref = db.clone();
        let toast_ref = toast_overlay.clone();
        let reset_cb = on_reset.clone();
        alert.connect_response(None, move |_, response| {
            if response == "reset" {
                match db_ref.reset_all_data() {
                    Ok(()) => {
                        let toast = adw::Toast::new("All data has been reset");
                        toast_ref.add_toast(toast);
                        if let Some(cb) = reset_cb.borrow().as_ref() {
                            cb();
                        }
                    }
                    Err(e) => {
                        let toast = adw::Toast::new(&format!("Error: {}", e));
                        toast_ref.add_toast(toast);
                    }
                }
            }
        });

        alert.present(Some(row));
    }

    fn import_csv_action(
        row: &adw::ActionRow,
        db: &Rc<Database>,
        toast_overlay: &adw::ToastOverlay,
    ) {
        Self::show_import_dialog(row, db, toast_overlay, "CSV", "*.csv", "CSV files");
    }

    fn import_json_action(
        row: &adw::ActionRow,
        db: &Rc<Database>,
        toast_overlay: &adw::ToastOverlay,
    ) {
        Self::show_import_dialog(row, db, toast_overlay, "JSON", "*.json", "JSON files");
    }

    fn import_qif_action(
        row: &adw::ActionRow,
        db: &Rc<Database>,
        toast_overlay: &adw::ToastOverlay,
    ) {
        Self::show_import_dialog(row, db, toast_overlay, "QIF", "*.qif", "QIF files");
    }

    fn import_ofx_action(
        row: &adw::ActionRow,
        db: &Rc<Database>,
        toast_overlay: &adw::ToastOverlay,
    ) {
        Self::show_import_dialog(row, db, toast_overlay, "OFX", "*.ofx", "OFX files");
    }

    fn import_pdf_action(
        row: &adw::ActionRow,
        db: &Rc<Database>,
        toast_overlay: &adw::ToastOverlay,
    ) {
        let filter = gtk::FileFilter::new();
        filter.add_pattern("*.pdf");
        filter.set_name(Some("PDF files"));

        let filters = gio::ListStore::new::<gtk::FileFilter>();
        filters.append(&filter);

        let file_dialog = gtk::FileDialog::builder()
            .title("Select PDF Statement")
            .default_filter(&filter)
            .filters(&filters)
            .build();

        let db_ref = db.clone();
        let toast_ref = toast_overlay.clone();
        let parent_row = row.clone();
        let window = row.root().and_then(|r| r.downcast::<gtk::Window>().ok());
        file_dialog.open(window.as_ref(), gio::Cancellable::NONE, move |result| {
            if let Ok(file) = result {
                if let Some(path) = file.path() {
                    let bytes = match std::fs::read(&path) {
                        Ok(b) => b,
                        Err(e) => {
                            let toast = adw::Toast::new(&format!("Error reading file: {}", e));
                            toast_ref.add_toast(toast);
                            return;
                        }
                    };

                    let rows = match import_pdf::extract_transactions_from_pdf(&bytes) {
                        Ok(r) if r.is_empty() => {
                            let toast = adw::Toast::new("No transactions found in PDF");
                            toast_ref.add_toast(toast);
                            return;
                        }
                        Ok(r) => r,
                        Err(e) => {
                            let toast = adw::Toast::new(&format!("PDF parse error: {}", e));
                            toast_ref.add_toast(toast);
                            return;
                        }
                    };

                    Self::show_pdf_preview_dialog(&parent_row, &db_ref, &toast_ref, rows);
                }
            }
        });
    }

    fn show_pdf_preview_dialog(
        parent: &adw::ActionRow,
        db: &Rc<Database>,
        toast_overlay: &adw::ToastOverlay,
        rows: Vec<outlay_core::models::PdfParsedRow>,
    ) {
        let dialog = adw::Dialog::builder()
            .title("Import PDF Statement")
            .content_width(500)
            .content_height(500)
            .build();

        let toolbar = adw::ToolbarView::new();
        toolbar.add_top_bar(&adw::HeaderBar::new());

        let content = gtk::Box::new(gtk::Orientation::Vertical, 8);
        content.set_margin_top(8);
        content.set_margin_bottom(8);
        content.set_margin_start(12);
        content.set_margin_end(12);

        let info_label = gtk::Label::new(Some(&format!(
            "Found {} transaction(s). Select which to import.",
            rows.len()
        )));
        info_label.set_xalign(0.0);
        info_label.add_css_class("dim-label");
        content.append(&info_label);

        // Merge/Replace toggle
        let merge_check = gtk::CheckButton::with_label("Merge (skip duplicates)");
        merge_check.set_active(true);
        let replace_check = gtk::CheckButton::with_label("Replace all data");
        replace_check.set_group(Some(&merge_check));
        let mode_box = gtk::Box::new(gtk::Orientation::Horizontal, 12);
        mode_box.append(&merge_check);
        mode_box.append(&replace_check);
        content.append(&mode_box);

        // Scrollable list of parsed rows
        let scrolled = gtk::ScrolledWindow::builder()
            .vexpand(true)
            .hscrollbar_policy(gtk::PolicyType::Never)
            .build();

        let list_box = gtk::ListBox::new();
        list_box.set_selection_mode(gtk::SelectionMode::None);
        list_box.add_css_class("boxed-list");

        // Load categories for the combo boxes
        let all_cats = db.list_categories(None).unwrap_or_default();

        // Store checkboxes and category combos for each row
        let check_buttons: Rc<RefCell<Vec<(gtk::CheckButton, gtk::DropDown, usize)>>> =
            Rc::new(RefCell::new(Vec::new()));

        for (i, parsed) in rows.iter().enumerate() {
            let check = gtk::CheckButton::new();
            check.set_active(true);

            let date_str = parsed
                .date
                .map(|d| d.format("%Y-%m-%d").to_string())
                .unwrap_or_else(|| "No date".to_string());

            let type_str = if parsed.is_credit { "Income" } else { "Expense" };
            let subtitle = format!(
                "{} - {} {:.2}",
                date_str, type_str, parsed.amount
            );

            let action_row = adw::ActionRow::builder()
                .title(&parsed.description)
                .subtitle(&subtitle)
                .build();

            action_row.add_prefix(&check);

            // Category combo
            let filtered_cats: Vec<&Category> = all_cats
                .iter()
                .filter(|c| {
                    if parsed.is_credit {
                        c.transaction_type == TransactionType::Income
                    } else {
                        c.transaction_type == TransactionType::Expense
                    }
                })
                .collect();

            let cat_labels: Vec<String> = filtered_cats
                .iter()
                .map(|c| {
                    let icon = crate::icon_theme::resolve_category_icon(&c.icon, &c.color);
                    match icon {
                        Some(i) => format!("{}\t{}", i, c.name),
                        None => c.name.clone(),
                    }
                })
                .collect();

            let label_refs: Vec<&str> = cat_labels.iter().map(|s| s.as_str()).collect();
            let cat_model = gtk::StringList::new(&label_refs);
            let cat_combo = gtk::DropDown::builder()
                .model(&cat_model)
                .valign(gtk::Align::Center)
                .build();
            cat_combo.set_factory(Some(&crate::category_combo::make_category_factory()));
            cat_combo.set_list_factory(Some(&crate::category_combo::make_category_factory()));

            // Try auto-match via rules
            let mut matched_idx = 0u32;
            if let Ok(Some(cat_id)) = db.match_category(Some(&parsed.description), None) {
                if let Some(pos) = filtered_cats.iter().position(|c| c.id == cat_id) {
                    matched_idx = pos as u32;
                }
            }
            cat_combo.set_selected(matched_idx);

            action_row.add_suffix(&cat_combo);

            list_box.append(&action_row);
            check_buttons.borrow_mut().push((check, cat_combo, i));
        }

        scrolled.set_child(Some(&list_box));
        content.append(&scrolled);

        // Bottom buttons
        let btn_box = gtk::Box::new(gtk::Orientation::Horizontal, 8);
        btn_box.set_halign(gtk::Align::End);
        btn_box.set_margin_top(8);

        let select_all_btn = gtk::Button::with_label("Select All");
        let deselect_all_btn = gtk::Button::with_label("Deselect All");

        {
            let checks = check_buttons.clone();
            select_all_btn.connect_clicked(move |_| {
                for (cb, _, _) in checks.borrow().iter() {
                    cb.set_active(true);
                }
            });
        }
        {
            let checks = check_buttons.clone();
            deselect_all_btn.connect_clicked(move |_| {
                for (cb, _, _) in checks.borrow().iter() {
                    cb.set_active(false);
                }
            });
        }

        let import_btn = gtk::Button::with_label("Import Selected");
        import_btn.add_css_class("suggested-action");

        btn_box.append(&select_all_btn);
        btn_box.append(&deselect_all_btn);
        btn_box.append(&import_btn);
        content.append(&btn_box);

        // Wire import button
        let db_ref = db.clone();
        let toast_ref = toast_overlay.clone();
        let dialog_ref = dialog.clone();
        let checks = check_buttons.clone();
        let rows_clone = rows.clone();
        let all_cats_clone = all_cats.clone();
        import_btn.connect_clicked(move |_| {
            let merge = merge_check.is_active();
            let today = chrono::Local::now().date_naive();
            let base_currency = db_ref
                .get_setting("base_currency")
                .ok()
                .flatten()
                .unwrap_or_else(|| "USD".to_string());

            let mut imported = 0u32;
            let mut skipped = 0u32;

            for (cb, cat_combo, idx) in checks.borrow().iter() {
                if !cb.is_active() {
                    continue;
                }
                let parsed = &rows_clone[*idx];
                let txn_type = if parsed.is_credit {
                    TransactionType::Income
                } else {
                    TransactionType::Expense
                };
                let date = parsed.date.unwrap_or(today);

                // Get selected category from combo
                let filtered_cats: Vec<&Category> = all_cats_clone
                    .iter()
                    .filter(|c| c.transaction_type == txn_type)
                    .collect();
                let cat_idx = cat_combo.selected() as usize;
                let category_id = filtered_cats
                    .get(cat_idx)
                    .map(|c| c.id)
                    .unwrap_or(1);

                if merge {
                    if let Ok(true) = db_ref.find_duplicate_transaction(
                        parsed.amount,
                        txn_type,
                        category_id,
                        date,
                    ) {
                        skipped += 1;
                        continue;
                    }
                }

                let txn = NewTransaction {
                    amount: parsed.amount,
                    transaction_type: txn_type,
                    category_id,
                    currency: base_currency.clone(),
                    exchange_rate: 1.0,
                    note: Some(parsed.description.clone()),
                    date,
                    recurring_id: None,
                    payee: None,
                };

                match db_ref.insert_transaction(&txn) {
                    Ok(_) => imported += 1,
                    Err(e) => {
                        let toast = adw::Toast::new(&format!("Error: {}", e));
                        toast_ref.add_toast(toast);
                    }
                }
            }

            dialog_ref.close();

            let msg = if skipped > 0 {
                format!(
                    "Imported {} transaction(s), skipped {} duplicate(s)",
                    imported, skipped
                )
            } else {
                format!("Imported {} transaction(s) from PDF", imported)
            };
            let toast = adw::Toast::new(&msg);
            toast_ref.add_toast(toast);
        });

        toolbar.set_content(Some(&content));
        dialog.set_child(Some(&toolbar));
        dialog.present(Some(parent));
    }

    fn show_import_dialog(
        row: &adw::ActionRow,
        db: &Rc<Database>,
        toast_overlay: &adw::ToastOverlay,
        format_name: &str,
        pattern: &str,
        filter_name: &str,
    ) {
        let dialog = adw::Dialog::builder()
            .title(&format!("Import {}", format_name))
            .content_width(360)
            .content_height(220)
            .build();

        let toolbar = adw::ToolbarView::new();
        toolbar.add_top_bar(&adw::HeaderBar::new());

        let content = gtk::Box::new(gtk::Orientation::Vertical, 16);
        content.set_margin_top(16);
        content.set_margin_bottom(16);
        content.set_margin_start(16);
        content.set_margin_end(16);

        let desc = gtk::Label::new(Some(&format!(
            "Choose how to handle existing data when importing from {}.",
            format_name
        )));
        desc.set_wrap(true);
        desc.set_xalign(0.0);
        content.append(&desc);

        let merge_check = gtk::CheckButton::with_label("Merge (skip duplicates)");
        merge_check.set_active(true);
        let replace_check = gtk::CheckButton::with_label("Replace all data");
        replace_check.set_group(Some(&merge_check));

        content.append(&merge_check);
        content.append(&replace_check);

        let choose_btn = gtk::Button::with_label("Choose File");
        choose_btn.add_css_class("suggested-action");
        choose_btn.add_css_class("pill");
        choose_btn.set_halign(gtk::Align::Center);
        choose_btn.set_margin_top(8);

        let db_ref = db.clone();
        let toast_ref = toast_overlay.clone();
        let dialog_ref = dialog.clone();
        let parent_row = row.clone();
        let fmt = format_name.to_string();
        let pat = pattern.to_string();
        let fn_name = filter_name.to_string();
        choose_btn.connect_clicked(move |_| {
            let merge = merge_check.is_active();
            dialog_ref.close();

            let filter = gtk::FileFilter::new();
            filter.add_pattern(&pat);
            filter.set_name(Some(&fn_name));

            let filters = gio::ListStore::new::<gtk::FileFilter>();
            filters.append(&filter);

            let file_dialog = gtk::FileDialog::builder()
                .title(&format!("Select {} File", fmt))
                .default_filter(&filter)
                .filters(&filters)
                .build();

            let db_import = db_ref.clone();
            let toast_import = toast_ref.clone();
            let fmt_clone = fmt.clone();
            let window = parent_row.root().and_then(|r| r.downcast::<gtk::Window>().ok());
            file_dialog.open(window.as_ref(), gio::Cancellable::NONE, move |result: Result<gio::File, glib::Error>| {
                if let Ok(file) = result {
                    if let Some(path) = file.path() {
                        // 6.2: Show importing toast
                        let progress_toast = adw::Toast::new(&format!("Importing {}...", fmt_clone));
                        progress_toast.set_timeout(5);
                        toast_import.add_toast(progress_toast);

                        let result = match fmt_clone.as_str() {
                            "CSV" => import_csv::import_csv(&db_import, &path, merge),
                            "JSON" => import_json::import_json(&db_import, &path, merge),
                            "QIF" => outlay_core::import_qif::import_qif(&db_import, &path, merge),
                            "OFX" => outlay_core::import_ofx::import_ofx(&db_import, &path, merge),
                            _ => Err("Unknown format".into()),
                        };
                        match result {
                            Ok(count) => {
                                let toast = adw::Toast::new(&format!(
                                    "Imported {} transaction(s) from {}",
                                    count, path.display()
                                ));
                                toast_import.add_toast(toast);
                            }
                            Err(e) => {
                                let toast = adw::Toast::new(&format!("Import error: {}", e));
                                toast_import.add_toast(toast);
                            }
                        }
                    }
                }
            });
        });

        content.append(&choose_btn);
        toolbar.set_content(Some(&content));
        dialog.set_child(Some(&toolbar));
        dialog.present(Some(row));
    }

    // -- Rules helpers --

    fn populate_rules(
        db: &Rc<Database>,
        group: &adw::PreferencesGroup,
        toast_overlay: &adw::ToastOverlay,
    ) {
        let mut rows_to_remove = Vec::new();
        let mut child = group.first_child();
        while let Some(widget) = child {
            let next = widget.next_sibling();
            Self::collect_action_rows_recursive(&widget, &mut rows_to_remove);
            child = next;
        }
        for row in rows_to_remove {
            if let Some(r) = row.downcast_ref::<adw::ActionRow>() {
                group.remove(r);
            }
        }

        if let Ok(rules) = db.list_rules() {
            for rule in &rules {
                let cat_name = db
                    .get_category(rule.category_id)
                    .map(|c| c.name)
                    .unwrap_or_else(|_| "Unknown".to_string());
                let subtitle = format!(
                    "If {} contains \"{}\" -> {}",
                    rule.field, rule.pattern, cat_name
                );
                let row = adw::ActionRow::builder()
                    .title(&format!("Rule #{}", rule.id))
                    .subtitle(&subtitle)
                    .build();

                let del_btn = gtk::Button::from_icon_name("outlay-delete");
                del_btn.add_css_class("flat");
                del_btn.set_valign(gtk::Align::Center);
                del_btn.set_tooltip_text(Some("Delete rule"));
                {
                    let rule_id = rule.id;
                    let db_ref = db.clone();
                    let group_ref = group.clone();
                    let toast_ref = toast_overlay.clone();
                    del_btn.connect_clicked(move |_| {
                        let _ = db_ref.delete_rule(rule_id);
                        Self::populate_rules(&db_ref, &group_ref, &toast_ref);
                        let toast = adw::Toast::new("Rule deleted");
                        toast_ref.add_toast(toast);
                    });
                }
                row.add_suffix(&del_btn);
                group.add(&row);
            }
            if rules.is_empty() {
                let placeholder = adw::ActionRow::builder()
                    .title("No rules yet")
                    .build();
                placeholder.add_css_class("dim-label");
                group.add(&placeholder);
            }
        }
    }

    fn show_add_rule_dialog(
        parent: &gtk::Button,
        db: &Rc<Database>,
        group: &adw::PreferencesGroup,
        toast_overlay: &adw::ToastOverlay,
    ) {
        let dialog = adw::Dialog::builder()
            .title("Add Rule")
            .content_width(400)
            .content_height(420)
            .build();

        let toolbar = adw::ToolbarView::new();
        toolbar.add_top_bar(&adw::HeaderBar::new());

        let content = gtk::Box::new(gtk::Orientation::Vertical, 16);
        content.set_margin_top(16);
        content.set_margin_bottom(16);
        content.set_margin_start(16);
        content.set_margin_end(16);

        let form = adw::PreferencesGroup::new();

        let type_model = gtk::StringList::new(&["Expense", "Income"]);
        let type_row = adw::ComboRow::builder()
            .title("Type")
            .model(&type_model)
            .build();
        form.add(&type_row);

        // Field selector (note or payee)
        let field_model = gtk::StringList::new(&["Note", "Payee"]);
        let field_row = adw::ComboRow::builder()
            .title("Match field")
            .model(&field_model)
            .build();
        form.add(&field_row);

        let pattern_row = adw::EntryRow::builder()
            .title("Contains text")
            .build();
        form.add(&pattern_row);

        let cat_model = gtk::StringList::new(&[]);
        let cat_ids: Rc<RefCell<Vec<i64>>> = Rc::new(RefCell::new(Vec::new()));
        if let Ok(cats) = db.list_categories(Some(TransactionType::Expense)) {
            for cat in &cats {
                let icon_name = icon_theme::resolve_category_icon(&cat.icon, &cat.color);
                let entry = match &icon_name {
                    Some(icon) => format!("{}\t{}", icon, cat.name),
                    None => cat.name.clone(),
                };
                cat_model.append(&entry);
                cat_ids.borrow_mut().push(cat.id);
            }
        }
        let cat_row = adw::ComboRow::builder()
            .title("Assign category")
            .model(&cat_model)
            .build();
        cat_row.set_factory(Some(&Self::make_category_factory()));
        cat_row.set_list_factory(Some(&Self::make_category_factory()));
        form.add(&cat_row);

        // Update categories when type changes
        {
            let db_ref = db.clone();
            let cat_model = cat_model.clone();
            let cat_ids = cat_ids.clone();
            type_row.connect_selected_notify(move |row| {
                let txn_type = if row.selected() == 0 {
                    TransactionType::Expense
                } else {
                    TransactionType::Income
                };
                let n = cat_model.n_items();
                if n > 0 {
                    cat_model.splice(0, n, &[] as &[&str]);
                }
                cat_ids.borrow_mut().clear();
                if let Ok(cats) = db_ref.list_categories(Some(txn_type)) {
                    for cat in &cats {
                        let icon_name = icon_theme::resolve_category_icon(&cat.icon, &cat.color);
                        let entry = match &icon_name {
                            Some(icon) => format!("{}\t{}", icon, cat.name),
                            None => cat.name.clone(),
                        };
                        cat_model.append(&entry);
                        cat_ids.borrow_mut().push(cat.id);
                    }
                }
            });
        }

        let save_btn = gtk::Button::with_label("Save");
        save_btn.add_css_class("suggested-action");
        save_btn.add_css_class("pill");
        save_btn.set_halign(gtk::Align::Center);

        content.append(&form);
        content.append(&save_btn);
        toolbar.set_content(Some(&content));
        dialog.set_child(Some(&toolbar));

        {
            let db_ref = db.clone();
            let dialog_ref = dialog.clone();
            let group_ref = group.clone();
            let toast_ref = toast_overlay.clone();
            let cat_ids = cat_ids.clone();
            save_btn.connect_clicked(move |_| {
                let pattern = pattern_row.text().to_string();
                if pattern.trim().is_empty() {
                    let toast = adw::Toast::new("Please enter a pattern");
                    toast_ref.add_toast(toast);
                    return;
                }

                let field = if field_row.selected() == 0 { "note" } else { "payee" };
                let cat_idx = cat_row.selected() as usize;
                let ids = cat_ids.borrow();
                let category_id = match ids.get(cat_idx) {
                    Some(&id) => id,
                    None => {
                        let toast = adw::Toast::new("Please select a category");
                        toast_ref.add_toast(toast);
                        return;
                    }
                };

                match db_ref.insert_rule(field, pattern.trim(), category_id, 0) {
                    Ok(_) => {
                        dialog_ref.close();
                        Self::populate_rules(&db_ref, &group_ref, &toast_ref);
                        let toast = adw::Toast::new("Rule added");
                        toast_ref.add_toast(toast);
                    }
                    Err(e) => {
                        let toast = adw::Toast::new(&format!("Error: {}", e));
                        toast_ref.add_toast(toast);
                    }
                }
            });
        }

        dialog.present(Some(parent));
    }

    // -- Templates helpers --

    fn populate_templates(
        db: &Rc<Database>,
        group: &adw::PreferencesGroup,
        toast_overlay: &adw::ToastOverlay,
    ) {
        let mut rows_to_remove = Vec::new();
        let mut child = group.first_child();
        while let Some(widget) = child {
            let next = widget.next_sibling();
            Self::collect_action_rows_recursive(&widget, &mut rows_to_remove);
            child = next;
        }
        for row in rows_to_remove {
            if let Some(r) = row.downcast_ref::<adw::ActionRow>() {
                group.remove(r);
            }
        }

        if let Ok(templates) = db.list_templates() {
            for tmpl in &templates {
                let cat_name = db
                    .get_category(tmpl.category_id)
                    .map(|c| c.name)
                    .unwrap_or_else(|_| "Unknown".to_string());
                let amount_str = tmpl
                    .amount
                    .map(|a| format!("{:.2}", a))
                    .unwrap_or_else(|| "any".to_string());
                let subtitle = format!(
                    "{} {} - {}",
                    tmpl.transaction_type.as_str(),
                    amount_str,
                    cat_name
                );
                let row = adw::ActionRow::builder()
                    .title(&tmpl.name)
                    .subtitle(&subtitle)
                    .build();

                let del_btn = gtk::Button::from_icon_name("outlay-delete");
                del_btn.add_css_class("flat");
                del_btn.set_valign(gtk::Align::Center);
                del_btn.set_tooltip_text(Some("Delete template"));
                {
                    let tmpl_id = tmpl.id;
                    let db_ref = db.clone();
                    let group_ref = group.clone();
                    let toast_ref = toast_overlay.clone();
                    del_btn.connect_clicked(move |_| {
                        let _ = db_ref.delete_template(tmpl_id);
                        Self::populate_templates(&db_ref, &group_ref, &toast_ref);
                        let toast = adw::Toast::new("Template deleted");
                        toast_ref.add_toast(toast);
                    });
                }
                row.add_suffix(&del_btn);
                group.add(&row);
            }
            if templates.is_empty() {
                let placeholder = adw::ActionRow::builder()
                    .title("No templates yet")
                    .build();
                placeholder.add_css_class("dim-label");
                group.add(&placeholder);
            }
        }
    }

    fn show_add_template_dialog(
        parent: &gtk::Button,
        db: &Rc<Database>,
        group: &adw::PreferencesGroup,
        toast_overlay: &adw::ToastOverlay,
    ) {
        let dialog = adw::Dialog::builder()
            .title("Add Template")
            .content_width(400)
            .content_height(450)
            .build();

        let toolbar = adw::ToolbarView::new();
        toolbar.add_top_bar(&adw::HeaderBar::new());

        let content = gtk::Box::new(gtk::Orientation::Vertical, 16);
        content.set_margin_top(16);
        content.set_margin_bottom(16);
        content.set_margin_start(16);
        content.set_margin_end(16);

        let form = adw::PreferencesGroup::new();

        let name_row = adw::EntryRow::builder()
            .title("Template name")
            .build();
        form.add(&name_row);

        let type_model = gtk::StringList::new(&["Expense", "Income"]);
        let type_row = adw::ComboRow::builder()
            .title("Type")
            .model(&type_model)
            .build();
        form.add(&type_row);

        let amount_row = adw::EntryRow::builder()
            .title("Amount (optional)")
            .build();
        amount_row.set_input_purpose(gtk::InputPurpose::Number);
        crate::numpad::attach_numpad(&amount_row);
        form.add(&amount_row);

        let cat_model = gtk::StringList::new(&[]);
        let cat_ids: Rc<RefCell<Vec<i64>>> = Rc::new(RefCell::new(Vec::new()));
        if let Ok(cats) = db.list_categories(Some(TransactionType::Expense)) {
            for cat in &cats {
                let icon_name = icon_theme::resolve_category_icon(&cat.icon, &cat.color);
                let entry = match &icon_name {
                    Some(icon) => format!("{}\t{}", icon, cat.name),
                    None => cat.name.clone(),
                };
                cat_model.append(&entry);
                cat_ids.borrow_mut().push(cat.id);
            }
        }
        let cat_row = adw::ComboRow::builder()
            .title("Category")
            .model(&cat_model)
            .build();
        cat_row.set_factory(Some(&Self::make_category_factory()));
        cat_row.set_list_factory(Some(&Self::make_category_factory()));
        form.add(&cat_row);

        // Update categories when type changes
        {
            let db_ref = db.clone();
            let cat_model = cat_model.clone();
            let cat_ids = cat_ids.clone();
            type_row.connect_selected_notify(move |row| {
                let txn_type = if row.selected() == 0 {
                    TransactionType::Expense
                } else {
                    TransactionType::Income
                };
                let n = cat_model.n_items();
                if n > 0 {
                    cat_model.splice(0, n, &[] as &[&str]);
                }
                cat_ids.borrow_mut().clear();
                if let Ok(cats) = db_ref.list_categories(Some(txn_type)) {
                    for cat in &cats {
                        let icon_name = icon_theme::resolve_category_icon(&cat.icon, &cat.color);
                        let entry = match &icon_name {
                            Some(icon) => format!("{}\t{}", icon, cat.name),
                            None => cat.name.clone(),
                        };
                        cat_model.append(&entry);
                        cat_ids.borrow_mut().push(cat.id);
                    }
                }
            });
        }

        let payee_row = adw::EntryRow::builder()
            .title("Payee (optional)")
            .build();
        form.add(&payee_row);

        let note_row = adw::EntryRow::builder()
            .title("Note (optional)")
            .build();
        form.add(&note_row);

        let save_btn = gtk::Button::with_label("Save");
        save_btn.add_css_class("suggested-action");
        save_btn.add_css_class("pill");
        save_btn.set_halign(gtk::Align::Center);

        content.append(&form);
        content.append(&save_btn);

        let scroll = gtk::ScrolledWindow::builder()
            .hscrollbar_policy(gtk::PolicyType::Never)
            .child(&content)
            .build();

        toolbar.set_content(Some(&scroll));
        dialog.set_child(Some(&toolbar));

        let base_currency = db
            .get_setting("base_currency")
            .ok()
            .flatten()
            .unwrap_or_else(|| "USD".to_string());

        {
            let db_ref = db.clone();
            let dialog_ref = dialog.clone();
            let group_ref = group.clone();
            let toast_ref = toast_overlay.clone();
            let cat_ids = cat_ids.clone();
            let base_currency = base_currency.clone();
            save_btn.connect_clicked(move |_| {
                let name = name_row.text().to_string();
                if name.trim().is_empty() {
                    let toast = adw::Toast::new("Please enter a template name");
                    toast_ref.add_toast(toast);
                    return;
                }

                let txn_type = if type_row.selected() == 0 {
                    TransactionType::Expense
                } else {
                    TransactionType::Income
                };

                let amount_text = amount_row.text();
                let amount: Option<f64> = if amount_text.is_empty() {
                    None
                } else {
                    amount_text.parse().ok()
                };

                let cat_idx = cat_row.selected() as usize;
                let ids = cat_ids.borrow();
                let category_id = match ids.get(cat_idx) {
                    Some(&id) => id,
                    None => {
                        let toast = adw::Toast::new("Please select a category");
                        toast_ref.add_toast(toast);
                        return;
                    }
                };

                let payee_text = payee_row.text();
                let payee = if payee_text.is_empty() { None } else { Some(payee_text.to_string()) };
                let note_text = note_row.text();
                let note = if note_text.is_empty() { None } else { Some(note_text.to_string()) };

                match db_ref.insert_template(
                    name.trim(),
                    amount,
                    txn_type,
                    category_id,
                    &base_currency,
                    payee.as_deref(),
                    note.as_deref(),
                    None,
                ) {
                    Ok(_) => {
                        dialog_ref.close();
                        Self::populate_templates(&db_ref, &group_ref, &toast_ref);
                        let toast = adw::Toast::new("Template added");
                        toast_ref.add_toast(toast);
                    }
                    Err(e) => {
                        let toast = adw::Toast::new(&format!("Error: {}", e));
                        toast_ref.add_toast(toast);
                    }
                }
            });
        }

        dialog.present(Some(parent));
    }

    // -- Subscription category helpers --

    fn populate_subscription_categories(
        db: &Rc<Database>,
        expander: &adw::ExpanderRow,
        toast_overlay: &adw::ToastOverlay,
    ) {
        // Remove existing rows from expander
        let mut children = Vec::new();
        let mut child = expander.first_child();
        while let Some(w) = child {
            let next = w.next_sibling();
            if let Some(row) = w.downcast_ref::<adw::ActionRow>() {
                children.push(row.clone());
            }
            child = next;
        }
        for row in &children {
            expander.remove(row);
        }

        if let Ok(cats) = db.list_subscription_categories() {
            for cat in &cats {
                let row = adw::ActionRow::builder()
                    .title(&cat.name)
                    .build();

                let icon_name = icon_theme::resolve_category_icon(&cat.icon, &cat.color);
                if let Some(name) = &icon_name {
                    let icon = gtk::Image::from_icon_name(name);
                    icon.set_pixel_size(24);
                    row.add_prefix(&icon);
                }

                let del_btn = gtk::Button::from_icon_name("outlay-delete");
                del_btn.add_css_class("flat");
                del_btn.set_valign(gtk::Align::Center);
                del_btn.set_tooltip_text(Some("Delete"));
                {
                    let cat_id = cat.id;
                    let db_ref = db.clone();
                    let expander_ref = expander.clone();
                    let toast_ref = toast_overlay.clone();
                    del_btn.connect_clicked(move |_| {
                        let _ = db_ref.delete_subscription_category(cat_id);
                        Self::populate_subscription_categories(&db_ref, &expander_ref, &toast_ref);
                        toast_ref.add_toast(adw::Toast::new("Category deleted"));
                    });
                }
                row.add_suffix(&del_btn);
                expander.add_row(&row);
            }
        }
    }

    fn show_add_subscription_category_dialog(
        parent: &gtk::Button,
        db: &Rc<Database>,
        expander: &adw::ExpanderRow,
        toast_overlay: &adw::ToastOverlay,
    ) {
        let dialog = adw::Dialog::builder()
            .title("Add Subscription Category")
            .content_width(400)
            .content_height(350)
            .build();

        let toolbar = adw::ToolbarView::new();
        toolbar.add_top_bar(&adw::HeaderBar::new());

        let content = gtk::Box::new(gtk::Orientation::Vertical, 16);
        content.set_margin_top(16);
        content.set_margin_bottom(16);
        content.set_margin_start(16);
        content.set_margin_end(16);

        let form = adw::PreferencesGroup::new();

        let name_row = adw::EntryRow::builder()
            .title("Name")
            .build();
        form.add(&name_row);

        let icon_row = adw::ActionRow::builder()
            .title("Icon")
            .subtitle("None")
            .activatable(true)
            .build();
        let chosen_icon: Rc<RefCell<Option<String>>> = Rc::new(RefCell::new(None));
        let icon_preview = gtk::Image::new();
        icon_preview.set_pixel_size(24);
        icon_row.add_suffix(&icon_preview);
        {
            let chosen_icon = chosen_icon.clone();
            let icon_row_ref = icon_row.clone();
            let icon_preview = icon_preview.clone();
            let dialog_ref = dialog.clone();
            icon_row.connect_activated(move |_| {
                Self::show_icon_picker_dialog(
                    &dialog_ref, &chosen_icon, &icon_preview, &icon_row_ref,
                );
            });
        }
        form.add(&icon_row);

        let color_btn = gtk::ColorDialogButton::new(Some(gtk::ColorDialog::new()));
        color_btn.set_valign(gtk::Align::Center);
        let default_color = gdk::RGBA::parse("#95a5a6").unwrap_or(gdk::RGBA::BLACK);
        color_btn.set_rgba(&default_color);
        let color_row = adw::ActionRow::builder()
            .title("Color")
            .build();
        color_row.add_suffix(&color_btn);
        form.add(&color_row);

        let save_btn = gtk::Button::with_label("Save");
        save_btn.add_css_class("suggested-action");
        save_btn.add_css_class("pill");
        save_btn.set_halign(gtk::Align::Center);

        content.append(&form);
        content.append(&save_btn);
        toolbar.set_content(Some(&content));
        dialog.set_child(Some(&toolbar));

        {
            let db_ref = db.clone();
            let dialog_ref = dialog.clone();
            let expander_ref = expander.clone();
            let toast_ref = toast_overlay.clone();
            let chosen_icon = chosen_icon.clone();
            save_btn.connect_clicked(move |_| {
                let name = name_row.text().to_string();
                if name.trim().is_empty() {
                    toast_ref.add_toast(adw::Toast::new("Please enter a name"));
                    return;
                }

                let icon = chosen_icon.borrow().clone();
                let rgba = color_btn.rgba();
                let color = Self::rgba_to_hex(&rgba);

                match db_ref.insert_subscription_category(
                    name.trim(),
                    icon.as_deref(),
                    Some(&color),
                ) {
                    Ok(_) => {
                        dialog_ref.close();
                        Self::populate_subscription_categories(&db_ref, &expander_ref, &toast_ref);
                        toast_ref.add_toast(adw::Toast::new("Category added"));
                    }
                    Err(e) => {
                        toast_ref.add_toast(adw::Toast::new(&format!("Error: {}", e)));
                    }
                }
            });
        }

        dialog.present(Some(parent));
    }

    fn collect_action_rows_recursive(widget: &gtk::Widget, rows: &mut Vec<gtk::Widget>) {
        let mut child = widget.first_child();
        while let Some(w) = child {
            let next = w.next_sibling();
            if w.downcast_ref::<adw::ActionRow>().is_some() {
                rows.push(w.clone());
            } else {
                Self::collect_action_rows_recursive(&w, rows);
            }
            child = next;
        }
    }

    fn make_category_factory() -> gtk::SignalListItemFactory {
        let factory = gtk::SignalListItemFactory::new();
        factory.connect_setup(|_, item| {
            let item = item.downcast_ref::<gtk::ListItem>().unwrap();
            let hbox = gtk::Box::new(gtk::Orientation::Horizontal, 8);
            let icon = gtk::Image::new();
            icon.set_pixel_size(20);
            let label = gtk::Label::new(None);
            hbox.append(&icon);
            hbox.append(&label);
            item.set_child(Some(&hbox));
        });
        factory.connect_bind(|_, item| {
            let item = item.downcast_ref::<gtk::ListItem>().unwrap();
            let string_obj = item.item().and_downcast::<gtk::StringObject>().unwrap();
            let text = string_obj.string();
            let hbox = item.child().and_downcast::<gtk::Box>().unwrap();
            let icon = hbox.first_child().and_downcast::<gtk::Image>().unwrap();
            let label = icon.next_sibling().and_downcast::<gtk::Label>().unwrap();
            if let Some((icon_name, name)) = text.split_once('\t') {
                icon.set_icon_name(Some(icon_name));
                icon.set_visible(true);
                label.set_label(name);
            } else {
                icon.set_visible(false);
                label.set_label(&text);
            }
        });
        factory
    }
}
