use adw::prelude::*;
use adw::subclass::prelude::*;
use gtk::gio;
use std::cell::OnceCell;
use std::rc::Rc;
use std::time::Instant;

use crate::config::APP_ID;
use crate::core::analysis;
use crate::core::backup;
use crate::core::database::Database;
use crate::core::discovery;
use crate::core::footprint;
use crate::core::integrator;
use crate::core::launcher;
use crate::core::notification;
use crate::core::orphan;
use crate::core::security;
use crate::core::updater;
use crate::core::watcher;
use crate::i18n::{i18n, i18n_f, ni18n_f};
use crate::ui::catalog_view;
use crate::ui::cleanup_wizard;
use crate::ui::dashboard;
use crate::ui::detail_view;
use crate::ui::drop_dialog;
use crate::ui::duplicate_dialog;
use crate::ui::library_view::{LibraryState, LibraryView};
use crate::ui::preferences;
use crate::ui::security_report;
use crate::ui::update_dialog;
use crate::ui::updates_view;
use crate::ui::widgets;

mod imp {
    use super::*;

    pub struct DriftwoodWindow {
        pub settings: OnceCell<gio::Settings>,
        pub toast_overlay: OnceCell<adw::ToastOverlay>,
        pub view_stack: OnceCell<adw::ViewStack>,
        pub installed_nav: OnceCell<adw::NavigationView>,
        pub library_view: OnceCell<LibraryView>,
        pub database: OnceCell<Rc<Database>>,
        pub drop_overlay: OnceCell<gtk::Box>,
        pub drop_revealer: OnceCell<gtk::Revealer>,
        pub watcher_handle: std::cell::RefCell<Option<notify::RecommendedWatcher>>,
        pub enrichment_banner: OnceCell<gtk::Box>,
    }

    impl Default for DriftwoodWindow {
        fn default() -> Self {
            Self {
                settings: OnceCell::new(),
                toast_overlay: OnceCell::new(),
                view_stack: OnceCell::new(),
                installed_nav: OnceCell::new(),
                library_view: OnceCell::new(),
                database: OnceCell::new(),
                drop_overlay: OnceCell::new(),
                drop_revealer: OnceCell::new(),
                watcher_handle: std::cell::RefCell::new(None),
                enrichment_banner: OnceCell::new(),
            }
        }
    }

    #[glib::object_subclass]
    impl ObjectSubclass for DriftwoodWindow {
        const NAME: &'static str = "DriftwoodWindow";
        type Type = super::DriftwoodWindow;
        type ParentType = adw::ApplicationWindow;
    }

    impl ObjectImpl for DriftwoodWindow {
        fn constructed(&self) {
            self.parent_constructed();
            let window = self.obj();
            window.setup_settings();
            window.setup_database();
            window.setup_ui();
            window.restore_window_state();
            window.load_initial_data();
        }
    }

    impl WidgetImpl for DriftwoodWindow {}
    impl WindowImpl for DriftwoodWindow {
        fn close_request(&self) -> glib::Propagation {
            self.obj().save_window_state();
            self.parent_close_request()
        }
    }
    impl ApplicationWindowImpl for DriftwoodWindow {}
    impl AdwApplicationWindowImpl for DriftwoodWindow {}
}

glib::wrapper! {
    pub struct DriftwoodWindow(ObjectSubclass<imp::DriftwoodWindow>)
        @extends adw::ApplicationWindow, gtk::ApplicationWindow, gtk::Window, gtk::Widget,
        @implements gio::ActionGroup, gio::ActionMap,
            gtk::Accessible, gtk::Buildable, gtk::ConstraintTarget,
            gtk::Native, gtk::Root, gtk::ShortcutManager;
}

/// Find a child widget by its widget name (breadth-first).
fn find_child_by_name(parent: &impl IsA<gtk::Widget>, name: &str) -> Option<gtk::Widget> {
    let parent_widget = parent.upcast_ref::<gtk::Widget>();
    let mut child = parent_widget.first_child();
    while let Some(c) = child {
        if c.widget_name() == name {
            return Some(c);
        }
        if let Some(found) = find_child_by_name(&c, name) {
            return Some(found);
        }
        child = c.next_sibling();
    }
    None
}

fn shortcut_row(accel: &str, description: &str) -> adw::ActionRow {
    let row = adw::ActionRow::builder()
        .title(description)
        .build();
    let accel_label = gtk::Label::builder()
        .label(accel)
        .css_classes(["monospace", "dimmed"])
        .valign(gtk::Align::Center)
        .build();
    accel_label.update_property(&[gtk::accessible::Property::Label(
        &format!("Keyboard shortcut: {}", accel),
    )]);
    row.add_suffix(&accel_label);
    row
}

impl DriftwoodWindow {
    pub fn new(app: &crate::application::DriftwoodApplication) -> Self {
        glib::Object::builder()
            .property("application", app)
            .build()
    }

    fn setup_settings(&self) {
        let settings = gio::Settings::new(APP_ID);
        self.imp()
            .settings
            .set(settings)
            .expect("Settings already initialized");
    }

    fn settings(&self) -> &gio::Settings {
        self.imp().settings.get().expect("Settings not initialized")
    }

    fn setup_database(&self) {
        let db = Database::open().expect("Failed to open database");
        if self.imp().database.set(Rc::new(db)).is_err() {
            panic!("Database already initialized");
        }
    }

    fn database(&self) -> &Rc<Database> {
        self.imp().database.get().expect("Database not initialized")
    }

