use adw::prelude::*;
use chrono::Datelike;
use gtk::{gio, glib};
use outlay_core::db::Database;
use outlay_core::exchange::ExchangeRateService;
use outlay_core::models::{NewTransaction, TransactionType};
use outlay_core::ocr;
use std::cell::{Cell, RefCell};
use std::rc::Rc;

use crate::edit_dialog;
use crate::icon_theme;

type PendingAttachment = (String, String, Vec<u8>);

enum SymbolPosition {
    Prefix,
    Suffix,
}

fn currency_info(code: &str) -> (&'static str, SymbolPosition) {
    match code.to_uppercase().as_str() {
        "USD" => ("$", SymbolPosition::Prefix),
        "EUR" => ("\u{20ac}", SymbolPosition::Suffix),    // euro sign
        "GBP" => ("\u{00a3}", SymbolPosition::Prefix),    // pound sign
        "JPY" => ("\u{00a5}", SymbolPosition::Prefix),    // yen sign
        "CAD" => ("C$", SymbolPosition::Prefix),
        "AUD" => ("A$", SymbolPosition::Prefix),
        "CHF" => ("CHF", SymbolPosition::Prefix),
        "CNY" => ("\u{00a5}", SymbolPosition::Prefix),    // yuan sign
        "INR" => ("\u{20b9}", SymbolPosition::Prefix),    // rupee sign
        "BRL" => ("R$", SymbolPosition::Prefix),
        "MXN" => ("$", SymbolPosition::Prefix),
        "KRW" => ("\u{20a9}", SymbolPosition::Prefix),    // won sign
        "SGD" => ("S$", SymbolPosition::Prefix),
        "HKD" => ("HK$", SymbolPosition::Prefix),
        "SEK" => ("kr", SymbolPosition::Suffix),
        "NOK" => ("kr", SymbolPosition::Suffix),
        "DKK" => ("kr", SymbolPosition::Suffix),
        "PLN" => ("zl", SymbolPosition::Suffix),
        "ZAR" => ("R", SymbolPosition::Prefix),
        "TRY" => ("\u{20ba}", SymbolPosition::Prefix),    // lira sign
        "RUB" => ("\u{20bd}", SymbolPosition::Suffix),    // ruble sign
        "NZD" => ("$", SymbolPosition::Prefix),
        "THB" => ("\u{0e3f}", SymbolPosition::Prefix),    // baht sign
        "TWD" => ("NT$", SymbolPosition::Prefix),
        "CZK" => ("Kc", SymbolPosition::Suffix),
        "HUF" => ("Ft", SymbolPosition::Suffix),
        "ILS" => ("\u{20aa}", SymbolPosition::Prefix),    // shekel sign
        "PHP" => ("\u{20b1}", SymbolPosition::Prefix),    // peso sign
        "MYR" => ("RM", SymbolPosition::Prefix),
        "IDR" => ("Rp", SymbolPosition::Prefix),
        _ => ("$", SymbolPosition::Prefix),
    }
}