    fn setup_ui(&self) {
        // Set initial window title for screen readers (WCAG 2.4.2)
        self.set_title(Some("Driftwood"));

        // Build the hamburger menu model (slim - tabs handle catalog/updates/scan)
        let menu = gio::Menu::new();
        menu.append(Some(&i18n("Dashboard")), Some("win.dashboard"));

        let section2 = gio::Menu::new();
        section2.append(Some(&i18n("Find Duplicates")), Some("win.find-duplicates"));
        section2.append(Some(&i18n("Security Report")), Some("win.security-report"));
        section2.append(Some(&i18n("Disk Cleanup")), Some("win.cleanup"));
        section2.append(Some(&i18n("Export App List")), Some("win.export-app-list"));
        section2.append(Some(&i18n("Import App List")), Some("win.import-app-list"));
        menu.append_section(None, &section2);

        let section3 = gio::Menu::new();
        section3.append(Some(&i18n("Preferences")), Some("win.preferences"));
        section3.append(Some(&i18n("Keyboard Shortcuts")), Some("win.show-shortcuts"));
        menu.append_section(None, &section3);

        // Library view (contains header bar, search, grid/list, empty state)
        let library_view = LibraryView::new(&menu);

        // Installed view: NavigationView for drill-down (detail, dashboard, etc.)
        let installed_nav = adw::NavigationView::new();
        installed_nav.push(&library_view.page);

        // Catalog view (has its own internal NavigationView for drill-down)
        let (catalog_nav, enrichment_banner) = catalog_view::build_catalog_page(self.database());

        // Updates view
        let updates_toolbar = updates_view::build_updates_view(self.database());

        // ViewStack with 3 top-level pages
        let view_stack = adw::ViewStack::new();
        view_stack.set_vexpand(true);

        let installed_vs_page = view_stack.add_titled(&installed_nav, Some("installed"), &i18n("Installed"));
        installed_vs_page.set_icon_name(Some("view-grid-symbolic"));

        let catalog_vs_page = view_stack.add_titled(&catalog_nav, Some("catalog"), &i18n("Catalog"));
        catalog_vs_page.set_icon_name(Some("system-software-install-symbolic"));

        let updates_vs_page = view_stack.add_titled(&updates_toolbar, Some("updates"), &i18n("Updates"));
        updates_vs_page.set_icon_name(Some("software-update-available-symbolic"));

        // ViewSwitcherBar at the bottom for tab navigation
        let view_switcher_bar = adw::ViewSwitcherBar::builder()
            .stack(&view_stack)
            .reveal(true)
            .build();

        // Toast overlay wraps only the ViewStack so toasts appear above the tab bar
        let toast_overlay = adw::ToastOverlay::new();
        toast_overlay.set_child(Some(&view_stack));

        // Main content box: toast-wrapped ViewStack + bottom switcher bar
        let main_box = gtk::Box::builder()
            .orientation(gtk::Orientation::Vertical)
            .build();
        main_box.append(&toast_overlay);
        main_box.append(&view_switcher_bar);

        // Drop overlay - centered opaque card over a dimmed scrim
        let drop_overlay_icon = gtk::Image::builder()
            .icon_name("document-open-symbolic")
            .pixel_size(64)
            .halign(gtk::Align::Center)
            .accessible_role(gtk::AccessibleRole::Presentation)
            .build();
        drop_overlay_icon.add_css_class("drop-zone-icon");

        let drop_overlay_title = gtk::Label::builder()
            .label(&i18n("Add AppImage"))
            .css_classes(["title-1"])
            .halign(gtk::Align::Center)
            .build();

        let drop_overlay_subtitle = gtk::Label::builder()
            .label(&i18n("Drop an AppImage file (.AppImage) here, or click to browse your files"))
            .css_classes(["body", "dimmed"])
            .halign(gtk::Align::Center)
            .wrap(true)
            .build();

        // The card itself - acts as a clickable button to open file picker
        let drop_zone_card = gtk::Box::builder()
            .orientation(gtk::Orientation::Vertical)
            .spacing(16)
            .halign(gtk::Align::Center)
            .valign(gtk::Align::Center)
            .width_request(320)
            .focusable(true)
            .accessible_role(gtk::AccessibleRole::Button)
            .build();
        drop_zone_card.add_css_class("drop-zone-card");
        drop_zone_card.set_cursor_from_name(Some("pointer"));
        drop_zone_card.update_property(&[gtk::accessible::Property::Label("Browse files to add an AppImage")]);
        drop_zone_card.append(&drop_overlay_icon);
        drop_zone_card.append(&drop_overlay_title);
        drop_zone_card.append(&drop_overlay_subtitle);

        // Click on the card opens file picker (stop propagation so scrim doesn't dismiss)
        {
            let window_weak = self.downgrade();
            let card_click = gtk::GestureClick::new();
            card_click.connect_pressed(move |gesture, _, _, _| {
                gesture.set_state(gtk::EventSequenceState::Claimed);
                let Some(window) = window_weak.upgrade() else { return };
                window.open_file_picker();
            });
            drop_zone_card.add_controller(card_click);
        }

        // Keyboard activation: Enter/Space on the card opens file picker
        {
            let window_weak = self.downgrade();
            let key_ctrl = gtk::EventControllerKey::new();
            key_ctrl.connect_key_pressed(move |_ctrl, key, _code, _mods| {
                if matches!(key, gtk::gdk::Key::Return | gtk::gdk::Key::KP_Enter | gtk::gdk::Key::space) {
                    if let Some(window) = window_weak.upgrade() {
                        window.open_file_picker();
                    }
                    glib::Propagation::Stop
                } else {
                    glib::Propagation::Proceed
                }
            });
            drop_zone_card.add_controller(key_ctrl);
        }

        // Revealer for crossfade animation
        let drop_revealer = gtk::Revealer::builder()
            .transition_type(gtk::RevealerTransitionType::Crossfade)
            .transition_duration(200)
            .reveal_child(false)
            .halign(gtk::Align::Center)
            .valign(gtk::Align::Center)
            .vexpand(true)
            .hexpand(true)
            .build();
        drop_revealer.set_child(Some(&drop_zone_card));

        // Scrim (dimmed background) that fills the whole window
        let drop_overlay_content = gtk::Box::builder()
            .orientation(gtk::Orientation::Vertical)
            .halign(gtk::Align::Fill)
            .valign(gtk::Align::Fill)
            .hexpand(true)
            .vexpand(true)
            .build();
        drop_overlay_content.add_css_class("drop-overlay-scrim");
        drop_overlay_content.append(&drop_revealer);
        drop_overlay_content.set_visible(false);

        // Click on scrim (outside the card) to dismiss
        {
            let overlay_ref = drop_overlay_content.clone();
            let revealer_ref = drop_revealer.clone();
            let click = gtk::GestureClick::new();
            click.connect_pressed(move |gesture, _, _, _| {
                gesture.set_state(gtk::EventSequenceState::Claimed);
                revealer_ref.set_reveal_child(false);
                let overlay_hide = overlay_ref.clone();
                glib::timeout_add_local_once(
                    std::time::Duration::from_millis(200),
                    move || { overlay_hide.set_visible(false); },
                );
            });
            drop_overlay_content.add_controller(click);
        }

        // Escape key dismisses the overlay (WCAG 2.1.1 keyboard access)
        {
            let overlay_ref = drop_overlay_content.clone();
            let revealer_ref = drop_revealer.clone();
            let window_weak = self.downgrade();
            let key_ctrl = gtk::EventControllerKey::new();
            key_ctrl.connect_key_pressed(move |_ctrl, key, _code, _mods| {
                if key == gtk::gdk::Key::Escape && overlay_ref.is_visible() {
                    revealer_ref.set_reveal_child(false);
                    let overlay_hide = overlay_ref.clone();
                    glib::timeout_add_local_once(
                        std::time::Duration::from_millis(200),
                        move || { overlay_hide.set_visible(false); },
                    );
                    // Return focus to main content
                    if let Some(window) = window_weak.upgrade() {
                        if let Some(vs) = window.imp().view_stack.get() {
                            if let Some(child) = vs.visible_child() {
                                child.grab_focus();
                            }
                        }
                    }
                    glib::Propagation::Stop
                } else {
                    glib::Propagation::Proceed
                }
            });
            drop_overlay_content.add_controller(key_ctrl);
        }

        // Overlay wraps main content so the drop indicator sits on top
        let overlay = gtk::Overlay::new();
        overlay.set_child(Some(&main_box));
        overlay.add_overlay(&drop_overlay_content);

        // --- Drag-and-drop support ---
        let drop_target = gtk::DropTarget::new(gio::File::static_type(), gtk::gdk::DragAction::COPY);

        // Show overlay on drag enter
        {
            let drop_indicator = drop_overlay_content.clone();
            let revealer_ref = drop_revealer.clone();
            drop_target.connect_enter(move |_target, _x, _y| {
                drop_indicator.set_visible(true);
                revealer_ref.set_reveal_child(true);
                gtk::gdk::DragAction::COPY
            });
        }

        // Hide overlay on drag leave
        {
            let drop_indicator = drop_overlay_content.clone();
            let revealer_ref = drop_revealer.clone();
            drop_target.connect_leave(move |_target| {
                revealer_ref.set_reveal_child(false);
                let overlay_hide = drop_indicator.clone();
                glib::timeout_add_local_once(
                    std::time::Duration::from_millis(200),
                    move || { overlay_hide.set_visible(false); },
                );
            });
        }

        // Handle the drop
        {
            let drop_indicator = drop_overlay_content.clone();
            let revealer_ref = drop_revealer.clone();
            let toast_ref = toast_overlay.clone();
            let window_weak = self.downgrade();
            drop_target.connect_drop(move |_target, value, _x, _y| {
                revealer_ref.set_reveal_child(false);
                let overlay_hide = drop_indicator.clone();
                glib::timeout_add_local_once(
                    std::time::Duration::from_millis(200),
                    move || { overlay_hide.set_visible(false); },
                );

                let file = match value.get::<gio::File>() {
                    Ok(f) => f,
                    Err(_) => return false,
                };

                let path = match file.path() {
                    Some(p) => p,
                    None => return false,
                };

                // Validate it's an AppImage via magic bytes
                if discovery::detect_appimage(&path).is_none() {
                    toast_ref.add_toast(widgets::error_toast(&i18n("Not a valid AppImage file")));
                    return true;
                }

                let Some(window) = window_weak.upgrade() else {
                    return false;
                };

                let db = window.database().clone();
                let toast_for_dialog = toast_ref.clone();
                let window_weak2 = window.downgrade();

                drop_dialog::show_drop_dialog(
                    &window,
                    vec![path],
                    &toast_for_dialog,
                    move || {
                        if let Some(win) = window_weak2.upgrade() {
                            let lib_view = win.imp().library_view.get().unwrap();
                            match db.get_all_appimages() {
                                Ok(records) => lib_view.populate(records),
                                Err(_) => lib_view.set_state(LibraryState::Empty),
                            }
                        }
                    },
                );

                true
            });
        }

        overlay.add_controller(drop_target);

        self.set_content(Some(&overlay));

        // Wire up card/row activation to push detail view (or toggle selection)
        {
            let nav = installed_nav.clone();
            let db = self.database().clone();
            let window_weak = self.downgrade();
            library_view.connect_grid_activated(move |record_id| {
                if let Some(window) = window_weak.upgrade() {
                    let lib_view = window.imp().library_view.get().unwrap();
                    if lib_view.in_selection_mode() {
                        lib_view.toggle_selection(record_id);
                        return;
                    }
                }
                if let Ok(Some(record)) = db.get_appimage_by_id(record_id) {
                    let page = detail_view::build_detail_page(&record, &db);
                    nav.push(&page);
                }
            });
        }
        {
            let nav = installed_nav.clone();
            let db = self.database().clone();
            let window_weak = self.downgrade();
            library_view.connect_list_activated(move |record_id| {
                if let Some(window) = window_weak.upgrade() {
                    let lib_view = window.imp().library_view.get().unwrap();
                    if lib_view.in_selection_mode() {
                        lib_view.toggle_selection(record_id);
                        return;
                    }
                }
                if let Ok(Some(record)) = db.get_appimage_by_id(record_id) {
                    let page = detail_view::build_detail_page(&record, &db);
                    nav.push(&page);
                }
            });
        }

        // Refresh library view when navigating back from detail
        // (in case integration or other state changed)
        {
            let db = self.database().clone();
            let window_weak = self.downgrade();
            installed_nav.connect_popped(move |_nav, _page| {
                if let Some(window) = window_weak.upgrade() {
                    // Update window title for accessibility (WCAG 2.4.8)
                    window.set_title(Some("Driftwood"));

                    let lib_view = window.imp().library_view.get().unwrap();
                    match db.get_all_appimages() {
                        Ok(records) => lib_view.populate(records),
                        Err(_) => lib_view.set_state(LibraryState::Empty),
                    }

                    // Return focus to the visible content (WCAG 2.4.3)
                    if let Some(vs) = window.imp().view_stack.get() {
                        if let Some(child) = vs.visible_child() {
                            child.grab_focus();
                        }
                    }
                }
            });
        }

        // Update window title when navigating to sub-pages (WCAG 2.4.8 Location)
        {
            let window_weak = self.downgrade();
            installed_nav.connect_pushed(move |nav| {
                if let Some(window) = window_weak.upgrade() {
                    if let Some(page) = nav.visible_page() {
                        let page_title = page.title();
                        if !page_title.is_empty() {
                            window.set_title(Some(&format!("Driftwood - {}", page_title)));
                        }
                    }
                    // Move focus to the pushed page (WCAG 2.4.3)
                    if let Some(visible) = nav.visible_page() {
                        visible.grab_focus();
                    }
                }
            });
        }

        // Store references
        self.imp()
            .drop_overlay
            .set(drop_overlay_content)
            .expect("DropOverlay already set");
        self.imp()
            .drop_revealer
            .set(drop_revealer)
            .expect("DropRevealer already set");
        self.imp()
            .toast_overlay
            .set(toast_overlay)
            .expect("ToastOverlay already set");
        self.imp()
            .view_stack
            .set(view_stack)
            .expect("ViewStack already set");
        self.imp()
            .installed_nav
            .set(installed_nav)
            .expect("InstalledNav already set");
        if self.imp().library_view.set(library_view).is_err() {
            panic!("LibraryView already set");
        }
        self.imp()
            .enrichment_banner
            .set(enrichment_banner)
            .expect("EnrichmentBanner already set");

        // Set up window actions
        self.setup_window_actions();
    }

    fn setup_window_actions(&self) {
        let dashboard_action = gio::ActionEntry::builder("dashboard")
            .activate(|window: &Self, _, _| {
                let db = window.database().clone();
                let nav = window.imp().installed_nav.get().unwrap();
                let page = dashboard::build_dashboard_page(&db);
                nav.push(&page);
            })
            .build();

        // Preferences action
        let preferences_action = gio::ActionEntry::builder("preferences")
            .activate(|window: &Self, _, _| {
                preferences::show_preferences_dialog(window);
            })
            .build();

        // Scan action - runs real scan
        let scan_action = gio::ActionEntry::builder("scan")
            .activate(|window: &Self, _, _| {
                window.trigger_scan(None);
            })
            .build();

        // Clean orphans action
        let clean_toast = self.imp().toast_overlay.get().unwrap().clone();
        let clean_action = gio::ActionEntry::builder("clean-orphans")
            .activate(move |_window: &Self, _, _| {
                let toast_ref = clean_toast.clone();
                glib::spawn_future_local(async move {
                    let result = gio::spawn_blocking(|| {
                        orphan::clean_all_orphans()
                    })
                    .await;

                    match result {
                        Ok(Ok(summary)) => {
                            let msg = format!(
                                "{} {} {}, {} {}",
                                i18n("Cleaned"),
                                summary.entries_removed,
                                i18n("desktop entries"),
                                summary.icons_removed,
                                i18n("icons"),
                            );
                            toast_ref.add_toast(widgets::info_toast(&msg));
                        }
                        _ => {
                            toast_ref.add_toast(widgets::error_toast(&i18n("Failed to clean orphaned entries")));
                        }
                    }
                });
            })
            .build();

        // Search action - toggles search bar
        let search_action = gio::ActionEntry::builder("search")
            .activate(|window: &Self, _, _| {
                let lib_view = window.imp().library_view.get().unwrap();
                lib_view.toggle_search();
            })
            .build();

        // Check for updates action
        let updates_toast = self.imp().toast_overlay.get().unwrap().clone();
        let check_updates_action = gio::ActionEntry::builder("check-updates")
            .activate(move |_window: &Self, _, _| {
                let toast_ref = updates_toast.clone();
                glib::spawn_future_local(async move {
                    let result = gio::spawn_blocking(move || {
                        let bg_db = Database::open().expect("Failed to open database");
                        update_dialog::batch_check_updates(&bg_db)
                    })
                    .await;

                    match result {
                        Ok(0) => {
                            toast_ref.add_toast(widgets::info_toast(&i18n("All AppImages are up to date")));
                        }
                        Ok(n) => {
                            let msg = ni18n_f(
                                "{count} update available",
                                "{count} updates available",
                                n as u32,
                                &[("{count}", &n.to_string())],
                            );
                            toast_ref.add_toast(widgets::info_toast(&msg));
                        }
                        Err(_) => {
                            toast_ref.add_toast(widgets::error_toast(&i18n("Failed to check for updates")));
                        }
                    }
                });
            })
            .build();

        // Find duplicates action
        let find_duplicates_action = gio::ActionEntry::builder("find-duplicates")
            .activate(|window: &Self, _, _| {
                let db = window.database().clone();
                let toast_overlay = window.imp().toast_overlay.get().unwrap();
                duplicate_dialog::show_duplicate_dialog(window, &db, toast_overlay);
            })
            .build();

        // Security report action
        let security_report_action = gio::ActionEntry::builder("security-report")
            .activate(|window: &Self, _, _| {
                let db = window.database().clone();
                let nav = window.imp().installed_nav.get().unwrap();
                let page = security_report::build_security_report_page(&db);
                nav.push(&page);
            })
            .build();

        // Disk cleanup wizard action
        let cleanup_action = gio::ActionEntry::builder("cleanup")
            .activate(|window: &Self, _, _| {
                let db = window.database().clone();
                cleanup_wizard::show_cleanup_wizard(window, &db);
            })
            .build();

        // Catalog browser action - switches to catalog tab
        let catalog_action = gio::ActionEntry::builder("catalog")
            .activate(|window: &Self, _, _| {
                let view_stack = window.imp().view_stack.get().unwrap();
                view_stack.set_visible_child_name("catalog");
                if let Some(child) = view_stack.visible_child() {
                    child.grab_focus();
                }
            })
            .build();

        // Show keyboard shortcuts dialog
        let shortcuts_action = gio::ActionEntry::builder("show-shortcuts")
            .activate(|window: &Self, _, _| {
                window.show_shortcuts_dialog();
            })
            .build();

        // Show drop overlay hint (triggered by "Add app" button)
        let show_drop_hint_action = gio::ActionEntry::builder("show-drop-hint")
            .activate(|window: &Self, _, _| {
                if let Some(overlay) = window.imp().drop_overlay.get() {
                    overlay.set_visible(true);
                    if let Some(revealer) = window.imp().drop_revealer.get() {
                        revealer.set_reveal_child(true);
                        // Move focus into the drop zone card (WCAG 2.4.3)
                        if let Some(card) = revealer.child() {
                            card.grab_focus();
                        }
                    }
                    // Announce to screen readers
                    widgets::announce(overlay, "Drop zone opened. Drop an AppImage file or press Enter to browse.");
                }
            })
            .build();

        // Export app list action
        let export_action = gio::ActionEntry::builder("export-app-list")
            .activate(|window: &Self, _, _| {
                window.show_export_dialog();
            })
            .build();

        // Import app list action
        let import_action = gio::ActionEntry::builder("import-app-list")
            .activate(|window: &Self, _, _| {
                window.show_import_dialog();
            })
            .build();

        self.add_action_entries([
            dashboard_action,
            preferences_action,
            scan_action,
            clean_action,
            search_action,
            check_updates_action,
            find_duplicates_action,
            security_report_action,
            cleanup_action,
            catalog_action,
            shortcuts_action,
            show_drop_hint_action,
            export_action,
            import_action,
        ]);

        // Sort library action (parameterized with sort mode string)
        let sort_action = gio::SimpleAction::new("sort-library", Some(glib::VariantTy::STRING));
        {
            let window_weak = self.downgrade();
            sort_action.connect_activate(move |_, param| {
                let Some(window) = window_weak.upgrade() else { return };
                let Some(mode_str) = param.and_then(|p| p.get::<String>()) else { return };
                let lib_view = window.imp().library_view.get().unwrap();
                let sort_mode = match mode_str.as_str() {
                    "recent" => crate::ui::library_view::SortMode::RecentlyAdded,
                    "size" => crate::ui::library_view::SortMode::Size,
                    _ => crate::ui::library_view::SortMode::NameAsc,
                };
                lib_view.set_sort_mode(sort_mode);

                // Announce sort change for screen readers (WCAG 4.1.3)
                let sort_label = match mode_str.as_str() {
                    "recent" => "Sorted by recently added",
                    "size" => "Sorted by size",
                    _ => "Sorted by name",
                };
                if let Some(toast_overlay) = window.imp().toast_overlay.get() {
                    let toast = adw::Toast::builder()
                        .title(sort_label)
                        .timeout(2)
                        .build();
                    toast_overlay.add_toast(toast);
                }

                let settings_key = match mode_str.as_str() {
                    "recent" => "recently-added",
                    "size" => "size",
                    _ => "name",
                };
                let settings = gio::Settings::new(APP_ID);
                settings.set_string("sort-mode", settings_key).ok();
            });
        }
        self.add_action(&sort_action);

        // --- Batch actions ---
        let batch_integrate_action = gio::SimpleAction::new("batch-integrate", None);
        {
            let window_weak = self.downgrade();
            batch_integrate_action.connect_activate(move |_, _| {
                let Some(window) = window_weak.upgrade() else { return };
                let lib_view = window.imp().library_view.get().unwrap();
                let ids = lib_view.selected_ids();
                if ids.is_empty() { return; }
                let db = window.database().clone();
                let toast_overlay = window.imp().toast_overlay.get().unwrap().clone();
                let mut count = 0;
                for id in &ids {
                    if let Ok(Some(record)) = db.get_appimage_by_id(*id) {
                        if !record.integrated {
                            if let Ok(result) = integrator::integrate_tracked(&record, &db) {
                                let desktop_path = result.desktop_file_path.to_string_lossy().to_string();
                                db.set_integrated(*id, true, Some(&desktop_path)).ok();
                                count += 1;
                            }
                        }
                    }
                }
                lib_view.exit_selection_mode();
                if count > 0 {
                    toast_overlay.add_toast(widgets::info_toast(&ni18n_f("Integrated {} app", "Integrated {} apps", count as u32, &[("{}", &count.to_string())])));
                    if let Ok(records) = db.get_all_appimages() {
                        lib_view.populate(records);
                    }
                }
            });
        }
        self.add_action(&batch_integrate_action);

        let batch_delete_action = gio::SimpleAction::new("batch-delete", None);
        {
            let window_weak = self.downgrade();
            batch_delete_action.connect_activate(move |_, _| {
                let Some(window) = window_weak.upgrade() else { return };
                let lib_view = window.imp().library_view.get().unwrap();
                let ids = lib_view.selected_ids();
                if ids.is_empty() { return; }
                let db = window.database().clone();
                let toast_overlay = window.imp().toast_overlay.get().unwrap().clone();
                let mut count = 0;
                for id in &ids {
                    if let Ok(Some(record)) = db.get_appimage_by_id(*id) {
                        if record.integrated {
                            integrator::undo_all_modifications(&db, *id).ok();
                            integrator::remove_integration(&record).ok();
                        }
                        std::fs::remove_file(&record.path).ok();
                        db.remove_appimage(*id).ok();
                        count += 1;
                    }
                }
                lib_view.exit_selection_mode();
                if count > 0 {
                    toast_overlay.add_toast(widgets::info_toast(&ni18n_f("Deleted {} app", "Deleted {} apps", count as u32, &[("{}", &count.to_string())])));
                    if let Ok(records) = db.get_all_appimages() {
                        lib_view.populate(records);
                    }
                }
            });
        }
        self.add_action(&batch_delete_action);

        let update_all_action = gio::SimpleAction::new("update-all", None);
        {
            let window_weak = self.downgrade();
            update_all_action.connect_activate(move |_, _| {
                let Some(window) = window_weak.upgrade() else { return };
                let db = window.database();
                crate::ui::batch_update_dialog::show_batch_update_dialog(&window, db);
            });
        }
        self.add_action(&update_all_action);

        let fix_fuse_action = gio::SimpleAction::new("fix-fuse", None);
        {
            let window_weak = self.downgrade();
            fix_fuse_action.connect_activate(move |_, _| {
                let Some(window) = window_weak.upgrade() else { return };
                crate::ui::fuse_wizard::show_fuse_wizard(&window);
            });
        }
        self.add_action(&fix_fuse_action);

        // --- Context menu actions (parameterized with record ID) ---
        let param_type = Some(glib::VariantTy::INT64);

        // Launch action
        let launch_action = gio::SimpleAction::new("launch-appimage", param_type);
        {
            let window_weak = self.downgrade();
            launch_action.connect_activate(move |_, param| {
                let Some(window) = window_weak.upgrade() else { return };
                let Some(record_id) = param.and_then(|p| p.get::<i64>()) else { return };
                let toast_overlay = window.imp().toast_overlay.get().unwrap().clone();
                let window_ref = window.clone();
                let (path_str, app_name, launch_args_raw) = {
                    let db = window.database();
                    match db.get_appimage_by_id(record_id) {
                        Ok(Some(r)) => {
                            let name = r.app_name.clone().unwrap_or_else(|| r.filename.clone());
                            (r.path.clone(), name, r.launch_args.clone())
                        }
                        _ => return,
                    }
                };
                let launch_args = launcher::parse_launch_args(launch_args_raw.as_deref());
                glib::spawn_future_local(async move {
                    let path_bg = path_str.clone();
                    let result = gio::spawn_blocking(move || {
                        let bg_db = crate::core::database::Database::open().expect("DB open");
                        let appimage_path = std::path::Path::new(&path_bg);
                        launcher::launch_appimage(&bg_db, record_id, appimage_path, "gui_context", &launch_args, &[])
                    }).await;
                    match result {
                        Ok(launcher::LaunchResult::Started { pid, method }) => {
                            log::info!("Launched: {} (PID: {}, method: {})", path_str, pid, method.as_str());
                        }
                        Ok(launcher::LaunchResult::Crashed { exit_code, stderr, method }) => {
                            log::error!("App crashed (exit {}, method: {}): {}", exit_code.unwrap_or(-1), method.as_str(), stderr);
                            widgets::show_crash_dialog(&window_ref, &app_name, exit_code, &stderr);
                        }
                        Ok(launcher::LaunchResult::Failed(msg)) => {
                            log::error!("Failed to launch: {}", msg);
                            toast_overlay.add_toast(widgets::error_toast(&i18n_f("Could not launch: {error}", &[("{error}", &msg)])));
                        }
                        Err(_) => {
                            log::error!("Launch task panicked");
                        }
                    }
                });
            });
        }
        self.add_action(&launch_action);

        // Check for updates action (per-app)
        let check_update_action = gio::SimpleAction::new("check-update", param_type);
        {
            let window_weak = self.downgrade();
            check_update_action.connect_activate(move |_, param| {
                let Some(window) = window_weak.upgrade() else { return };
                let Some(record_id) = param.and_then(|p| p.get::<i64>()) else { return };
                let toast_overlay = window.imp().toast_overlay.get().unwrap().clone();
                glib::spawn_future_local(async move {
                    let result = gio::spawn_blocking(move || {
                        let bg_db = Database::open().expect("DB open failed");
                        if let Ok(Some(record)) = bg_db.get_appimage_by_id(record_id) {
                            let appimage_path = std::path::Path::new(&record.path);
                            if !appimage_path.exists() {
                                return false;
                            }
                            let (_type_label, raw_info, check_result) = updater::check_appimage_for_update(
                                appimage_path,
                                record.app_version.as_deref(),
                            );
                            if raw_info.is_some() {
                                bg_db.update_update_info(record_id, raw_info.as_deref(), None).ok();
                                if let Some(source) = updater::detect_source_url(raw_info.as_deref()) {
                                    bg_db.set_source_url(record_id, Some(&source)).ok();
                                }
                            }
                            if let Some(result) = check_result {
                                if result.update_available {
                                    if let Some(ref version) = result.latest_version {
                                        bg_db.set_update_available(record_id, Some(version), result.download_url.as_deref()).ok();
                                        return true;
                                    }
                                } else {
                                    bg_db.clear_update_available(record_id).ok();
                                }
                            }
                        }
                        false
                    }).await;
                    match result {
                        Ok(true) => toast_overlay.add_toast(widgets::info_toast(&i18n("Update available!"))),
                        Ok(false) => toast_overlay.add_toast(widgets::info_toast(&i18n("Already up to date"))),
                        Err(_) => toast_overlay.add_toast(widgets::error_toast(&i18n("Update check failed"))),
                    }
                });
            });
        }
        self.add_action(&check_update_action);

        // Scan for vulnerabilities (per-app)
        let scan_security_action = gio::SimpleAction::new("scan-security", param_type);
        {
            let window_weak = self.downgrade();
            scan_security_action.connect_activate(move |_, param| {
                let Some(window) = window_weak.upgrade() else { return };
                let Some(record_id) = param.and_then(|p| p.get::<i64>()) else { return };
                let toast_overlay = window.imp().toast_overlay.get().unwrap().clone();
                glib::spawn_future_local(async move {
                    let result = gio::spawn_blocking(move || {
                        let bg_db = Database::open().expect("DB open failed");
                        if let Ok(Some(record)) = bg_db.get_appimage_by_id(record_id) {
                            let appimage_path = std::path::Path::new(&record.path);
                            let scan_result = security::scan_and_store(&bg_db, record_id, appimage_path);
                            return Some(scan_result.total_cves());
                        }
                        None
                    }).await;
                    match result {
                        Ok(Some(total)) => {
                            if total == 0 {
                                toast_overlay.add_toast(widgets::info_toast(&i18n("No vulnerabilities found")));
                            } else {
                                let msg = ni18n_f("Found {} CVE", "Found {} CVEs", total as u32, &[("{}", &total.to_string())]);
                                toast_overlay.add_toast(widgets::info_toast(&msg));
                            }

                            // Send desktop notifications for new CVE findings if enabled
                            let settings = gio::Settings::new(APP_ID);
                            if settings.boolean("security-notifications") {
                                let threshold = settings.string("security-notification-threshold").to_string();
                                glib::spawn_future_local(async move {
                                    let _ = gio::spawn_blocking(move || {
                                        let bg_db = Database::open().expect("Failed to open database");
                                        notification::check_and_notify(&bg_db, &threshold);
                                    })
                                    .await;
                                });
                            }
                        }
                        _ => toast_overlay.add_toast(widgets::error_toast(&i18n("Security scan failed"))),
                    }
                });
            });
        }
        self.add_action(&scan_security_action);

        // Toggle integration
        let toggle_integration_action = gio::SimpleAction::new("toggle-integration", param_type);
        {
            let window_weak = self.downgrade();
            toggle_integration_action.connect_activate(move |_, param| {
                let Some(window) = window_weak.upgrade() else { return };
                let Some(record_id) = param.and_then(|p| p.get::<i64>()) else { return };
                let db = window.database().clone();
                let toast_overlay = window.imp().toast_overlay.get().unwrap().clone();
                if let Ok(Some(record)) = db.get_appimage_by_id(record_id) {
                    if record.integrated {
                        // Remove integration immediately, offer undo toast
                        integrator::undo_all_modifications(&db, record_id).ok();
                        integrator::remove_integration(&record).ok();
                        db.set_integrated(record_id, false, None).ok();

                        let undo_toast = adw::Toast::builder()
                            .title(i18n("Removed from app menu"))
                            .button_label(i18n("Undo"))
                            .timeout(7)
                            .build();

                        let db_undo = db.clone();
                        let window_weak_undo = window.downgrade();
                        undo_toast.connect_button_clicked(move |_| {
                            let Some(win) = window_weak_undo.upgrade() else { return };
                            if let Ok(Some(rec)) = db_undo.get_appimage_by_id(record_id) {
                                match integrator::integrate_tracked(&rec, &db_undo) {
                                    Ok(result) => {
                                        let desktop_path = result.desktop_file_path.to_string_lossy().to_string();
                                        db_undo.set_integrated(record_id, true, Some(&desktop_path)).ok();
                                    }
                                    Err(e) => log::error!("Re-integration failed: {}", e),
                                }
                                let lib_view = win.imp().library_view.get().unwrap();
                                if let Ok(records) = db_undo.get_all_appimages() {
                                    lib_view.populate(records);
                                }
                            }
                        });
                        toast_overlay.add_toast(undo_toast);
                    } else {
                        match integrator::integrate_tracked(&record, &db) {
                            Ok(result) => {
                                let desktop_path = result.desktop_file_path.to_string_lossy().to_string();
                                db.set_integrated(record_id, true, Some(&desktop_path)).ok();
                                toast_overlay.add_toast(widgets::info_toast(&i18n("Integrated into desktop menu")));
                            }
                            Err(e) => {
                                log::error!("Integration failed: {}", e);
                                toast_overlay.add_toast(widgets::error_toast(&i18n("Integration failed")));
                            }
                        }
                    }
                    // Refresh library view
                    let lib_view = window.imp().library_view.get().unwrap();
                    match db.get_all_appimages() {
                        Ok(records) => lib_view.populate(records),
                        Err(_) => {}
                    }
                }
            });
        }
        self.add_action(&toggle_integration_action);

        // Open containing folder
        let open_folder_action = gio::SimpleAction::new("open-folder", param_type);
        {
            let window_weak = self.downgrade();
            open_folder_action.connect_activate(move |_, param| {
                let Some(window) = window_weak.upgrade() else { return };
                let Some(record_id) = param.and_then(|p| p.get::<i64>()) else { return };
                let db = window.database().clone();
                if let Ok(Some(record)) = db.get_appimage_by_id(record_id) {
                    let file = gio::File::for_path(&record.path);
                    let file_launcher = gtk::FileLauncher::new(Some(&file));
                    file_launcher.open_containing_folder(gtk::Window::NONE, None::<&gio::Cancellable>, |_| {});
                }
            });
        }
        self.add_action(&open_folder_action);

        // Copy path to clipboard
        let copy_path_action = gio::SimpleAction::new("copy-path", param_type);
        {
            let window_weak = self.downgrade();
            copy_path_action.connect_activate(move |_, param| {
                let Some(window) = window_weak.upgrade() else { return };
                let Some(record_id) = param.and_then(|p| p.get::<i64>()) else { return };
                let db = window.database().clone();
                let toast_overlay = window.imp().toast_overlay.get().unwrap().clone();
                if let Ok(Some(record)) = db.get_appimage_by_id(record_id) {
                    let display = gtk::prelude::WidgetExt::display(&window);
                    let clipboard = display.clipboard();
                    clipboard.set_text(&record.path);
                    toast_overlay.add_toast(widgets::info_toast(&i18n("Path copied to clipboard")));
                }
            });
        }
        self.add_action(&copy_path_action);

        // Uninstall action (from right-click context menu)
        let uninstall_action = gio::SimpleAction::new("uninstall-appimage", param_type);
        {
            let window_weak = self.downgrade();
            uninstall_action.connect_activate(move |_, param| {
                let Some(window) = window_weak.upgrade() else { return };
                let Some(record_id) = param.and_then(|p| p.get::<i64>()) else { return };
                let db = window.database().clone();
                let toast_overlay = window.imp().toast_overlay.get().unwrap().clone();
                if let Ok(Some(record)) = db.get_appimage_by_id(record_id) {
                    let fp = footprint::get_footprint(&db, record.id, record.size_bytes as u64);
                    let fp_paths: Vec<(String, String, u64)> = fp.paths.iter()
                        .filter(|p| p.exists)
                        .map(|p| (
                            p.path.to_string_lossy().to_string(),
                            p.path_type.label().to_string(),
                            p.size_bytes,
                        ))
                        .collect();
                    let is_integrated = record.integrated;
                    let window_ref = window.clone();
                    let db_refresh = db.clone();
                    detail_view::show_uninstall_dialog_with_callback(
                        &toast_overlay,
                        &record,
                        &db,
                        is_integrated,
                        &fp_paths,
                        Some(Rc::new(move || {
                            // Refresh the library view after uninstall (or undo)
                            if let Some(lib_view) = window_ref.imp().library_view.get() {
                                if let Ok(records) = db_refresh.get_all_appimages() {
                                    lib_view.populate(records);
                                }
                            }
                        })),
                    );
                }
            });
        }
        self.add_action(&uninstall_action);

        // View switching actions for keyboard shortcuts
        let show_installed_action = gio::SimpleAction::new("show-installed", None);
        {
            let window_weak = self.downgrade();
            show_installed_action.connect_activate(move |_, _| {
                let Some(window) = window_weak.upgrade() else { return };
                if let Some(vs) = window.imp().view_stack.get() {
                    vs.set_visible_child_name("installed");
                    if let Some(child) = vs.visible_child() {
                        child.grab_focus();
                    }
                }
            });
        }
        self.add_action(&show_installed_action);

        let show_catalog_action = gio::SimpleAction::new("show-catalog", None);
        {
            let window_weak = self.downgrade();
            show_catalog_action.connect_activate(move |_, _| {
                let Some(window) = window_weak.upgrade() else { return };
                if let Some(vs) = window.imp().view_stack.get() {
                    vs.set_visible_child_name("catalog");
                    if let Some(child) = vs.visible_child() {
                        child.grab_focus();
                    }
                }
            });
        }
        self.add_action(&show_catalog_action);

        let show_updates_action = gio::SimpleAction::new("show-updates", None);
        {
            let window_weak = self.downgrade();
            show_updates_action.connect_activate(move |_, _| {
                let Some(window) = window_weak.upgrade() else { return };
                if let Some(vs) = window.imp().view_stack.get() {
                    vs.set_visible_child_name("updates");
                    if let Some(child) = vs.visible_child() {
                        child.grab_focus();
                    }
                }
            });
        }
        self.add_action(&show_updates_action);

        // Command palette (Ctrl+K)
        let palette_action = gio::SimpleAction::new("command-palette", None);
        {
            let window_weak = self.downgrade();
            palette_action.connect_activate(move |_, _| {
                let Some(window) = window_weak.upgrade() else { return };
                window.show_command_palette();
            });
        }
        self.add_action(&palette_action);

        // Keyboard shortcuts
        if let Some(app) = self.application() {
            let gtk_app = app.downcast_ref::<gtk::Application>().unwrap();
            gtk_app.set_accels_for_action("win.scan", &["<Control>r", "F5"]);
            gtk_app.set_accels_for_action("win.search", &["<Control>f"]);
            gtk_app.set_accels_for_action("win.preferences", &["<Control>comma"]);
            gtk_app.set_accels_for_action("win.dashboard", &["<Control>d"]);
            gtk_app.set_accels_for_action("win.check-updates", &["<Control>u"]);
            gtk_app.set_accels_for_action("win.show-shortcuts", &["<Control>question"]);
            gtk_app.set_accels_for_action("win.show-installed", &["<Control>1"]);
            gtk_app.set_accels_for_action("win.show-catalog", &["<Control>2"]);
            gtk_app.set_accels_for_action("win.show-updates", &["<Control>3"]);
            gtk_app.set_accels_for_action("win.command-palette", &["<Control>k"]);
        }
    }