pub struct LogView {
    pub container: gtk::Box,
    pub toast_overlay: adw::ToastOverlay,
    db: Rc<Database>,
    category_model: gtk::StringList,
    category_ids: Rc<RefCell<Vec<i64>>>,
    expense_btn: gtk::ToggleButton,
    income_btn: gtk::ToggleButton,
    amount_entry: gtk::Entry,
    category_row: adw::ComboRow,
    currency_row: adw::ComboRow,
    currency_codes: Rc<Vec<String>>,
}

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

        let clamp = adw::Clamp::new();
        clamp.set_maximum_size(600);
        clamp.set_margin_top(32);
        clamp.set_margin_bottom(32);
        clamp.set_margin_start(16);
        clamp.set_margin_end(16);

        let inner = gtk::Box::new(gtk::Orientation::Vertical, 32);

        let category_ids: Rc<RefCell<Vec<i64>>> = Rc::new(RefCell::new(Vec::new()));
        let exchange_rate: Rc<Cell<f64>> = Rc::new(Cell::new(1.0));

        // Get base currency from settings
        let base_currency = db
            .get_setting("base_currency")
            .ok()
            .flatten()
            .unwrap_or_else(|| "USD".to_string());

        // Currency codes list - base currency pinned to top
        let mut currencies = ExchangeRateService::supported_currencies();
        if let Some(pos) = currencies.iter().position(|(c, _)| c.eq_ignore_ascii_case(&base_currency)) {
            let base = currencies.remove(pos);
            currencies.insert(0, base);
        }
        let currency_codes: Vec<&str> = currencies.iter().map(|(code, _)| *code).collect();
        let base_idx = 0usize;

        // -- Monthly summary cards --
        let today = chrono::Local::now().date_naive();
        let summary_year = Rc::new(Cell::new(today.year()));
        let summary_month = Rc::new(Cell::new(today.month()));

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

        let income_card = Self::make_summary_card("INCOME");
        let expense_card = Self::make_summary_card("EXPENSES");
        let net_card = Self::make_summary_card("NET");

        summary_box.append(&income_card.0);
        summary_box.append(&expense_card.0);
        summary_box.append(&net_card.0);

        // Store refs to amount labels for refresh
        let income_amount_label = income_card.1;
        let expense_amount_label = expense_card.1;
        let net_amount_label = net_card.1;

        Self::refresh_summary(
            &db, &income_amount_label, &expense_amount_label, &net_amount_label,
            &base_currency, summary_year.get(), summary_month.get(),
        );

        // Summary month navigation
        let summary_nav = {
            let sy = summary_year.clone();
            let sm = summary_month.clone();
            let il = income_amount_label.clone();
            let el = expense_amount_label.clone();
            let nl = net_amount_label.clone();
            let bc = base_currency.clone();
            let db_nav = db.clone();
            crate::month_nav::MonthNav::new(move |year, month| {
                sy.set(year);
                sm.set(month);
                Self::refresh_summary(&db_nav, &il, &el, &nl, &bc, year, month);
            })
        };

        // -- Transaction type toggle --
        let type_box = gtk::Box::new(gtk::Orientation::Horizontal, 0);
        type_box.add_css_class("linked");
        type_box.add_css_class("type-toggle");
        type_box.set_halign(gtk::Align::Center);

        let expense_btn = gtk::ToggleButton::with_label("Expense");
        expense_btn.set_active(true);
        expense_btn.set_hexpand(true);

        let income_btn = gtk::ToggleButton::with_label("Income");
        income_btn.set_group(Some(&expense_btn));
        income_btn.set_hexpand(true);

        type_box.append(&expense_btn);
        type_box.append(&income_btn);

        // -- Hero amount input with currency symbol --
        let amount_box = gtk::Box::new(gtk::Orientation::Horizontal, 4);
        amount_box.set_halign(gtk::Align::Center);
        amount_box.set_valign(gtk::Align::Center);

        let prefix_label = gtk::Label::new(None);
        prefix_label.add_css_class("currency-symbol");
        prefix_label.add_css_class("amount-display");

        let suffix_label = gtk::Label::new(None);
        suffix_label.add_css_class("currency-symbol");
        suffix_label.add_css_class("amount-display");

        let amount_entry = gtk::Entry::builder()
            .placeholder_text("0.00")
            .input_purpose(gtk::InputPurpose::Number)
            .xalign(0.5)
            .width_chars(4)
            .build();
        amount_entry.add_css_class("amount-hero");

        // Dynamically resize entry width to hug content
        {
            let entry_ref = amount_entry.clone();
            amount_entry.connect_changed(move |e| {
                let len = e.text().len();
                let chars = if len < 4 { 4 } else { len as i32 + 1 };
                entry_ref.set_width_chars(chars);
            });
        }

        amount_box.append(&prefix_label);
        amount_box.append(&amount_entry);
        amount_box.append(&suffix_label);

        // Live expression result label (e.g. "= 20.00")
        let expr_label = gtk::Label::new(None);
        expr_label.add_css_class("dim-label");
        expr_label.add_css_class("caption");
        expr_label.set_visible(false);
        {
            let expr_label_ref = expr_label.clone();
            amount_entry.connect_changed(move |e| {
                let text = e.text();
                let has_operator = text.contains('+') || text.contains('-')
                    || text.contains('*') || text.contains('/');
                if has_operator {
                    if let Some(val) = outlay_core::expr::eval_expr(&text) {
                        expr_label_ref.set_label(&format!("= {:.2}", val));
                        expr_label_ref.set_visible(true);
                    } else {
                        expr_label_ref.set_visible(false);
                    }
                } else {
                    expr_label_ref.set_visible(false);
                }
            });
        }

        // Set initial currency symbol
        {
            let initial_code = currency_codes.get(base_idx).copied().unwrap_or("USD");
            let (symbol, pos) = currency_info(initial_code);
            match pos {
                SymbolPosition::Prefix => {
                    prefix_label.set_label(symbol);
                    prefix_label.set_visible(true);
                    suffix_label.set_visible(false);
                }
                SymbolPosition::Suffix => {
                    suffix_label.set_label(symbol);
                    suffix_label.set_visible(true);
                    prefix_label.set_visible(false);
                }
            }
        }

        // Inline validation: red outline when amount is empty/invalid on focus loss
        {
            let focus_ctl = gtk::EventControllerFocus::new();
            let entry_ref = amount_entry.clone();
            focus_ctl.connect_leave(move |_| {
                let text = entry_ref.text();
                if !text.is_empty() {
                    match outlay_core::expr::eval_expr(&text) {
                        Some(v) if v > 0.0 => entry_ref.remove_css_class("error"),
                        _ => entry_ref.add_css_class("error"),
                    }
                }
            });
            let entry_ref2 = amount_entry.clone();
            amount_entry.connect_changed(move |_| {
                entry_ref2.remove_css_class("error");
            });
            amount_entry.add_controller(focus_ctl);
        }

        // -- Number keypad popover (attached to entry) --
        crate::numpad::attach_numpad(&amount_entry);

        // -- Natural language quick entry --
        let nl_group = adw::PreferencesGroup::builder()
            .title("QUICK ENTRY")
            .build();
        let nl_entry = adw::EntryRow::builder()
            .title("Quick entry")
            .build();
        nl_entry.set_text("");
        nl_group.add(&nl_entry);

        let nl_preview = gtk::Box::new(gtk::Orientation::Horizontal, 8);
        nl_preview.set_margin_start(12);
        nl_preview.set_margin_end(12);
        nl_preview.set_margin_top(4);
        nl_preview.set_margin_bottom(4);
        nl_preview.set_visible(false);

        let nl_icon = gtk::Image::from_icon_name("folder-symbolic");
        nl_icon.set_pixel_size(20);
        let nl_cat_label = gtk::Label::new(None);
        nl_cat_label.add_css_class("caption");
        let nl_amount_label = gtk::Label::new(None);
        nl_amount_label.add_css_class("heading");
        let nl_detail_label = gtk::Label::new(None);
        nl_detail_label.add_css_class("dim-label");
        nl_detail_label.set_hexpand(true);
        nl_detail_label.set_halign(gtk::Align::Start);
        let nl_add_btn = gtk::Button::with_label("Add");
        nl_add_btn.add_css_class("suggested-action");
        nl_add_btn.add_css_class("pill");

        nl_preview.append(&nl_icon);
        nl_preview.append(&nl_cat_label);
        nl_preview.append(&nl_amount_label);
        nl_preview.append(&nl_detail_label);
        nl_preview.append(&nl_add_btn);
        nl_group.add(&nl_preview);

        // NL parse state
        let nl_parsed: Rc<RefCell<Option<outlay_core::models::ParsedTransaction>>> = Rc::new(RefCell::new(None));
        let nl_debounce: Rc<Cell<u32>> = Rc::new(Cell::new(0));

        // Wire NL entry text change with debounce
        {
            let db_ref = db.clone();
            let preview_ref = nl_preview.clone();
            let cat_label = nl_cat_label.clone();
            let amt_label = nl_amount_label.clone();
            let detail_label = nl_detail_label.clone();
            let icon_ref = nl_icon.clone();
            let parsed_ref = nl_parsed.clone();
            let debounce_ref = nl_debounce.clone();
            nl_entry.connect_changed(move |entry| {
                let text = entry.text().to_string();
                let id = debounce_ref.get().wrapping_add(1);
                debounce_ref.set(id);

                let db_c = db_ref.clone();
                let preview_c = preview_ref.clone();
                let cat_c = cat_label.clone();
                let amt_c = amt_label.clone();
                let det_c = detail_label.clone();
                let icon_c = icon_ref.clone();
                let parsed_c = parsed_ref.clone();
                let deb_c = debounce_ref.clone();
                glib::timeout_add_local_once(std::time::Duration::from_millis(300), move || {
                    if deb_c.get() != id { return; }
                    let cats = db_c.list_categories(None).unwrap_or_default();
                    let result = outlay_core::nlp::parse_transaction(&text, &cats);
                    if let Some(ref parsed) = result {
                        let cat_name = parsed.category_name.as_deref().unwrap_or("Uncategorized");
                        cat_c.set_label(cat_name);
                        amt_c.set_label(&format!("{:.2}", parsed.amount));
                        let mut detail_parts = Vec::new();
                        if let Some(ref p) = parsed.payee {
                            detail_parts.push(format!("at {}", p));
                        }
                        if let Some(ref n) = parsed.note {
                            detail_parts.push(n.clone());
                        }
                        det_c.set_label(&detail_parts.join(" - "));

                        // Try to find icon for the category
                        if let Some(cid) = parsed.category_id {
                            if let Ok(cat) = db_c.get_category(cid) {
                                let resolved = crate::icon_theme::resolve_category_icon(&cat.icon, &cat.color);
                                if let Some(name) = resolved {
                                    icon_c.set_icon_name(Some(&name));
                                }
                            }
                        }
                        preview_c.set_visible(true);
                    } else {
                        preview_c.set_visible(false);
                    }
                    *parsed_c.borrow_mut() = result;
                });
            });
        }

        // Wire NL add button
        {
            let db_ref = db.clone();
            let parsed_ref = nl_parsed.clone();
            let entry_ref = nl_entry.clone();
            let preview_ref = nl_preview.clone();
            let toast_ref = toast_overlay.clone();
            nl_add_btn.connect_clicked(move |_| {
                let parsed = parsed_ref.borrow().clone();
                if let Some(parsed) = parsed {
                    let base_currency = db_ref.get_setting("base_currency")
                        .ok().flatten()
                        .unwrap_or_else(|| "USD".to_string());
                    let cat_id = parsed.category_id.unwrap_or_else(|| {
                        db_ref.list_categories(Some(outlay_core::models::TransactionType::Expense))
                            .ok()
                            .and_then(|c| c.first().map(|cat| cat.id))
                            .unwrap_or(1)
                    });
                    let today = chrono::Local::now().date_naive();
                    let txn = outlay_core::models::NewTransaction {
                        amount: parsed.amount,
                        transaction_type: parsed.transaction_type,
                        category_id: cat_id,
                        currency: base_currency.clone(),
                        exchange_rate: 1.0,
                        note: parsed.note,
                        date: today,
                        recurring_id: None,
                        payee: parsed.payee,
                    };
                    if db_ref.insert_transaction(&txn).is_ok() {
                        let cat_name = parsed.category_name.as_deref().unwrap_or("Unknown");
                        let toast = adw::Toast::new(&format!("Added {:.2} {} to {}", parsed.amount, base_currency, cat_name));
                        toast_ref.add_toast(toast);
                        entry_ref.set_text("");
                        preview_ref.set_visible(false);
                    }
                }
            });
        }

        // Wire Enter key on NL entry
        {
            let add_btn_ref = nl_add_btn.clone();
            nl_entry.connect_apply(move |_| {
                add_btn_ref.emit_clicked();
            });
        }

        // -- Form group --
        let form_group = adw::PreferencesGroup::builder()
            .title("NEW TRANSACTION")
            .build();

        // Category (first in form group for tab order)
        let category_model = gtk::StringList::new(&[]);
        let category_row = adw::ComboRow::builder()
            .title("Category")
            .model(&category_model)
            .build();
        category_row.set_factory(Some(&Self::make_category_factory()));
        category_row.set_list_factory(Some(&Self::make_category_factory()));

        Self::populate_categories_from_db(&db, &category_model, &category_ids, TransactionType::Expense);

        form_group.add(&category_row);

        // Currency
        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_row = adw::ComboRow::builder()
            .title("Currency")
            .model(&currency_model)
            .selected(base_idx as u32)
            .build();
        form_group.add(&currency_row);

        // Exchange rate info label
        let rate_label = gtk::Label::new(None);
        rate_label.add_css_class("dim-label");
        rate_label.add_css_class("caption");
        rate_label.set_halign(gtk::Align::Start);
        rate_label.set_margin_start(16);
        rate_label.set_visible(false);

        // Date picker
        let date_fmt = db.get_date_format_string();
        let today_date = chrono::Local::now().date_naive();
        let selected_date = Rc::new(Cell::new(today_date));
        let date_str = today_date.format(&date_fmt).to_string();

        let date_label = gtk::Label::new(Some(&date_str));
        date_label.set_halign(gtk::Align::End);
        date_label.set_hexpand(true);

        let calendar = gtk::Calendar::new();
        let popover = gtk::Popover::new();
        popover.set_child(Some(&calendar));

        let date_menu_btn = gtk::MenuButton::new();
        date_menu_btn.set_popover(Some(&popover));
        let calendar_icon = gtk::Image::from_icon_name("outlay-calendar");
        calendar_icon.set_pixel_size(28);
        date_menu_btn.set_child(Some(&calendar_icon));
        date_menu_btn.add_css_class("flat");
        date_menu_btn.set_tooltip_text(Some("Pick date"));

        let today_btn = gtk::Button::with_label("Today");
        today_btn.add_css_class("flat");
        today_btn.add_css_class("caption");
        let yesterday_btn = gtk::Button::with_label("Yesterday");
        yesterday_btn.add_css_class("flat");
        yesterday_btn.add_css_class("caption");

        {
            let dl = date_label.clone();
            let cal = calendar.clone();
            let sd = selected_date.clone();
            let fmt = date_fmt.clone();
            today_btn.connect_clicked(move |_| {
                let now_date = chrono::Local::now().date_naive();
                sd.set(now_date);
                dl.set_label(&now_date.format(&fmt).to_string());
                let now = glib::DateTime::now_local().unwrap();
                cal.set_year(now.year());
                cal.set_month(now.month() - 1);
                cal.set_day(now.day_of_month());
            });
        }
        {
            let dl = date_label.clone();
            let cal = calendar.clone();
            let sd = selected_date.clone();
            let fmt = date_fmt.clone();
            yesterday_btn.connect_clicked(move |_| {
                let today = chrono::Local::now().date_naive();
                let yday = today - chrono::Duration::days(1);
                sd.set(yday);
                dl.set_label(&yday.format(&fmt).to_string());
                cal.set_year(yday.year());
                cal.set_month(yday.month0() as i32);
                cal.set_day(yday.day() as i32);
            });
        }

        let date_box = gtk::Box::new(gtk::Orientation::Horizontal, 4);
        date_box.append(&today_btn);
        date_box.append(&yesterday_btn);
        date_box.append(&date_label);
        date_box.append(&date_menu_btn);

        let date_row = adw::ActionRow::builder()
            .title("Date")
            .build();
        date_row.add_suffix(&date_box);
        date_row.set_activatable_widget(Some(&date_menu_btn));

        let date_label_ref = date_label.clone();
        let popover_ref = popover.clone();
        let sd = selected_date.clone();
        let fmt = date_fmt.clone();
        calendar.connect_day_selected(move |cal| {
            let dt = cal.date();
            if let Some(d) = chrono::NaiveDate::from_ymd_opt(dt.year(), dt.month() as u32, dt.day_of_month() as u32) {
                sd.set(d);
                date_label_ref.set_label(&d.format(&fmt).to_string());
            }
            popover_ref.popdown();
        });

        form_group.add(&date_row);

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

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

        // Tags
        let tags_row = adw::EntryRow::builder()
            .title("Tags (comma-separated)")
            .build();
        form_group.add(&tags_row);

        // Split toggle
        let split_switch = adw::SwitchRow::builder()
            .title("Split across categories")
            .build();
        form_group.add(&split_switch);

        // Split entries section (hidden until split mode is on)
        let split_box = gtk::Box::new(gtk::Orientation::Vertical, 8);
        split_box.set_visible(false);

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

        let split_remaining = gtk::Label::new(Some("Remaining: 0.00"));
        split_remaining.add_css_class("caption");
        split_remaining.set_halign(gtk::Align::End);
        split_remaining.set_margin_end(8);

        let add_split_btn = gtk::Button::with_label("Add Split");
        add_split_btn.add_css_class("flat");
        add_split_btn.set_halign(gtk::Align::Start);

        split_box.append(&split_list);
        split_box.append(&split_remaining);
        split_box.append(&add_split_btn);

        // Track split row widgets: (ListBoxRow, category_ids, DropDown, Entry)
        type SplitRow = (gtk::ListBoxRow, Vec<i64>, gtk::DropDown, gtk::Entry);
        let split_entries: Rc<RefCell<Vec<SplitRow>>> = Rc::new(RefCell::new(Vec::new()));

        // Helper: build one split entry row
        let build_split_entry = {
            let db_se = db.clone();
            let split_list_se = split_list.clone();
            let split_entries_se = split_entries.clone();
            let split_remaining_se = split_remaining.clone();
            let amount_entry_se = amount_entry.clone();
            move |txn_type: TransactionType| {
                let cats = db_se.list_categories(Some(txn_type)).unwrap_or_default();
                let cat_ids: Vec<i64> = cats.iter().map(|c| c.id).collect();
                let labels: Vec<String> = 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> = labels.iter().map(|s| s.as_str()).collect();
                let model = gtk::StringList::new(&label_refs);
                let dropdown = gtk::DropDown::new(Some(model), gtk::Expression::NONE);
                dropdown.set_factory(Some(&crate::category_combo::make_category_factory()));
                dropdown.set_list_factory(Some(&crate::category_combo::make_category_factory()));
                dropdown.set_hexpand(true);

                let amt_entry = gtk::Entry::new();
                amt_entry.set_placeholder_text(Some("0.00"));
                amt_entry.set_input_purpose(gtk::InputPurpose::Number);
                amt_entry.set_width_chars(8);

                let del_btn = gtk::Button::from_icon_name("outlay-delete");
                del_btn.add_css_class("flat");
                del_btn.add_css_class("circular");
                del_btn.set_valign(gtk::Align::Center);

                let hbox = gtk::Box::new(gtk::Orientation::Horizontal, 8);
                hbox.set_margin_start(12);
                hbox.set_margin_end(8);
                hbox.set_margin_top(6);
                hbox.set_margin_bottom(6);
                hbox.append(&dropdown);
                hbox.append(&amt_entry);
                hbox.append(&del_btn);

                let row = gtk::ListBoxRow::new();
                row.set_child(Some(&hbox));
                row.set_activatable(false);

                split_list_se.append(&row);

                // Update remaining when amount changes
                {
                    let entries_ref = split_entries_se.clone();
                    let remaining_ref = split_remaining_se.clone();
                    let total_ref = amount_entry_se.clone();
                    amt_entry.connect_changed(move |_| {
                        Self::update_split_remaining(&total_ref, &entries_ref, &remaining_ref);
                    });
                }

                // Delete button removes this split row
                {
                    let list_del = split_list_se.clone();
                    let entries_del = split_entries_se.clone();
                    let row_del = row.clone();
                    let remaining_del = split_remaining_se.clone();
                    let total_del = amount_entry_se.clone();
                    del_btn.connect_clicked(move |_| {
                        list_del.remove(&row_del);
                        entries_del.borrow_mut().retain(|(r, _, _, _)| r != &row_del);
                        Self::update_split_remaining(&total_del, &entries_del, &remaining_del);
                    });
                }

                split_entries_se.borrow_mut().push((row, cat_ids, dropdown, amt_entry));
            }
        };

        // Wire split switch
        {
            let cat_row_ref = category_row.clone();
            let split_box_ref = split_box.clone();
            let split_list_ref = split_list.clone();
            let entries_ref = split_entries.clone();
            let expense_ref = expense_btn.clone();
            let build_entry = build_split_entry.clone();
            split_switch.connect_active_notify(move |sw| {
                let active = sw.is_active();
                cat_row_ref.set_visible(!active);
                split_box_ref.set_visible(active);
                if active && entries_ref.borrow().is_empty() {
                    // Add two initial split rows
                    let tt = if expense_ref.is_active() { TransactionType::Expense } else { TransactionType::Income };
                    build_entry(tt);
                    build_entry(tt);
                }
                if !active {
                    // Clear splits
                    while let Some(child) = split_list_ref.first_child() {
                        split_list_ref.remove(&child.downcast::<gtk::ListBoxRow>().unwrap());
                    }
                    entries_ref.borrow_mut().clear();
                }
            });
        }

        // Wire "Add Split" button
        {
            let expense_ref = expense_btn.clone();
            let build_entry = build_split_entry.clone();
            add_split_btn.connect_clicked(move |_| {
                let tt = if expense_ref.is_active() { TransactionType::Expense } else { TransactionType::Income };
                build_entry(tt);
            });
        }

        // Auto-categorization: when note or payee changes, try to match a rule
        {
            let db_ac = db.clone();
            let cat_row_ac = category_row.clone();
            let ids_ac = category_ids.clone();
            let payee_ac = payee_row.clone();
            let toast_ac = toast_overlay.clone();
            note_row.connect_changed(move |nr| {
                let note_text = nr.text();
                let payee_text = payee_ac.text();
                let note_opt = if note_text.is_empty() { None } else { Some(note_text.as_str()) };
                let payee_opt = if payee_text.is_empty() { None } else { Some(payee_text.as_str()) };
                if let Ok(Some(cat_id)) = db_ac.match_category(note_opt, payee_opt) {
                    let ids = ids_ac.borrow();
                    if let Some(pos) = ids.iter().position(|&id| id == cat_id) {
                        if cat_row_ac.selected() != pos as u32 {
                            cat_row_ac.set_selected(pos as u32);
                            let toast = adw::Toast::new("Category matched by rule");
                            toast.set_timeout(5);
                            toast_ac.add_toast(toast);
                        }
                    }
                }
            });
        }
        {
            let db_ac = db.clone();
            let cat_row_ac = category_row.clone();
            let ids_ac = category_ids.clone();
            let note_ac = note_row.clone();
            let toast_ac = toast_overlay.clone();
            payee_row.connect_changed(move |pr| {
                let payee_text = pr.text();
                let note_text = note_ac.text();
                let note_opt = if note_text.is_empty() { None } else { Some(note_text.as_str()) };
                let payee_opt = if payee_text.is_empty() { None } else { Some(payee_text.as_str()) };
                if let Ok(Some(cat_id)) = db_ac.match_category(note_opt, payee_opt) {
                    let ids = ids_ac.borrow();
                    if let Some(pos) = ids.iter().position(|&id| id == cat_id) {
                        if cat_row_ac.selected() != pos as u32 {
                            cat_row_ac.set_selected(pos as u32);
                            let toast = adw::Toast::new("Category matched by rule");
                            toast.set_timeout(5);
                            toast_ac.add_toast(toast);
                        }
                    }
                }
            });
        }

        // -- Attachment UI --
        let pending_attachments: Rc<RefCell<Vec<PendingAttachment>>> =
            Rc::new(RefCell::new(Vec::new()));

        let attach_box = gtk::Box::new(gtk::Orientation::Vertical, 8);

        // Empty state: dashed drop-zone button
        let attach_placeholder = gtk::Button::new();
        attach_placeholder.add_css_class("flat");
        attach_placeholder.add_css_class("attach-drop-zone");
        {
            let content = gtk::Box::new(gtk::Orientation::Vertical, 6);
            content.set_margin_top(20);
            content.set_margin_bottom(20);
            content.set_halign(gtk::Align::Center);
            let icon = gtk::Image::from_icon_name("mail-attachment-symbolic");
            icon.set_pixel_size(24);
            icon.add_css_class("dim-label");
            let label = gtk::Label::new(Some("Attach receipt"));
            label.add_css_class("dim-label");
            label.add_css_class("caption");
            content.append(&icon);
            content.append(&label);
            attach_placeholder.set_child(Some(&content));
        }

        // Thumbnails state: flow box (hidden until first attachment)
        let attach_flow = gtk::FlowBox::new();
        attach_flow.set_selection_mode(gtk::SelectionMode::None);
        attach_flow.set_max_children_per_line(4);
        attach_flow.set_min_children_per_line(1);
        attach_flow.set_row_spacing(8);
        attach_flow.set_column_spacing(8);
        attach_flow.set_homogeneous(true);
        attach_flow.set_visible(false);

        // "Add another" button (visible only when thumbnails are showing)
        let attach_more_btn = gtk::Button::new();
        attach_more_btn.add_css_class("flat");
        attach_more_btn.set_halign(gtk::Align::Start);
        {
            let content = gtk::Box::new(gtk::Orientation::Horizontal, 6);
            let icon = gtk::Image::from_icon_name("list-add-symbolic");
            icon.set_pixel_size(16);
            let label = gtk::Label::new(Some("Add another"));
            label.add_css_class("caption");
            content.append(&icon);
            content.append(&label);
            attach_more_btn.set_child(Some(&content));
        }
        attach_more_btn.set_visible(false);

        attach_box.append(&attach_placeholder);
        attach_box.append(&attach_flow);
        attach_box.append(&attach_more_btn);

        // Shared file-picker logic for both buttons
        let open_picker: Rc<dyn Fn(&gtk::Button)> = {
            let pending = pending_attachments.clone();
            let flow = attach_flow.clone();
            let placeholder = attach_placeholder.clone();
            let more_btn = attach_more_btn.clone();
            let amount_ref = amount_entry.clone();
            let toast_ref = toast_overlay.clone();
            Rc::new(move |btn: &gtk::Button| {
                let filter = gtk::FileFilter::new();
                filter.add_mime_type("image/png");
                filter.add_mime_type("image/jpeg");
                filter.add_mime_type("image/webp");
                filter.set_name(Some("Images"));

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

                let file_dialog = gtk::FileDialog::builder()
                    .title("Attach Receipt")
                    .default_filter(&filter)
                    .filters(&filters)
                    .build();

                let pending = pending.clone();
                let flow = flow.clone();
                let placeholder = placeholder.clone();
                let more_btn = more_btn.clone();
                let amount_ref = amount_ref.clone();
                let toast_ref = toast_ref.clone();
                let window = btn.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() {
                            match std::fs::read(&path) {
                                Ok(data) if data.len() <= 5 * 1024 * 1024 => {
                                    let filename = path.file_name()
                                        .and_then(|n| n.to_str())
                                        .unwrap_or("receipt")
                                        .to_string();
                                    let mime = if filename.ends_with(".png") {
                                        "image/png"
                                    } else if filename.ends_with(".webp") {
                                        "image/webp"
                                    } else {
                                        "image/jpeg"
                                    };

                                    let data_for_ocr = data.clone();
                                    pending.borrow_mut().push((
                                        filename,
                                        mime.to_string(),
                                        data,
                                    ));
                                    Self::rebuild_attach_flow(
                                        &pending, &flow, &placeholder, &more_btn,
                                    );

                                    // Run OCR in background (if tesseract available)
                                    if ocr::is_available() {
                                        let amount_ocr = amount_ref.clone();
                                        let toast_ocr = toast_ref.clone();
                                        glib::spawn_future_local(async move {
                                            let result = gio::spawn_blocking(move || {
                                                ocr::extract_amounts_from_image(&data_for_ocr)
                                            })
                                            .await
                                            .ok()
                                            .flatten();
                                            if let Some(amounts) = result {
                                                if amounts.len() == 1 {
                                                    amount_ocr.set_text(
                                                        &format!("{:.2}", amounts[0].0),
                                                    );
                                                    let toast = adw::Toast::new(
                                                        "Amount detected from receipt",
                                                    );
                                                    toast_ocr.add_toast(toast);
                                                } else {
                                                    show_ocr_amount_picker(
                                                        &toast_ocr,
                                                        &amounts,
                                                        &amount_ocr,
                                                        &toast_ocr,
                                                    );
                                                }
                                            }
                                        });
                                    }
                                }
                                Ok(_) => {
                                    let toast = adw::Toast::new("File too large (max 5MB)");
                                    toast_ref.add_toast(toast);
                                }
                                Err(e) => {
                                    let toast = adw::Toast::new(&format!("Read error: {}", e));
                                    toast_ref.add_toast(toast);
                                }
                            }
                        }
                    }
                });
            })
        };

        // Wire both buttons to the same file picker
        {
            let picker = open_picker.clone();
            attach_placeholder.connect_clicked(move |btn| (picker)(btn));
        }
        {
            let picker = open_picker;
            attach_more_btn.connect_clicked(move |btn| (picker)(btn));
        }

        // -- Save buttons --
        let save_btn_box = gtk::Box::new(gtk::Orientation::Horizontal, 12);
        save_btn_box.set_halign(gtk::Align::Center);
        save_btn_box.set_margin_top(8);

        let save_button = gtk::Button::with_label("Save");
        save_button.add_css_class("suggested-action");
        save_button.add_css_class("pill");
        save_button.add_css_class("save-button");

        let save_next_button = gtk::Button::with_label("Save + Next");
        save_next_button.add_css_class("flat");
        save_next_button.add_css_class("pill");

        // -- Wire currency change to fetch exchange rate + update symbol --
        {
            let db_ref = db.clone();
            let rate_ref = exchange_rate.clone();
            let rate_label_ref = rate_label.clone();
            let base_currency_clone = base_currency.clone();
            let currency_codes_clone: Vec<String> = currency_codes.iter().map(|s| s.to_string()).collect();
            let prefix_ref = prefix_label.clone();
            let suffix_ref = suffix_label.clone();
            currency_row.connect_selected_notify(move |row| {
                let idx = row.selected() as usize;
                if idx >= currency_codes_clone.len() {
                    return;
                }
                let selected = &currency_codes_clone[idx];

                // Update currency symbol
                let (symbol, pos) = currency_info(selected);
                match pos {
                    SymbolPosition::Prefix => {
                        prefix_ref.set_label(symbol);
                        prefix_ref.set_visible(true);
                        suffix_ref.set_visible(false);
                    }
                    SymbolPosition::Suffix => {
                        suffix_ref.set_label(symbol);
                        suffix_ref.set_visible(true);
                        prefix_ref.set_visible(false);
                    }
                }
                if selected.eq_ignore_ascii_case(&base_currency_clone) {
                    rate_ref.set(1.0);
                    rate_label_ref.set_visible(false);
                } else {
                    // Fetch rate asynchronously
                    let db_async = db_ref.clone();
                    let base = base_currency_clone.clone();
                    let target = selected.clone();
                    let rate_async = rate_ref.clone();
                    let label_async = rate_label_ref.clone();
                    glib::spawn_future_local(async move {
                        let service = ExchangeRateService::new(&db_async);
                        match service.get_rate(&base, &target).await {
                            Ok(rate) => {
                                rate_async.set(rate);
                                label_async.set_label(&format!(
                                    "1 {} = {:.4} {}", base, rate, target
                                ));
                                label_async.set_visible(true);
                            }
                            Err(_) => {
                                rate_async.set(1.0);
                                label_async.set_label("Could not fetch exchange rate");
                                label_async.set_visible(true);
                            }
                        }
                    });
                }
            });
        }

        // Session-only memory for last-used category per type (1.2)
        let last_expense_cat: Rc<Cell<u32>> = Rc::new(Cell::new(0));
        let last_income_cat: Rc<Cell<u32>> = Rc::new(Cell::new(0));
        let save_next_mode = Rc::new(Cell::new(false));

        // -- Wire type toggle to filter categories from DB --
        {
            let db_ref = db.clone();
            let model_ref = category_model.clone();
            let ids_ref = category_ids.clone();
            let cat_row_ref = category_row.clone();
            let lec = last_expense_cat.clone();
            expense_btn.connect_toggled(move |btn| {
                if btn.is_active() {
                    Self::populate_categories_from_db(
                        &db_ref, &model_ref, &ids_ref, TransactionType::Expense,
                    );
                    cat_row_ref.set_selected(lec.get());
                }
            });
        }
        {
            let db_ref = db.clone();
            let model_ref = category_model.clone();
            let ids_ref = category_ids.clone();
            let cat_row_ref = category_row.clone();
            let lic = last_income_cat.clone();
            income_btn.connect_toggled(move |btn| {
                if btn.is_active() {
                    Self::populate_categories_from_db(
                        &db_ref, &model_ref, &ids_ref, TransactionType::Income,
                    );
                    cat_row_ref.set_selected(lic.get());
                }
            });
        }

        // -- Recent transactions --
        let recent_group = adw::PreferencesGroup::builder()
            .title("RECENT")
            .build();

        let currency_codes_rc = Rc::new(currency_codes.iter().map(|s| s.to_string()).collect::<Vec<_>>());

        Self::refresh_recent(
            &db, &recent_group, &toast_overlay,
            &expense_btn, &income_btn, &amount_entry,
            &category_row, &category_ids, &category_model,
            &currency_row, &currency_codes_rc,
        );

        // -- Wire save button --
        {
            let db_ref = db.clone();
            let expense_btn_ref = expense_btn.clone();
            let income_btn_ref = income_btn.clone();
            let amount_entry_ref = amount_entry.clone();
            let currency_row_ref = currency_row.clone();
            let category_row_ref = category_row.clone();
            let category_model_ref = category_model.clone();
            let ids_ref = category_ids.clone();
            let selected_date_ref = selected_date.clone();
            let note_row_ref = note_row.clone();
            let payee_row_ref = payee_row.clone();
            let tags_row_ref = tags_row.clone();
            let recent_group_ref = recent_group.clone();
            let toast_overlay_ref = toast_overlay.clone();
            let rate_ref = exchange_rate.clone();
            let currency_codes_save: Vec<String> = currency_codes.iter().map(|s| s.to_string()).collect();
            let currency_codes_rc_ref = currency_codes_rc.clone();
            let app_ref = app.clone();
            let income_label_ref = income_amount_label.clone();
            let expense_label_ref = expense_amount_label.clone();
            let net_label_ref = net_amount_label.clone();
            let base_currency_ref = base_currency.clone();
            let sy = summary_year.clone();
            let sm = summary_month.clone();
            let pending_save = pending_attachments.clone();
            let flow_save = attach_flow.clone();
            let placeholder_save = attach_placeholder.clone();
            let more_save = attach_more_btn.clone();
            let save_next_flag = save_next_mode.clone();
            let last_exp_cat = last_expense_cat.clone();
            let last_inc_cat = last_income_cat.clone();
            let split_switch_save = split_switch.clone();
            let split_entries_save = split_entries.clone();
            let split_list_save = split_list.clone();

            save_button.connect_clicked(move |btn| {
                let is_save_next = save_next_flag.get();
                save_next_flag.set(false);
                let amount_text = amount_entry_ref.text();
                let amount: f64 = match outlay_core::expr::eval_expr(&amount_text) {
                    Some(v) if v > 0.0 => v,
                    _ => {
                        let toast = adw::Toast::new("Please enter a valid amount");
                        toast_overlay_ref.add_toast(toast);
                        return;
                    }
                };

                let txn_type = if expense_btn_ref.is_active() {
                    TransactionType::Expense
                } else {
                    TransactionType::Income
                };

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

                let currency_idx = currency_row_ref.selected() as usize;
                let currency = currency_codes_save
                    .get(currency_idx)
                    .cloned()
                    .unwrap_or_else(|| "USD".to_string());

                let date = selected_date_ref.get();

                let note_text = note_row_ref.text();
                let note = if note_text.is_empty() {
                    None
                } else {
                    Some(note_text.to_string())
                };

                let payee_text = payee_row_ref.text();
                let payee = if payee_text.is_empty() {
                    None
                } else {
                    Some(payee_text.to_string())
                };

                // Parse tags from comma-separated input
                let tags_text = tags_row_ref.text();
                let tag_names: Vec<String> = tags_text
                    .split(',')
                    .map(|s| s.trim().to_string())
                    .filter(|s| !s.is_empty())
                    .collect();

                let new_txn = NewTransaction {
                    amount,
                    transaction_type: txn_type,
                    category_id,
                    currency,
                    exchange_rate: rate_ref.get(),
                    note,
                    date,
                    recurring_id: None,
                    payee,
                };

                // Build the actual save closure (shared between direct save and dialog confirm)
                let do_save = {
                    let db_ref = db_ref.clone();
                    let toast_overlay_ref = toast_overlay_ref.clone();
                    let recent_group_ref = recent_group_ref.clone();
                    let expense_btn_ref = expense_btn_ref.clone();
                    let income_btn_ref = income_btn_ref.clone();
                    let amount_entry_ref = amount_entry_ref.clone();
                    let category_row_ref = category_row_ref.clone();
                    let ids_ref = ids_ref.clone();
                    let category_model_ref = category_model_ref.clone();
                    let currency_row_ref = currency_row_ref.clone();
                    let currency_codes_rc_ref = currency_codes_rc_ref.clone();
                    let app_ref = app_ref.clone();
                    let income_label_ref = income_label_ref.clone();
                    let expense_label_ref = expense_label_ref.clone();
                    let net_label_ref = net_label_ref.clone();
                    let base_currency_ref = base_currency_ref.clone();
                    let note_row_ref = note_row_ref.clone();
                    let payee_row_do = payee_row_ref.clone();
                    let tags_row_do = tags_row_ref.clone();
                    let sy = sy.clone();
                    let sm = sm.clone();
                    let pending_do = pending_save.clone();
                    let flow_do = flow_save.clone();
                    let placeholder_do = placeholder_save.clone();
                    let more_do = more_save.clone();
                    let last_exp_cat = last_exp_cat.clone();
                    let last_inc_cat = last_inc_cat.clone();
                    let split_switch_do = split_switch_save.clone();
                    let split_list_do = split_list_save.clone();
                    let split_entries_do = split_entries_save.clone();
                    Rc::new(move |new_txn: NewTransaction, tag_names: Vec<String>, splits: Vec<(i64, f64, Option<String>)>| {
                        let txn_type = new_txn.transaction_type;
                        let date = new_txn.date;
                        let category_id = new_txn.category_id;

                        match db_ref.insert_transaction(&new_txn) {
                            Ok(new_id) => {
                                // Save pending attachments
                                for (fname, mime, data) in pending_do.borrow().iter() {
                                    let _ = db_ref.insert_attachment(new_id, fname, mime, data);
                                }
                                pending_do.borrow_mut().clear();

                                // Save tags
                                if !tag_names.is_empty() {
                                    let mut tag_ids = Vec::new();
                                    for name in &tag_names {
                                        if let Ok(tid) = db_ref.get_or_create_tag(name) {
                                            tag_ids.push(tid);
                                        }
                                    }
                                    let _ = db_ref.set_transaction_tags(new_id, &tag_ids);
                                }

                                // Save splits
                                if !splits.is_empty() {
                                    let _ = db_ref.insert_splits(new_id, &splits);
                                }
                                while let Some(child) = flow_do.first_child() {
                                    flow_do.remove(&child);
                                }
                                flow_do.set_visible(false);
                                placeholder_do.set_visible(true);
                                more_do.set_visible(false);

                                let cat_name = db_ref
                                    .get_category(category_id)
                                    .map(|c| c.name)
                                    .unwrap_or_default();
                                let msg = format!(
                                    "Saved: {:.2} {}",
                                    new_txn.amount, cat_name,
                                );
                                let toast = adw::Toast::new(&msg);
                                toast.set_button_label(Some("Undo"));
                                {
                                    let db_undo = db_ref.clone();
                                    let recent_undo = recent_group_ref.clone();
                                    let toast_undo = toast_overlay_ref.clone();
                                    let eb_undo = expense_btn_ref.clone();
                                    let ib_undo = income_btn_ref.clone();
                                    let ae_undo = amount_entry_ref.clone();
                                    let cr_undo = category_row_ref.clone();
                                    let ci_undo = ids_ref.clone();
                                    let cm_undo = category_model_ref.clone();
                                    let cur_undo = currency_row_ref.clone();
                                    let cc_undo = currency_codes_rc_ref.clone();
                                    let il_undo = income_label_ref.clone();
                                    let el_undo = expense_label_ref.clone();
                                    let nl_undo = net_label_ref.clone();
                                    let bc_undo = base_currency_ref.clone();
                                    let sy_undo = sy.clone();
                                    let sm_undo = sm.clone();
                                    toast.connect_button_clicked(move |_| {
                                        db_undo.delete_transaction(new_id).ok();
                                        Self::refresh_recent(
                                            &db_undo, &recent_undo, &toast_undo,
                                            &eb_undo, &ib_undo, &ae_undo,
                                            &cr_undo, &ci_undo, &cm_undo,
                                            &cur_undo, &cc_undo,
                                        );
                                        Self::refresh_summary(
                                            &db_undo, &il_undo, &el_undo, &nl_undo,
                                            &bc_undo, sy_undo.get(), sm_undo.get(),
                                        );
                                    });
                                }
                                toast_overlay_ref.add_toast(toast);

                                if txn_type == TransactionType::Expense {
                                    let month_str = format!("{:04}-{:02}", date.year(), date.month());
                                    if let Ok(thresholds) = db_ref.check_budget_thresholds(category_id, &month_str) {
                                        let cat_name = db_ref
                                            .get_category(category_id)
                                            .map(|c| c.name)
                                            .unwrap_or_else(|_| "Category".to_string());
                                        let progress = db_ref
                                            .get_budget_progress(category_id, &month_str)
                                            .ok()
                                            .flatten();
                                        let pct = progress.map(|(_, _, p)| p).unwrap_or(0.0);

                                        for threshold in thresholds {
                                            Self::send_budget_notification(
                                                &app_ref, &cat_name, pct, threshold,
                                            );
                                            db_ref.record_notification(category_id, &month_str, threshold).ok();
                                        }
                                    }
                                }

                                amount_entry_ref.set_text("");
                                note_row_ref.set_text("");
                                payee_row_do.set_text("");
                                tags_row_do.set_text("");

                                // Reset split mode
                                if split_switch_do.is_active() {
                                    split_switch_do.set_active(false);
                                }
                                while let Some(child) = split_list_do.first_child() {
                                    split_list_do.remove(&child.downcast::<gtk::ListBoxRow>().unwrap());
                                }
                                split_entries_do.borrow_mut().clear();

                                // Remember last-used category per type (1.2)
                                if expense_btn_ref.is_active() {
                                    last_exp_cat.set(category_row_ref.selected());
                                } else {
                                    last_inc_cat.set(category_row_ref.selected());
                                }

                                // Save + Next: re-focus for quick entry (1.1)
                                if is_save_next {
                                    amount_entry_ref.grab_focus();
                                }

                                Self::refresh_recent(
                                    &db_ref, &recent_group_ref, &toast_overlay_ref,
                                    &expense_btn_ref, &income_btn_ref, &amount_entry_ref,
                                    &category_row_ref, &ids_ref, &category_model_ref,
                                    &currency_row_ref, &currency_codes_rc_ref,
                                );
                                Self::refresh_summary(
                                    &db_ref, &income_label_ref, &expense_label_ref, &net_label_ref,
                                    &base_currency_ref, sy.get(), sm.get(),
                                );
                            }
                            Err(e) => {
                                let toast = adw::Toast::new(&format!("Error saving: {}", e));
                                toast_overlay_ref.add_toast(toast);
                            }
                        }
                    })
                };

                // Collect splits if split mode is active
                let splits: Vec<(i64, f64, Option<String>)> = if split_switch_save.is_active() {
                    let entries = split_entries_save.borrow();
                    let mut collected = Vec::new();
                    let mut split_total = 0.0_f64;
                    for (_row, cat_ids, dropdown, amt_entry) in entries.iter() {
                        let idx = dropdown.selected() as usize;
                        let cat_id = cat_ids.get(idx).copied().unwrap_or(0);
                        let amt_text = amt_entry.text();
                        let amt: f64 = outlay_core::expr::eval_expr(&amt_text).unwrap_or(0.0);
                        if amt > 0.0 && cat_id > 0 {
                            collected.push((cat_id, amt, None));
                            split_total += amt;
                        }
                    }
                    if (split_total - amount).abs() > 0.01 {
                        let toast = adw::Toast::new(&format!(
                            "Split total ({:.2}) does not match amount ({:.2})",
                            split_total, amount
                        ));
                        toast_overlay_ref.add_toast(toast);
                        return;
                    }
                    collected
                } else {
                    Vec::new()
                };

                // Check for duplicate transaction
                let is_dup = db_ref
                    .find_duplicate_transaction(amount, txn_type, category_id, date)
                    .unwrap_or(false);

                if is_dup {
                    let alert = adw::AlertDialog::new(
                        Some("Possible duplicate"),
                        Some("A similar transaction already exists for this date. Save anyway?"),
                    );
                    alert.add_response("cancel", "Cancel");
                    alert.add_response("save", "Save anyway");
                    alert.set_response_appearance("save", adw::ResponseAppearance::Suggested);
                    alert.set_default_response(Some("cancel"));
                    alert.set_close_response("cancel");

                    let do_save_ref = do_save.clone();
                    let tag_names_dup = tag_names.clone();
                    let splits_dup = splits.clone();
                    alert.connect_response(None, move |_, response| {
                        if response == "save" {
                            do_save_ref(new_txn.clone(), tag_names_dup.clone(), splits_dup.clone());
                        }
                    });
                    alert.present(Some(btn));
                } else {
                    do_save(new_txn, tag_names, splits);
                }
            });
        }

        // -- Wire save+next button: sets flag then activates save --
        {
            let flag = save_next_mode;
            let save_btn_ref = save_button.clone();
            save_next_button.connect_clicked(move |_| {
                flag.set(true);
                save_btn_ref.activate();
            });
        }

        // -- Templates popover --
        let templates_btn = gtk::MenuButton::new();
        templates_btn.set_icon_name("document-open-symbolic");
        templates_btn.add_css_class("flat");
        templates_btn.set_tooltip_text(Some("Load template"));

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

        let tpl_scroll = gtk::ScrolledWindow::builder()
            .hscrollbar_policy(gtk::PolicyType::Never)
            .max_content_height(300)
            .propagate_natural_height(true)
            .child(&templates_list_box)
            .build();
        tpl_scroll.set_size_request(280, -1);
        templates_popover.set_child(Some(&tpl_scroll));
        templates_btn.set_popover(Some(&templates_popover));

        // Populate templates
        {
            let db_tpl = db.clone();
            let list_tpl = templates_list_box.clone();
            let expense_tpl = expense_btn.clone();
            let income_tpl = income_btn.clone();
            let amount_tpl = amount_entry.clone();
            let cat_row_tpl = category_row.clone();
            let ids_tpl = category_ids.clone();
            let model_tpl = category_model.clone();
            let note_tpl = note_row.clone();
            let payee_tpl = payee_row.clone();
            let tags_tpl = tags_row.clone();
            let popover_tpl = templates_popover.clone();
            let toast_tpl = toast_overlay.clone();

            let populate_templates = Rc::new(move || {
                while let Some(child) = list_tpl.first_child() {
                    list_tpl.remove(&child);
                }
                if let Ok(templates) = db_tpl.list_templates() {
                    if templates.is_empty() {
                        let empty = gtk::Label::new(Some("No templates yet"));
                        empty.add_css_class("dim-label");
                        empty.set_margin_top(16);
                        empty.set_margin_bottom(16);
                        list_tpl.append(&empty);
                        return;
                    }
                    for tpl in &templates {
                        let cat_name = db_tpl.get_category(tpl.category_id)
                            .map(|c| c.name)
                            .unwrap_or_else(|_| "Unknown".to_string());
                        let subtitle = match tpl.amount {
                            Some(a) => format!("{:.2} {} - {}", a, tpl.currency, cat_name),
                            None => cat_name.clone(),
                        };
                        let type_prefix = match tpl.transaction_type {
                            TransactionType::Expense => "Expense",
                            TransactionType::Income => "Income",
                        };
                        let row = adw::ActionRow::builder()
                            .title(&tpl.name)
                            .subtitle(&format!("{} - {}", type_prefix, subtitle))
                            .activatable(true)
                            .build();

                        let tpl_clone = tpl.clone();
                        let exp_ref = expense_tpl.clone();
                        let inc_ref = income_tpl.clone();
                        let amt_ref = amount_tpl.clone();
                        let cr_ref = cat_row_tpl.clone();
                        let ids_ref = ids_tpl.clone();
                        let model_ref = model_tpl.clone();
                        let note_ref = note_tpl.clone();
                        let payee_ref = payee_tpl.clone();
                        let tags_ref = tags_tpl.clone();
                        let pop_ref = popover_tpl.clone();
                        let db_pop = db_tpl.clone();
                        let toast_ref = toast_tpl.clone();
                        row.connect_activated(move |_| {
                            // Set type
                            match tpl_clone.transaction_type {
                                TransactionType::Expense => exp_ref.set_active(true),
                                TransactionType::Income => inc_ref.set_active(true),
                            }
                            // Populate categories for this type
                            Self::populate_categories_from_db(
                                &db_pop, &model_ref, &ids_ref, tpl_clone.transaction_type,
                            );
                            // Set amount
                            if let Some(a) = tpl_clone.amount {
                                amt_ref.set_text(&format!("{:.2}", a));
                            }
                            // Set category
                            if let Some(pos) = ids_ref.borrow().iter().position(|&id| id == tpl_clone.category_id) {
                                cr_ref.set_selected(pos as u32);
                            }
                            // Set note/payee/tags
                            note_ref.set_text(tpl_clone.note.as_deref().unwrap_or(""));
                            payee_ref.set_text(tpl_clone.payee.as_deref().unwrap_or(""));
                            tags_ref.set_text(tpl_clone.tags.as_deref().unwrap_or(""));
                            pop_ref.popdown();
                            let toast = adw::Toast::new(&format!("Loaded: {}", tpl_clone.name));
                            toast.set_timeout(5);
                            toast_ref.add_toast(toast);
                        });

                        list_tpl.append(&row);
                    }
                }
            });
            populate_templates();
        }

        // -- Save as Template button --
        let save_tpl_btn = gtk::Button::with_label("Save as Template");
        save_tpl_btn.add_css_class("flat");
        save_tpl_btn.add_css_class("pill");
        {
            let db_tpl = db.clone();
            let expense_tpl = expense_btn.clone();
            let amount_tpl = amount_entry.clone();
            let cat_row_tpl = category_row.clone();
            let ids_tpl = category_ids.clone();
            let currency_row_tpl = currency_row.clone();
            let currency_codes_tpl: Vec<String> = currency_codes.iter().map(|s| s.to_string()).collect();
            let note_tpl = note_row.clone();
            let payee_tpl = payee_row.clone();
            let tags_tpl = tags_row.clone();
            let toast_tpl = toast_overlay.clone();
            save_tpl_btn.connect_clicked(move |btn| {
                let amount_text = amount_tpl.text();
                let amount: Option<f64> = outlay_core::expr::eval_expr(&amount_text).filter(|v| *v > 0.0);

                let txn_type = if expense_tpl.is_active() {
                    TransactionType::Expense
                } else {
                    TransactionType::Income
                };

                let cat_idx = cat_row_tpl.selected() as usize;
                let ids = ids_tpl.borrow();
                let category_id = match ids.get(cat_idx) {
                    Some(&id) => id,
                    None => {
                        let toast = adw::Toast::new("Select a category first");
                        toast_tpl.add_toast(toast);
                        return;
                    }
                };

                let cur_idx = currency_row_tpl.selected() as usize;
                let currency = currency_codes_tpl.get(cur_idx).cloned().unwrap_or_else(|| "USD".to_string());

                let note_text = note_tpl.text();
                let note = if note_text.is_empty() { None } else { Some(note_text.to_string()) };
                let payee_text = payee_tpl.text();
                let payee = if payee_text.is_empty() { None } else { Some(payee_text.to_string()) };
                let tags_text = tags_tpl.text();
                let tags = if tags_text.is_empty() { None } else { Some(tags_text.to_string()) };

                let alert = adw::AlertDialog::new(
                    Some("Save as Template"),
                    Some("Enter a name for this template:"),
                );
                alert.add_response("cancel", "Cancel");
                alert.add_response("save", "Save");
                alert.set_response_appearance("save", adw::ResponseAppearance::Suggested);
                alert.set_default_response(Some("save"));
                alert.set_close_response("cancel");

                let entry = adw::EntryRow::builder()
                    .title("Template name")
                    .build();
                alert.set_extra_child(Some(&entry));

                let db_save = db_tpl.clone();
                let toast_save = toast_tpl.clone();
                alert.connect_response(None, move |_, response| {
                    if response == "save" {
                        let name = entry.text().to_string();
                        if name.trim().is_empty() {
                            let toast = adw::Toast::new("Template name cannot be empty");
                            toast_save.add_toast(toast);
                            return;
                        }
                        match db_save.insert_template(
                            name.trim(),
                            amount,
                            txn_type,
                            category_id,
                            &currency,
                            payee.as_deref(),
                            note.as_deref(),
                            tags.as_deref(),
                        ) {
                            Ok(_) => {
                                let toast = adw::Toast::new("Template saved");
                                toast_save.add_toast(toast);
                            }
                            Err(e) => {
                                let toast = adw::Toast::new(&format!("Error: {}", e));
                                toast_save.add_toast(toast);
                            }
                        }
                    }
                });
                alert.present(Some(btn));
            });
        }

        // -- Assemble --
        inner.append(&amount_box);
        inner.append(&expr_label);
        inner.append(&type_box);
        inner.append(&nl_group);
        inner.append(&form_group);
        inner.append(&split_box);
        inner.append(&attach_box);
        inner.append(&rate_label);
        save_btn_box.append(&save_button);
        save_btn_box.append(&save_next_button);
        save_btn_box.append(&templates_btn);
        save_btn_box.append(&save_tpl_btn);
        inner.append(&save_btn_box);
        inner.append(&recent_group);

        clamp.set_child(Some(&inner));
        container.append(&clamp);

        LogView {
            container,
            toast_overlay,
            db,
            category_model,
            category_ids,
            expense_btn,
            income_btn,
            amount_entry,
            category_row,
            currency_row,
            currency_codes: currency_codes_rc,
        }
    }

    pub fn set_income_mode(&self, income: bool) {
        if income {
            self.income_btn.set_active(true);
        } else {
            self.expense_btn.set_active(true);
        }
    }

    pub fn focus_amount(&self) {
        let entry = self.amount_entry.clone();
        glib::timeout_add_local_once(std::time::Duration::from_millis(300), move || {
            entry.grab_focus();
        });
    }

    pub fn refresh_categories(&self) {
        let txn_type = if self.expense_btn.is_active() {
            TransactionType::Expense
        } else {
            TransactionType::Income
        };
        Self::populate_categories_from_db(&self.db, &self.category_model, &self.category_ids, txn_type);
    }

    fn make_summary_card(label_text: &str) -> (gtk::Box, gtk::Label) {
        let card = gtk::Box::new(gtk::Orientation::Vertical, 4);
        card.add_css_class("card");
        card.add_css_class("summary-card");
        card.set_hexpand(true);

        let label = gtk::Label::new(Some(label_text));
        label.add_css_class("summary-card-label");
        label.set_halign(gtk::Align::Start);

        let amount = gtk::Label::new(Some("0.00"));
        amount.add_css_class("summary-card-amount");
        amount.set_halign(gtk::Align::Start);

        card.append(&label);
        card.append(&amount);

        (card, amount)
    }

    fn refresh_summary(
        db: &Database,
        income_label: &gtk::Label,
        expense_label: &gtk::Label,
        net_label: &gtk::Label,
        base_currency: &str,
        year: i32,
        month: u32,
    ) {
        let income = db.get_monthly_total(year, month, TransactionType::Income).unwrap_or(0.0);
        let expense = db.get_monthly_total(year, month, TransactionType::Expense).unwrap_or(0.0);
        let net = income - expense;

        income_label.set_label(&format!("+{:.2} {}", income, base_currency));
        income_label.remove_css_class("amount-income");
        income_label.remove_css_class("amount-expense");
        income_label.add_css_class("amount-income");

        expense_label.set_label(&format!("-{:.2} {}", expense, base_currency));
        expense_label.remove_css_class("amount-income");
        expense_label.remove_css_class("amount-expense");
        expense_label.add_css_class("amount-expense");

        if net >= 0.0 {
            net_label.set_label(&format!("+{:.2} {}", net, base_currency));
            net_label.remove_css_class("amount-expense");
            net_label.add_css_class("amount-income");
        } else {
            net_label.set_label(&format!("{:.2} {}", net, base_currency));
            net_label.remove_css_class("amount-income");
            net_label.add_css_class("amount-expense");
        }
    }

    fn send_budget_notification(
        app: &adw::Application,
        category: &str,
        percentage: f64,
        threshold: u32,
    ) {
        let body = match threshold {
            75 => format!("{} is at {:.0}% of budget", category, percentage),
            90 => format!("{} is at {:.0}% of budget - almost at limit!", category, percentage),
            100 => format!("{} is over budget at {:.0}%!", category, percentage),
            _ => return,
        };

        // GTK notification
        let notification = gio::Notification::new("Budget Alert");
        notification.set_body(Some(&body));
        app.send_notification(
            Some(&format!("budget-{}-{}", category, threshold)),
            &notification,
        );

        // System notification via notify-send
        let urgency = if threshold >= 100 { "critical" } else { "normal" };
        outlay_core::notifications::send_notification("Budget Alert", &body, urgency);
    }

    fn populate_categories_from_db(
        db: &Database,
        model: &gtk::StringList,
        ids: &Rc<RefCell<Vec<i64>>>,
        txn_type: TransactionType,
    ) {
        while model.n_items() > 0 {
            model.remove(0);
        }
        let mut id_list = ids.borrow_mut();
        id_list.clear();

        if let Ok(cats) = db.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(),
                };
                model.append(&entry);
                id_list.push(cat.id);
            }
        }
    }

    fn update_split_remaining(
        total_entry: &gtk::Entry,
        entries: &Rc<RefCell<Vec<(gtk::ListBoxRow, Vec<i64>, gtk::DropDown, gtk::Entry)>>>,
        label: &gtk::Label,
    ) {
        let total: f64 = outlay_core::expr::eval_expr(&total_entry.text()).unwrap_or(0.0);
        let split_sum: f64 = entries.borrow().iter().map(|(_, _, _, e)| {
            outlay_core::expr::eval_expr(&e.text()).unwrap_or(0.0)
        }).sum();
        let remaining = total - split_sum;
        if remaining.abs() < 0.01 {
            label.set_label("Splits balanced");
            label.remove_css_class("amount-expense");
            label.add_css_class("amount-income");
        } else {
            label.set_label(&format!("Remaining: {:.2}", remaining));
            label.remove_css_class("amount-income");
            if remaining < 0.0 {
                label.add_css_class("amount-expense");
            } else {
                label.remove_css_class("amount-expense");
            }
        }
    }

    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
    }

    fn refresh_recent(
        db: &Rc<Database>,
        group: &adw::PreferencesGroup,
        toast_overlay: &adw::ToastOverlay,
        expense_btn: &gtk::ToggleButton,
        income_btn: &gtk::ToggleButton,
        amount_entry: &gtk::Entry,
        category_row: &adw::ComboRow,
        category_ids: &Rc<RefCell<Vec<i64>>>,
        category_model: &gtk::StringList,
        currency_row: &adw::ComboRow,
        currency_codes: &Rc<Vec<String>>,
    ) {
        // Collect all rows first, then remove them
        let mut rows_to_remove = Vec::new();
        let mut child = group.first_child();
        while let Some(widget) = child {
            let next = widget.next_sibling();
            // The PreferencesGroup has an internal structure:
            // a Box containing a ListBox. We need to find all ActionRows
            // inside the internal ListBox and remove them from the group.
            if widget.downcast_ref::<adw::ActionRow>().is_some() {
                rows_to_remove.push(widget.clone());
            } else {
                // Walk into container children to find rows
                Self::collect_action_rows(&widget, &mut rows_to_remove);
            }
            child = next;
        }
        for row in rows_to_remove {
            if let Some(action_row) = row.downcast_ref::<adw::ActionRow>() {
                group.remove(action_row);
            }
        }

        match db.list_recent_transactions(5) {
            Ok(txns) if !txns.is_empty() => {
                for txn in &txns {
                    let cat = db.get_category(txn.category_id).ok();
                    let cat_name = cat.as_ref()
                        .map(|c| c.name.clone())
                        .unwrap_or_else(|| "Unknown".to_string());
                    let cat_icon = cat.as_ref().and_then(|c| c.icon.clone());
                    let cat_color = cat.as_ref().and_then(|c| c.color.clone());

                    let amount_str = match txn.transaction_type {
                        TransactionType::Expense => format!("-{:.2} {}", txn.amount, txn.currency),
                        TransactionType::Income => format!("+{:.2} {}", txn.amount, txn.currency),
                    };

                    let subtitle = match &txn.note {
                        Some(n) if !n.is_empty() => format!("{} - {}", txn.date, n),
                        _ => txn.date.to_string(),
                    };

                    let row = adw::ActionRow::builder()
                        .title(&cat_name)
                        .subtitle(&subtitle)
                        .activatable(true)
                        .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 amount_label = gtk::Label::new(Some(&amount_str));
                    amount_label.add_css_class("amount-display");
                    match txn.transaction_type {
                        TransactionType::Expense => amount_label.add_css_class("amount-expense"),
                        TransactionType::Income => amount_label.add_css_class("amount-income"),
                    }
                    row.add_suffix(&amount_label);

                    // Repeat button to pre-fill form with this transaction
                    let repeat_btn = gtk::Button::from_icon_name("tabler-repeat");
                    repeat_btn.add_css_class("flat");
                    repeat_btn.set_valign(gtk::Align::Center);
                    repeat_btn.set_tooltip_text(Some("Repeat this transaction"));
                    {
                        let txn_type = txn.transaction_type;
                        let txn_amount = txn.amount;
                        let txn_cat_id = txn.category_id;
                        let txn_currency = txn.currency.clone();
                        let expense_ref = expense_btn.clone();
                        let income_ref = income_btn.clone();
                        let amount_ref = amount_entry.clone();
                        let cat_row_ref = category_row.clone();
                        let cat_ids_ref = category_ids.clone();
                        let cur_row_ref = currency_row.clone();
                        let cur_codes_ref = currency_codes.clone();
                        repeat_btn.connect_clicked(move |_| {
                            // Set type toggle
                            match txn_type {
                                TransactionType::Expense => expense_ref.set_active(true),
                                TransactionType::Income => income_ref.set_active(true),
                            }
                            // Set amount
                            amount_ref.set_text(&format!("{:.2}", txn_amount));
                            // Select matching category
                            let ids = cat_ids_ref.borrow();
                            if let Some(pos) = ids.iter().position(|&id| id == txn_cat_id) {
                                cat_row_ref.set_selected(pos as u32);
                            }
                            // Select matching currency
                            if let Some(pos) = cur_codes_ref.iter().position(|c| c.eq_ignore_ascii_case(&txn_currency)) {
                                cur_row_ref.set_selected(pos as u32);
                            }
                        });
                    }
                    row.add_suffix(&repeat_btn);

                    // Arrow to indicate clickability
                    let arrow = gtk::Image::from_icon_name("outlay-next");
                    arrow.add_css_class("dim-label");
                    row.add_suffix(&arrow);

                    // Connect row activation to edit dialog
                    let txn_id = txn.id;
                    let db_ref = db.clone();
                    let group_ref = group.clone();
                    let toast_ref = toast_overlay.clone();
                    let eb = expense_btn.clone();
                    let ib = income_btn.clone();
                    let ae = amount_entry.clone();
                    let cr = category_row.clone();
                    let ci = category_ids.clone();
                    let cm = category_model.clone();
                    let cur = currency_row.clone();
                    let cc = currency_codes.clone();
                    row.connect_activated(move |row| {
                        let db_c = db_ref.clone();
                        let group_c = group_ref.clone();
                        let toast_c = toast_ref.clone();
                        let eb = eb.clone();
                        let ib = ib.clone();
                        let ae = ae.clone();
                        let cr = cr.clone();
                        let ci = ci.clone();
                        let cm = cm.clone();
                        let cur = cur.clone();
                        let cc = cc.clone();
                        edit_dialog::show_edit_dialog(row, txn_id, &db_ref, &toast_ref, move || {
                            Self::refresh_recent(&db_c, &group_c, &toast_c, &eb, &ib, &ae, &cr, &ci, &cm, &cur, &cc);
                        });
                    });

                    group.add(&row);
                }
            }
            _ => {
                let placeholder = adw::ActionRow::builder()
                    .title("No transactions yet")
                    .build();
                placeholder.add_css_class("dim-label");
                group.add(&placeholder);
            }
        }
    }

    fn rebuild_attach_flow(
        pending: &Rc<RefCell<Vec<PendingAttachment>>>,
        flow: &gtk::FlowBox,
        placeholder: &gtk::Button,
        more_btn: &gtk::Button,
    ) {
        while let Some(child) = flow.first_child() {
            flow.remove(&child);
        }
        let items = pending.borrow().clone();
        if items.is_empty() {
            flow.set_visible(false);
            placeholder.set_visible(true);
            more_btn.set_visible(false);
            return;
        }
        flow.set_visible(true);
        placeholder.set_visible(false);
        more_btn.set_visible(true);

        for (j, (fname, _, fdata)) in items.iter().enumerate() {
            let thumb = gtk::Box::new(gtk::Orientation::Vertical, 0);
            thumb.set_overflow(gtk::Overflow::Hidden);
            thumb.add_css_class("attach-thumbnail");

            let overlay = gtk::Overlay::new();
            let b = glib::Bytes::from(fdata);
            let tex = gtk::gdk::Texture::from_bytes(&b).ok();
            let img = if let Some(t) = &tex {
                let p = gtk::Picture::for_paintable(t);
                p.set_content_fit(gtk::ContentFit::Cover);
                p.set_size_request(80, 80);
                p.upcast::<gtk::Widget>()
            } else {
                let l = gtk::Label::new(Some(fname));
                l.set_size_request(80, 80);
                l.upcast::<gtk::Widget>()
            };
            overlay.set_child(Some(&img));

            let del = gtk::Button::from_icon_name("outlay-delete");
            del.add_css_class("flat");
            del.add_css_class("circular");
            del.add_css_class("osd");
            del.set_halign(gtk::Align::End);
            del.set_valign(gtk::Align::Start);
            del.set_tooltip_text(Some("Remove attachment"));
            overlay.add_overlay(&del);

            // Click thumbnail to view full image
            if let Some(texture) = tex {
                let click = gtk::GestureClick::new();
                let fname_owned = fname.clone();
                let data_owned = fdata.clone();
                click.connect_released(move |gesture, _, _, _| {
                    if let Some(widget) = gesture.widget() {
                        show_image_preview(&widget, &fname_owned, &texture, &data_owned);
                    }
                });
                thumb.add_controller(click);
            }

            thumb.append(&overlay);

            let pd = pending.clone();
            let fd = flow.clone();
            let ph = placeholder.clone();
            let mb = more_btn.clone();
            del.connect_clicked(move |_| {
                if j < pd.borrow().len() {
                    pd.borrow_mut().remove(j);
                }
                Self::rebuild_attach_flow(&pd, &fd, &ph, &mb);
            });

            flow.insert(&thumb, -1);
        }
    }

    fn collect_action_rows(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(&w, rows);
            }
            child = next;
        }
    }
}