    fn load_initial_data(&self) {
        let db = self.database();
        let library_view = self.imp().library_view.get().unwrap();

        // Ensure default catalog sources exist
        crate::core::catalog::ensure_default_sources(db);

        match db.get_all_appimages() {
            Ok(records) if !records.is_empty() => {
                library_view.populate(records);
            }
            _ => {
                // Empty database - show empty state and default to Catalog tab
                library_view.set_state(LibraryState::Empty);
                if let Some(view_stack) = self.imp().view_stack.get() {
                    view_stack.set_visible_child_name("catalog");
                }
            }
        }

        // Scan on startup if enabled in preferences
        if self.settings().boolean("auto-scan-on-startup") {
            let scan_toast = if let Some(toast_overlay) = self.imp().toast_overlay.get() {
                let toast = adw::Toast::builder()
                    .title(&i18n("Scanning for apps in your configured folders..."))
                    .timeout(10)
                    .build();
                toast_overlay.add_toast(toast.clone());
                Some(toast)
            } else {
                None
            };
            self.trigger_scan(scan_toast);
        }

        // Start watching scan directories for new AppImage files
        self.start_file_watcher();

        // Auto-cleanup old backups based on retention setting
        let retention_days = self.settings().int("backup-retention-days") as u32;
        glib::spawn_future_local(async move {
            let _ = gio::spawn_blocking(move || {
                let bg_db = Database::open().expect("Failed to open database");
                match crate::core::backup::auto_cleanup_old_backups(&bg_db, retention_days) {
                    Ok(removed) if removed > 0 => {
                        log::info!("Auto-cleaned {} old backup(s) (retention: {} days)", removed, retention_days);
                    }
                    Err(e) => log::warn!("Backup auto-cleanup failed: {}", e),
                    _ => {}
                }
            })
            .await;
        });

        // Run background security scan and notify if auto-security-scan is enabled
        let settings_sec = self.settings().clone();
        if settings_sec.boolean("auto-security-scan") {
            let threshold = settings_sec.string("security-notification-threshold").to_string();
            glib::spawn_future_local(async move {
                let _ = gio::spawn_blocking(move || {
                    let bg_db = Database::open().expect("Failed to open database");
                    let notifications = notification::scan_and_notify(&bg_db, &threshold);
                    for n in &notifications {
                        log::info!(
                            "CVE notification sent: app={} (id={}), severity={}, count={}",
                            n.app_name, n.appimage_id, n.severity, n.cve_count,
                        );
                    }
                })
                .await;
            });
        }

        // Scheduled background update check
        let settings_upd = self.settings().clone();
        if settings_upd.boolean("auto-check-updates") {
            let last_check = settings_upd.string("last-update-check").to_string();
            let interval_hours = settings_upd.int("update-check-interval-hours") as i64;
            let should_check = if last_check.is_empty() {
                true
            } else if let Ok(last) = chrono::NaiveDateTime::parse_from_str(&last_check, "%Y-%m-%d %H:%M:%S") {
                let now = chrono::Utc::now().naive_utc();
                let elapsed = now.signed_duration_since(last);
                elapsed.num_hours() >= interval_hours
            } else {
                true
            };
            if should_check {
                let settings_save = settings_upd.clone();
                let update_toast = self.imp().toast_overlay.get().cloned();
                glib::spawn_future_local(async move {
                    let result = gio::spawn_blocking(move || {
                        let bg_db = Database::open().expect("DB open failed");
                        update_dialog::batch_check_updates_detailed(&bg_db)
                    })
                    .await;
                    if let Ok((count, names)) = result {
                        log::info!("Background update check: {} updates available", count);
                        if count > 0 {
                            if let Some(toast_overlay) = update_toast {
                                let title = if names.len() <= 3 {
                                    i18n_f("Updates available: {apps}", &[("{apps}", &names.join(", "))])
                                } else {
                                    i18n_f("{count} app updates available ({apps}, ...)",
                                        &[("{count}", &count.to_string()), ("{apps}", &names[..2].join(", "))])
                                };
                                let toast = adw::Toast::builder()
                                    .title(&title)
                                    .button_label(i18n("View"))
                                    .action_name("win.show-updates")
                                    .timeout(5)
                                    .build();
                                toast_overlay.add_toast(toast);
                            }
                        }
                    }
                    let now = chrono::Utc::now().format("%Y-%m-%d %H:%M:%S").to_string();
                    settings_save.set_string("last-update-check", &now).ok();
                });
            }
        }

        // Check for orphaned desktop entries in the background
        let toast_overlay = self.imp().toast_overlay.get().unwrap().clone();
        glib::spawn_future_local(async move {
            let result = gio::spawn_blocking(|| {
                orphan::detect_orphans().len()
            })
            .await;

            if let Ok(count) = result {
                if count > 0 {
                    let msg = ni18n_f(
                        "{} orphaned desktop entry found. Use 'Clean' to remove it.",
                        "{} orphaned desktop entries found. Use 'Clean' to remove them.",
                        count as u32,
                        &[("{}", &count.to_string())],
                    );
                    let toast = adw::Toast::builder()
                        .title(&msg)
                        .timeout(5)
                        .button_label(&i18n("Clean"))
                        .action_name("win.clean-orphans")
                        .build();
                    toast_overlay.add_toast(toast);
                }
            }
        });

        // Update badge on Updates tab
        self.refresh_update_badge();

        // Background GitHub enrichment
        self.start_background_enrichment();
    }

    fn start_background_enrichment(&self) {
        let settings = self.settings().clone();
        if !settings.boolean("catalog-auto-enrich") {
            return;
        }

        let db = self.database().clone();

        // Check if there are unenriched apps
        let (enriched, total) = db.catalog_enrichment_progress().unwrap_or((0, 0));
        if total == 0 || enriched >= total {
            return;
        }

        let banner = self.imp().enrichment_banner.get().cloned();
        let view_stack = self.imp().view_stack.get().cloned();

        // Show banner initially
        if let Some(ref b) = banner {
            b.set_visible(true);
            if let Some(label) = find_child_by_name(b, "enrich-label") {
                if let Ok(l) = label.downcast::<gtk::Label>() {
                    l.set_label(&format!("Enriching app data from GitHub ({}/{})...", enriched, total));
                }
            }
        }

        // Channel for progress updates
        let (tx, rx) = std::sync::mpsc::channel::<(i64, i64, bool)>();

        let token = settings.string("github-token").to_string();

        // Background thread: runs batch enrichment
        glib::spawn_future_local(async move {
            let tx_c = tx.clone();

            gio::spawn_blocking(move || {
                let bg_db = match crate::core::database::Database::open() {
                    Ok(db) => db,
                    Err(_) => return,
                };

                loop {
                    let result = crate::core::github_enrichment::background_enrich_batch(
                        &bg_db,
                        &token,
                        20,
                        &|done, total| {
                            tx_c.send((done, total, false)).ok();
                        },
                    );

                    match result {
                        Ok((count, should_continue)) => {
                            if count == 0 || !should_continue {
                                // Done or rate limited
                                if let Ok((done, total)) = bg_db.catalog_enrichment_progress() {
                                    tx_c.send((done, total, true)).ok();
                                }
                                break;
                            }
                        }
                        Err(e) => {
                            log::warn!("Background enrichment error: {}", e);
                            tx_c.send((0, 0, true)).ok();
                            break;
                        }
                    }
                }
            }).await.ok();
        });

        // Poll progress on the UI thread.
        // Timer keeps running until enrichment is truly complete (all apps enriched).
        // When rate-limited, shows a paused message but keeps the banner visible.
        let banner_ref = banner;
        glib::timeout_add_local(std::time::Duration::from_millis(250), move || {
            while let Ok((done, total, finished)) = rx.try_recv() {
                if let Some(ref b) = banner_ref {
                    if finished {
                        if total == 0 || done >= total {
                            // Truly done - all apps enriched (or error with no data)
                            b.set_visible(false);
                            return glib::ControlFlow::Break;
                        }
                        // Rate limited or paused - keep banner visible with updated text
                        if let Some(spinner) = find_child_by_name(b, "enrich-spinner") {
                            if let Ok(s) = spinner.downcast::<gtk::Spinner>() {
                                s.set_spinning(false);
                            }
                        }
                        if let Some(label) = find_child_by_name(b, "enrich-label") {
                            if let Ok(l) = label.downcast::<gtk::Label>() {
                                l.set_label(&format!(
                                    "Enriching paused - rate limit ({}/{} enriched)", done, total,
                                ));
                            }
                        }
                    } else {
                        // Active progress update
                        if let Some(label) = find_child_by_name(b, "enrich-label") {
                            if let Ok(l) = label.downcast::<gtk::Label>() {
                                l.set_label(&format!(
                                    "Enriching app data from GitHub ({}/{})...", done, total,
                                ));
                            }
                        }
                    }
                }
            }

            // Show banner only when on the catalog tab
            if let Some(ref vs) = view_stack {
                let on_catalog = vs.visible_child_name()
                    .map(|n| n == "catalog")
                    .unwrap_or(false);
                if let Some(ref b) = banner_ref {
                    b.set_visible(on_catalog);
                }
            }

            glib::ControlFlow::Continue
        });
    }