fn show_ocr_amount_picker(
    parent: &impl IsA<gtk::Widget>,
    amounts: &[(f64, String)],
    amount_entry: &gtk::Entry,
    toast_overlay: &adw::ToastOverlay,
) {
    let dialog = adw::Dialog::builder()
        .title("Detected amounts")
        .content_width(340)
        .build();

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

    let list = gtk::ListBox::builder()
        .selection_mode(gtk::SelectionMode::None)
        .css_classes(["boxed-list"])
        .margin_top(12)
        .margin_bottom(12)
        .margin_start(12)
        .margin_end(12)
        .build();

    for (amt, line_text) in amounts {
        let row = adw::ActionRow::builder()
            .title(&format!("{:.2}", amt))
            .subtitle(line_text)
            .activatable(true)
            .build();
        row.add_suffix(&gtk::Image::from_icon_name("go-next-symbolic"));

        let dialog_ref = dialog.clone();
        let entry_ref = amount_entry.clone();
        let toast_ref = toast_overlay.clone();
        let val = *amt;
        row.connect_activated(move |_| {
            entry_ref.set_text(&format!("{:.2}", val));
            let toast = adw::Toast::new("Amount applied from receipt");
            toast_ref.add_toast(toast);
            dialog_ref.close();
        });

        list.append(&row);
    }

    let scroll = gtk::ScrolledWindow::builder()
        .child(&list)
        .propagate_natural_height(true)
        .max_content_height(400)
        .build();

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

pub fn show_image_preview(
    parent: &impl IsA<gtk::Widget>,
    filename: &str,
    texture: &gtk::gdk::Texture,
    image_data: &[u8],
) {
    let dialog = adw::Dialog::builder()
        .title(filename)
        .content_width(10000)
        .content_height(10000)
        .build();

    let toolbar = adw::ToolbarView::new();
    let header = adw::HeaderBar::new();

    // Save button in header
    let save_btn = gtk::Button::from_icon_name("document-save-as-symbolic");
    save_btn.set_tooltip_text(Some("Save image as..."));
    header.pack_end(&save_btn);

    let fname = filename.to_string();
    let data = image_data.to_vec();
    let dialog_ref = dialog.clone();
    save_btn.connect_clicked(move |btn| {
        let file_dialog = gtk::FileDialog::builder()
            .title("Save receipt image")
            .initial_name(&fname)
            .build();

        let data_clone = data.clone();
        let window = dialog_ref.root()
            .or_else(|| btn.root())
            .and_then(|r| r.downcast::<gtk::Window>().ok());
        file_dialog.save(window.as_ref(), gio::Cancellable::NONE, move |result| {
            if let Ok(file) = result {
                if let Some(path) = file.path() {
                    let _ = std::fs::write(&path, &data_clone);
                }
            }
        });
    });

    toolbar.add_top_bar(&header);

    let picture = gtk::Picture::for_paintable(texture);
    picture.set_content_fit(gtk::ContentFit::Contain);
    picture.set_margin_top(8);
    picture.set_margin_bottom(8);
    picture.set_margin_start(8);
    picture.set_margin_end(8);

    let scroll = gtk::ScrolledWindow::builder()
        .child(&picture)
        .build();

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