    fn trigger_scan(&self, scan_toast: Option<adw::Toast>) {
        let library_view = self.imp().library_view.get().unwrap();
        library_view.set_state(LibraryState::Loading);

        let settings = self.settings();
        let mut dirs: Vec<String> = settings
            .strv("scan-directories")
            .iter()
            .map(|s| s.to_string())
            .collect();

        // Include system-wide AppImage directory if it exists
        let system_dir = crate::config::SYSTEM_APPIMAGE_DIR;
        if std::path::Path::new(system_dir).is_dir() && !dirs.iter().any(|d| d == system_dir) {
            dirs.push(system_dir.to_string());
        }

        // Include removable media mount points if enabled
        if settings.boolean("watch-removable-media") {
            for mount in crate::core::portable::detect_removable_mounts() {
                let mp = mount.mount_point.to_string_lossy().to_string();
                if !dirs.iter().any(|d| d == &mp) {
                    log::info!("Including removable mount: {} ({}, {})", mp, mount.device, mount.fs_type);
                    dirs.push(mp);
                }
            }
        }

        let toast_overlay = self.imp().toast_overlay.get().unwrap().clone();
        let window_weak = self.downgrade();

        // Two-pass scan:
        // Fast pass: discover files, upsert into DB, mark pending analysis
        // Background pass: heavy analysis per file
        glib::spawn_future_local(async move {
            // Fast registration
            let result = gio::spawn_blocking(move || {
                let bg_db = Database::open().expect("Failed to open database for scan");
                let start = Instant::now();
                let discovered = discovery::scan_directories(&dirs);

                let mut new_count = 0i32;
                let total = discovered.len() as i32;

                // Clean stale DB records for files that no longer exist
                let removed = bg_db.remove_missing_appimages().unwrap_or_default();
                let removed_count = removed.len() as i32;

                let mut skipped_count = 0i32;
                let mut needs_analysis: Vec<(i64, std::path::PathBuf, discovery::AppImageType)> = Vec::new();

                for d in &discovered {
                    let existing = bg_db
                        .get_appimage_by_path(&d.path.to_string_lossy())
                        .ok()
                        .flatten();

                    let modified = d.modified_time
                        .and_then(|t| t.duration_since(std::time::UNIX_EPOCH).ok())
                        .and_then(|dur| {
                            chrono::DateTime::from_timestamp(dur.as_secs() as i64, 0)
                                .map(|dt| dt.format("%Y-%m-%d %H:%M:%S").to_string())
                        });

                    // Skip re-processing unchanged files that are fully analyzed.
                    // Trust analysis_status as the primary signal - some AppImages
                    // genuinely don't have app_name or other optional fields.
                    if let Some(ref ex) = existing {
                        let size_unchanged = ex.size_bytes == d.size_bytes as i64;
                        let mtime_unchanged = modified.as_deref() == ex.file_modified.as_deref();
                        let analysis_done = ex.analysis_status.as_deref() == Some("complete");
                        let has_icon = ex.icon_path.is_some();
                        if size_unchanged && mtime_unchanged && analysis_done && has_icon {
                            skipped_count += 1;
                            continue;
                        }
                    }

                    let id = bg_db.upsert_appimage(
                        &d.path.to_string_lossy(),
                        &d.filename,
                        Some(d.appimage_type.as_i32()),
                        d.size_bytes as i64,
                        d.is_executable,
                        modified.as_deref(),
                    ).unwrap_or(0);

                    if existing.is_none() {
                        new_count += 1;
                    }

                    // Detect portable/removable media
                    if crate::core::portable::is_path_on_removable(&d.path) {
                        let mounts = crate::core::portable::detect_removable_mounts();
                        let mount_point = mounts.iter()
                            .find(|m| d.path.starts_with(&m.mount_point))
                            .map(|m| m.mount_point.to_string_lossy().to_string());
                        bg_db.set_portable(id, true, mount_point.as_deref()).ok();
                    }

                    // Mark for background analysis
                    bg_db.update_analysis_status(id, "pending").ok();
                    needs_analysis.push((id, d.path.clone(), d.appimage_type.clone()));
                }

                log::info!(
                    "Scan: {} files, {} new, {} removed, {} skipped (unchanged), took {}ms",
                    total, new_count, removed_count, skipped_count, start.elapsed().as_millis()
                );

                let duration = start.elapsed().as_millis() as i64;
                bg_db.log_scan(
                    "manual",
                    &dirs.iter().map(|s| s.to_string()).collect::<Vec<_>>(),
                    total,
                    new_count,
                    removed_count,
                    duration,
                ).ok();

                (total, new_count, needs_analysis)
            })
            .await;

            if let Ok((total, new_count, needs_analysis)) = result {
                // Dismiss the "Scanning..." toast now that fast scan is done
                if let Some(ref toast) = scan_toast {
                    toast.dismiss();
                }

                // Refresh the library view immediately (apps appear with "Analyzing..." badge)
                let window_weak2 = window_weak.clone();
                if let Some(window) = window_weak.upgrade() {
                    let db = window.database();
                    let lib_view = window.imp().library_view.get().unwrap();
                    match db.get_all_appimages() {
                        Ok(records) => lib_view.populate(records),
                        Err(_) => lib_view.set_state(LibraryState::Empty),
                    }
                }

                let msg = match new_count {
                    0 if total == 0 => i18n("No AppImages found"),
                    0 => format!("{} {}", total, i18n("AppImages up to date")),
                    1 => i18n("Found 1 new AppImage"),
                    n => format!("{} {} {}", i18n("Found"), n, i18n("new AppImages")),
                };
                toast_overlay.add_toast(widgets::info_toast(&msg));

                // Background analysis per file with debounced UI refresh
                let running = analysis::running_count();
                if running > 0 {
                    log::info!("Analyzing {} AppImage(s) in background ({} already running)", needs_analysis.len(), running);
                }
                if !needs_analysis.is_empty() {
                    let settings_scan = gio::Settings::new(crate::config::APP_ID);
                    let auto_integrate = settings_scan.boolean("auto-integrate");
                    let pending = Rc::new(std::cell::Cell::new(needs_analysis.len()));
                    let refresh_timer: Rc<std::cell::Cell<Option<glib::SourceId>>> =
                        Rc::new(std::cell::Cell::new(None));

                    for (id, path, appimage_type) in needs_analysis {
                        let window_weak3 = window_weak2.clone();
                        let pending = pending.clone();
                        let refresh_timer = refresh_timer.clone();

                        glib::spawn_future_local(async move {
                            let _ = gio::spawn_blocking(move || {
                                analysis::run_background_analysis(id, path, appimage_type, auto_integrate);
                            })
                            .await;

                            let remaining = pending.get().saturating_sub(1);
                            pending.set(remaining);

                            // Debounced refresh: wait 300ms before refreshing UI
                            if let Some(source_id) = refresh_timer.take() {
                                source_id.remove();
                            }

                            let window_weak4 = window_weak3.clone();
                            let refresh_timer_clear = refresh_timer.clone();
                            let timer_id = glib::timeout_add_local_once(
                                std::time::Duration::from_millis(300),
                                move || {
                                    // Clear the stored SourceId so nobody tries to remove a fired timer
                                    refresh_timer_clear.set(None);

                                    if let Some(window) = window_weak4.upgrade() {
                                        let db = window.database();
                                        let lib_view = window.imp().library_view.get().unwrap();
                                        match db.get_all_appimages() {
                                            Ok(records) => lib_view.populate(records),
                                            Err(_) => {}
                                        }
                                    }
                                },
                            );
                            refresh_timer.set(Some(timer_id));
                        });
                    }
                }
            }
        });
    }

    /// Update the badge number on the Updates tab in the ViewSwitcherBar.
    fn refresh_update_badge(&self) {
        let db = self.database();
        let count = db.updatable_count();
        if let Some(view_stack) = self.imp().view_stack.get() {
            if let Some(child) = view_stack.child_by_name("updates") {
                let page = view_stack.page(&child);
                page.set_badge_number(count as u32);
                page.set_needs_attention(count > 0);
            }
        }
        // Also update the banner in the Installed view
        if let Some(library_view) = self.imp().library_view.get() {
            library_view.set_update_count(count);
        }
    }

    fn start_file_watcher(&self) {
        let settings = self.settings();
        let dirs: Vec<std::path::PathBuf> = settings
            .strv("scan-directories")
            .iter()
            .map(|s| discovery::expand_tilde(&s.to_string()))
            .collect();

        if dirs.is_empty() {
            return;
        }

        // Use an atomic flag to communicate across the thread boundary.
        // The watcher callback (on a background thread) sets the flag,
        // and a glib timer on the main thread polls and dispatches.
        let changed = std::sync::Arc::new(std::sync::atomic::AtomicBool::new(false));

        let changed_watcher = changed.clone();
        let handle = watcher::start_watcher(dirs, move |event| {
            match &event {
                watcher::WatchEvent::Changed(paths) => {
                    log::info!("File watcher: {} path(s) changed: {:?}", paths.len(), paths);
                }
            }
            changed_watcher.store(true, std::sync::atomic::Ordering::Relaxed);
        });

        if let Some(h) = handle {
            self.imp().watcher_handle.replace(Some(h));

            // Poll the flag every second from the main thread.
            // Returns Break when the window is gone to stop the timer.
            let window_weak = self.downgrade();
            glib::timeout_add_local(std::time::Duration::from_secs(1), move || {
                let Some(window) = window_weak.upgrade() else {
                    return glib::ControlFlow::Break;
                };
                if changed.swap(false, std::sync::atomic::Ordering::Relaxed) {
                    window.trigger_scan(None);
                }
                glib::ControlFlow::Continue
            });
        }
    }

    fn show_command_palette(&self) {
        let db = self.database().clone();

        let dialog = adw::Dialog::builder()
            .title("Quick Launch")
            .content_width(450)
            .content_height(400)
            .build();

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

        let content_box = gtk::Box::builder()
            .orientation(gtk::Orientation::Vertical)
            .spacing(8)
            .margin_start(12)
            .margin_end(12)
            .margin_top(8)
            .margin_bottom(8)
            .build();

        let search_entry = gtk::SearchEntry::builder()
            .placeholder_text(&i18n("Type to search installed and catalog apps..."))
            .hexpand(true)
            .build();
        content_box.append(&search_entry);

        let scrolled = gtk::ScrolledWindow::builder()
            .vexpand(true)
            .build();

        let results_list = gtk::ListBox::builder()
            .selection_mode(gtk::SelectionMode::Single)
            .css_classes(["boxed-list"])
            .build();
        scrolled.set_child(Some(&results_list));
        content_box.append(&scrolled);

        // Populate results based on search
        let db_ref = db.clone();
        let results_ref = results_list.clone();
        let dialog_ref = dialog.clone();
        let window_weak = self.downgrade();

        let update_results = std::rc::Rc::new(move |query: &str| {
            // Clear existing
            while let Some(child) = results_ref.first_child() {
                results_ref.remove(&child);
            }

            if query.is_empty() {
                return;
            }

            let q = query.to_lowercase();

            // Search installed apps
            let installed = db_ref.get_all_appimages().unwrap_or_default();
            let mut count = 0;
            for record in &installed {
                let name = record.app_name.as_deref().unwrap_or(&record.filename);
                if name.to_lowercase().contains(&q) && count < 10 {
                    let row = adw::ActionRow::builder()
                        .title(name)
                        .subtitle(&i18n("Installed - click to launch"))
                        .activatable(true)
                        .build();
                    let icon = widgets::app_icon(
                        record.icon_path.as_deref(), name, 32,
                    );
                    row.add_prefix(&icon);
                    row.add_suffix(&widgets::accessible_suffix_icon("media-playback-start-symbolic", &i18n("Launch")));

                    let record_id = record.id;
                    let dialog_c = dialog_ref.clone();
                    let window_w = window_weak.clone();
                    row.connect_activated(move |_| {
                        dialog_c.close();
                        if let Some(win) = window_w.upgrade() {
                            gio::prelude::ActionGroupExt::activate_action(
                                &win, "launch-appimage", Some(&record_id.to_variant()),
                            );
                        }
                    });
                    results_ref.append(&row);
                    count += 1;
                }
            }

            // Search catalog apps
            if let Ok(catalog_results) = db_ref.search_catalog(
                query, None, 10, 0,
                crate::core::database::CatalogSortOrder::PopularityDesc,
            ) {
                for app in &catalog_results {
                    if count >= 15 { break; }
                    let row = adw::ActionRow::builder()
                        .title(&app.name)
                        .subtitle(&i18n("Catalog - click to view"))
                        .activatable(true)
                        .build();
                    let icon = widgets::app_icon(None, &app.name, 32);
                    row.add_prefix(&icon);
                    row.add_suffix(&widgets::accessible_suffix_icon("go-next-symbolic", &i18n("Open")));

                    let app_id = app.id;
                    let dialog_c = dialog_ref.clone();
                    let window_w = window_weak.clone();
                    let db_c = db_ref.clone();
                    row.connect_activated(move |_| {
                        dialog_c.close();
                        if let Some(win) = window_w.upgrade() {
                            // Switch to catalog tab
                            if let Some(vs) = win.imp().view_stack.get() {
                                vs.set_visible_child_name("catalog");
                            }
                            // Navigate to the app detail
                            if let Ok(Some(catalog_app)) = db_c.get_catalog_app(app_id) {
                                if let Some(toast) = win.imp().toast_overlay.get() {
                                    let detail = crate::ui::catalog_detail::build_catalog_detail_page(
                                        &catalog_app, &db_c, toast,
                                    );
                                    // Push onto the catalog NavigationView
                                    // The catalog page is a NavigationView inside the ViewStack
                                    if let Some(vs) = win.imp().view_stack.get() {
                                        if let Some(child) = vs.child_by_name("catalog") {
                                            if let Ok(nav) = child.downcast::<adw::NavigationView>() {
                                                nav.push(&detail);
                                            }
                                        }
                                    }
                                }
                            }
                        }
                    });
                    results_ref.append(&row);
                    count += 1;
                }
            }

            if count == 0 {
                let row = adw::ActionRow::builder()
                    .title(&i18n("No results found"))
                    .sensitive(false)
                    .build();
                results_ref.append(&row);
            }
        });

        {
            let update_fn = update_results.clone();
            search_entry.connect_search_changed(move |entry| {
                let query = entry.text().to_string();
                update_fn(&query);
            });
        }

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

        // Return focus to main content when command palette closes (WCAG 2.4.3)
        let window_weak = self.downgrade();
        dialog.connect_closed(move |_| {
            if let Some(window) = window_weak.upgrade() {
                if let Some(vs) = window.imp().view_stack.get() {
                    if let Some(child) = vs.visible_child() {
                        child.grab_focus();
                    }
                }
            }
        });

        dialog.present(Some(self));

        // Focus the search entry after presenting
        search_entry.grab_focus();
    }

    fn show_shortcuts_dialog(&self) {
        let dialog = adw::Dialog::builder()
            .title("Keyboard Shortcuts")
            .content_width(400)
            .content_height(420)
            .build();

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

        let scrolled = gtk::ScrolledWindow::builder()
            .vexpand(true)
            .build();

        let content = gtk::Box::builder()
            .orientation(gtk::Orientation::Vertical)
            .spacing(18)
            .margin_top(18)
            .margin_bottom(18)
            .margin_start(18)
            .margin_end(18)
            .build();

        // Navigation group
        let nav_group = adw::PreferencesGroup::builder()
            .title("Navigation")
            .build();
        nav_group.add(&shortcut_row("Ctrl+1", "Installed"));
        nav_group.add(&shortcut_row("Ctrl+2", "Catalog"));
        nav_group.add(&shortcut_row("Ctrl+3", "Updates"));
        nav_group.add(&shortcut_row("Ctrl+K", "Quick Launch"));
        nav_group.add(&shortcut_row("Ctrl+F", "Search"));
        nav_group.add(&shortcut_row("Ctrl+D", "Dashboard"));
        nav_group.add(&shortcut_row("Ctrl+,", "Preferences"));
        content.append(&nav_group);

        // Actions group
        let actions_group = adw::PreferencesGroup::builder()
            .title("Actions")
            .build();
        actions_group.add(&shortcut_row("Ctrl+R / F5", "Scan for AppImages"));
        actions_group.add(&shortcut_row("Ctrl+U", "Check for updates"));
        content.append(&actions_group);

        // Application group
        let app_group = adw::PreferencesGroup::builder()
            .title("Application")
            .build();
        app_group.add(&shortcut_row("Ctrl+?", "Keyboard shortcuts"));
        app_group.add(&shortcut_row("Ctrl+Q", "Quit"));
        content.append(&app_group);

        scrolled.set_child(Some(&content));
        toolbar.set_content(Some(&scrolled));
        dialog.set_child(Some(&toolbar));

        // Return focus to main content when dialog closes (WCAG 2.4.3)
        let window_weak = self.downgrade();
        dialog.connect_closed(move |_| {
            if let Some(window) = window_weak.upgrade() {
                if let Some(vs) = window.imp().view_stack.get() {
                    if let Some(child) = vs.visible_child() {
                        child.grab_focus();
                    }
                }
            }
        });

        dialog.present(Some(self));
    }

    fn dismiss_drop_overlay(&self) {
        if let Some(revealer) = self.imp().drop_revealer.get() {
            revealer.set_reveal_child(false);
        }
        if let Some(overlay) = self.imp().drop_overlay.get() {
            let overlay = overlay.clone();
            glib::timeout_add_local_once(
                std::time::Duration::from_millis(200),
                move || { overlay.set_visible(false); },
            );
        }
    }

    fn open_file_picker(&self) {
        self.dismiss_drop_overlay();

        let filter = gtk::FileFilter::new();
        filter.set_name(Some("AppImage files"));
        filter.add_pattern("*.AppImage");
        filter.add_pattern("*.appimage");
        // Also accept any file (AppImages don't always have the extension)
        let all_filter = gtk::FileFilter::new();
        all_filter.set_name(Some("All files"));
        all_filter.add_pattern("*");

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

        let dialog = gtk::FileDialog::builder()
            .title(i18n("Choose an AppImage"))
            .filters(&filters)
            .default_filter(&filter)
            .modal(true)
            .build();

        let window_weak = self.downgrade();
        dialog.open(Some(self), None::<&gio::Cancellable>, move |result| {
            let Ok(file) = result else { return };
            let Some(path) = file.path() else { return };
            let Some(window) = window_weak.upgrade() else { return };

            // Validate it's an AppImage via magic bytes
            if discovery::detect_appimage(&path).is_none() {
                let toast_overlay = window.imp().toast_overlay.get().unwrap();
                toast_overlay.add_toast(widgets::error_toast(&i18n("Not a valid AppImage file")));
                return;
            }

            let db = window.database().clone();
            let toast_overlay = window.imp().toast_overlay.get().unwrap().clone();
            let window_weak2 = window.downgrade();

            drop_dialog::show_drop_dialog(
                &window,
                vec![path],
                &toast_overlay,
                move || {
                    if let Some(win) = window_weak2.upgrade() {
                        let lib_view = win.imp().library_view.get().unwrap();
                        match db.get_all_appimages() {
                            Ok(records) => lib_view.populate(records),
                            Err(_) => lib_view.set_state(LibraryState::Empty),
                        }
                    }
                },
            );
        });
    }

    fn save_window_state(&self) {
        let settings = self.settings();
        let (width, height) = (self.width(), self.height());
        settings
            .set_int("window-width", width)
            .expect("Failed to save window width");
        settings
            .set_int("window-height", height)
            .expect("Failed to save window height");
        settings
            .set_boolean("window-maximized", self.is_maximized())
            .expect("Failed to save maximized state");
    }

    fn restore_window_state(&self) {
        let settings = self.settings();
        let width = settings.int("window-width");
        let height = settings.int("window-height");
        let maximized = settings.boolean("window-maximized");

        self.set_default_size(width, height);
        if maximized {
            self.maximize();
        }
    }

    fn show_export_dialog(&self) {
        let db = self.database().clone();
        let toast_overlay = self.imp().toast_overlay.get().unwrap().clone();

        let dialog = gtk::FileDialog::builder()
            .title(i18n("Export App List"))
            .initial_name("driftwood-apps.json")
            .build();

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

        let window = self.clone();
        dialog.save(Some(&window), gio::Cancellable::NONE, move |result| {
            if let Ok(file) = result {
                if let Some(path) = file.path() {
                    match backup::export_app_list(&db, &path) {
                        Ok(count) => {
                            toast_overlay.add_toast(
                                widgets::info_toast(&ni18n_f("Exported {} app", "Exported {} apps", count as u32, &[("{}", &count.to_string())])),
                            );
                        }
                        Err(e) => {
                            toast_overlay.add_toast(
                                widgets::error_toast(&i18n_f("Export failed: {error}", &[("{error}", &e.to_string())])),
                            );
                        }
                    }
                }
            }
        });
    }

    fn show_import_dialog(&self) {
        let db = self.database().clone();
        let toast_overlay = self.imp().toast_overlay.get().unwrap().clone();

        let dialog = gtk::FileDialog::builder()
            .title(i18n("Import App List"))
            .build();

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

        let window = self.clone();
        dialog.open(Some(&window), gio::Cancellable::NONE, move |result| {
            if let Ok(file) = result {
                if let Some(path) = file.path() {
                    match backup::import_app_list(&db, &path) {
                        Ok(result) => {
                            let msg = if result.missing.is_empty() {
                                ni18n_f("Imported metadata for {} app", "Imported metadata for {} apps", result.matched as u32, &[("{}", &result.matched.to_string())])
                            } else {
                                i18n_f(
                                    "Imported {matched} apps, {missing} not found",
                                    &[
                                        ("{matched}", &result.matched.to_string()),
                                        ("{missing}", &result.missing.len().to_string()),
                                    ],
                                )
                            };
                            toast_overlay.add_toast(widgets::info_toast(&msg));

                            // Show missing apps dialog if any
                            if !result.missing.is_empty() {
                                let missing_text = result.missing.join("\n");
                                log::info!("Import - missing apps:\n{}", missing_text);
                            }
                        }
                        Err(e) => {
                            toast_overlay.add_toast(
                                widgets::error_toast(&i18n_f("Import failed: {error}", &[("{error}", &e.to_string())])),
                            );
                        }
                    }
                }
            }
        });
    }
}
