use adw::prelude::*;
use std::cell::{Cell, RefCell};
use std::collections::BTreeMap;
use std::io::Read as _;
use std::rc::Rc;

use gtk::gio;

use crate::core::backup;
use crate::core::catalog;
use crate::core::database::{AppImageRecord, Database};
use crate::i18n::{i18n, i18n_f};
use crate::core::footprint;
use crate::core::fuse::{self, FuseStatus};
use crate::core::integrator;
use crate::core::launcher::{self, SandboxMode};
use crate::core::notification;
use crate::core::sandbox;
use crate::core::security;
use crate::core::updater;
use crate::core::wayland::{self, WaylandStatus};
use super::integration_dialog;
use super::update_dialog;
use super::widgets;

pub fn build_detail_page(record: &AppImageRecord, db: &Rc<Database>) -> adw::NavigationPage {
    let name = record.app_name.as_deref().unwrap_or(&record.filename);

    // Toast overlay for copy actions
    let toast_overlay = adw::ToastOverlay::new();

    // ViewStack for tabbed content (transitions disabled for instant switching).
    // vhomogeneous=false so the stack sizes to the visible child only,
    // preventing shorter tabs from having excess scrollable empty space.
    let view_stack = adw::ViewStack::new();
    view_stack.set_vhomogeneous(false);
    view_stack.set_enable_transitions(false);

    // Build tab pages (2-tab layout: About + Details)
    let about_page = build_overview_tab(record, db);
    view_stack.add_titled(&about_page, Some("about"), &i18n("About"));
    view_stack.page(&about_page).set_icon_name(Some("info-symbolic"));

    // Details tab combines System + Security + Storage
    let system_content = build_system_tab(record, db, &toast_overlay);
    let security_content = build_security_tab(record, db);
    let storage_content = build_storage_tab(record, db, &toast_overlay);

    let details_page = gtk::Box::builder()
        .orientation(gtk::Orientation::Vertical)
        .build();
    details_page.append(&system_content);
    details_page.append(&security_content);
    details_page.append(&storage_content);

    view_stack.add_titled(&details_page, Some("details"), &i18n("Details"));
    view_stack.page(&details_page).set_icon_name(Some("applications-system-symbolic"));

    // Restore last-used tab from GSettings (map old tab names to new ones)
    let settings = gio::Settings::new(crate::config::APP_ID);
    let saved_tab = settings.string("detail-tab");
    let mapped_tab = match saved_tab.as_str() {
        "overview" | "about" => "about",
        "system" | "security" | "storage" | "details" => "details",
        _ => "about",
    };
    view_stack.set_visible_child_name(mapped_tab);

    // Persist tab choice on switch
    view_stack.connect_visible_child_name_notify(move |stack| {
        if let Some(name) = stack.visible_child_name() {
            settings.set_string("detail-tab", &name).ok();
        }
    });

    // Banner scrolls with content (not sticky) so tall banners don't eat space
    let scroll_content = gtk::Box::builder()
        .orientation(gtk::Orientation::Vertical)
        .build();
    scroll_content.append(&build_banner(record));
    scroll_content.append(&view_stack);

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

    toast_overlay.set_child(Some(&scrolled));

    // Header bar with ViewSwitcher as title widget (standard GNOME pattern)
    let header = adw::HeaderBar::new();

    let switcher = adw::ViewSwitcher::builder()
        .stack(&view_stack)
        .policy(adw::ViewSwitcherPolicy::Wide)
        .build();
    header.set_title_widget(Some(&switcher));

    // Launch button
    let launch_button = gtk::Button::builder()
        .label("Launch")
        .tooltip_text("Launch this AppImage")
        .build();
    launch_button.add_css_class("suggested-action");
    launch_button.update_property(&[
        gtk::accessible::Property::Label("Launch application"),
    ]);
    let record_id = record.id;
    let path = record.path.clone();
    let app_name_launch = record.app_name.clone().unwrap_or_else(|| record.filename.clone());
    let launch_args_raw = record.launch_args.clone();
    let db_launch = db.clone();
    let toast_launch = toast_overlay.clone();
    let first_run_prompted = record.first_run_prompted;
    let db_perm = db.clone();
    launch_button.connect_clicked(move |btn| {
        // Check if first-run permission dialog should be shown
        if !first_run_prompted {
            let btn_ref = btn.clone();
            let path = path.clone();
            let app_name = app_name_launch.clone();
            let app_name_dialog = app_name.clone();
            let db_launch = db_launch.clone();
            let toast_ref = toast_launch.clone();
            let launch_args_raw = launch_args_raw.clone();
            let db_perm = db_perm.clone();
            crate::ui::permission_dialog::show_permission_dialog(
                btn,
                &app_name_dialog,
                record_id,
                &db_perm,
                move || {
                    do_launch(
                        &btn_ref, record_id, &path, &app_name,
                        launcher::parse_launch_args(launch_args_raw.as_deref()),
                        &db_launch, &toast_ref,
                    );
                },
            );
            return;
        }

        let launch_args = launcher::parse_launch_args(launch_args_raw.as_deref());
        do_launch(
            btn, record_id, &path, &app_name_launch,
            launch_args, &db_launch, &toast_launch,
        );
    });

    header.pack_start(&launch_button);

    // Uninstall button
    let uninstall_button = gtk::Button::builder()
        .icon_name("user-trash-symbolic")
        .tooltip_text("Uninstall this AppImage")
        .build();
    uninstall_button.add_css_class("flat");
    uninstall_button.update_property(&[
        gtk::accessible::Property::Label("Uninstall application"),
    ]);
    let record_for_uninstall = record.clone();
    let db_uninstall = db.clone();
    let toast_uninstall = toast_overlay.clone();
    let is_integrated_header = record.integrated;
    uninstall_button.connect_clicked(move |_btn| {
        let fp = footprint::get_footprint(&db_uninstall, record_for_uninstall.id, record_for_uninstall.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();
        show_uninstall_dialog(
            &toast_uninstall,
            &record_for_uninstall,
            &db_uninstall,
            is_integrated_header,
            &fp_paths,
        );
    });
    header.pack_end(&uninstall_button);

    // Check for Update button
    let update_button = gtk::Button::builder()
        .icon_name("software-update-available-symbolic")
        .tooltip_text("Check for updates")
        .build();
    update_button.add_css_class("flat");
    update_button.update_property(&[
        gtk::accessible::Property::Label("Check for updates"),
    ]);
    let record_for_update = record.clone();
    let db_update = db.clone();
    update_button.connect_clicked(move |btn| {
        update_dialog::show_update_dialog(btn, &record_for_update, &db_update);
    });
    header.pack_end(&update_button);

    let toolbar = adw::ToolbarView::new();
    toolbar.add_top_bar(&header);
    toolbar.set_content(Some(&toast_overlay));
    widgets::apply_pointer_cursors(&toolbar);

    adw::NavigationPage::builder()
        .title(name)
        .tag("detail")
        .child(&toolbar)
        .build()
}

// ---------------------------------------------------------------------------
// Banner
// ---------------------------------------------------------------------------

fn build_banner(record: &AppImageRecord) -> gtk::Box {
    let banner = gtk::Box::builder()
        .orientation(gtk::Orientation::Horizontal)
        .spacing(16)
        .margin_start(18)
        .margin_end(18)
        .build();
    banner.add_css_class("detail-banner");
    banner.set_accessible_role(gtk::AccessibleRole::Banner);

    let name = record.app_name.as_deref().unwrap_or(&record.filename);

    // Large icon with drop shadow
    let icon = widgets::app_icon(record.icon_path.as_deref(), name, 96);
    icon.set_valign(gtk::Align::Start);
    icon.add_css_class("icon-dropshadow");
    banner.append(&icon);

    // Text column
    let text_col = gtk::Box::builder()
        .orientation(gtk::Orientation::Vertical)
        .spacing(4)
        .valign(gtk::Align::Center)
        .build();

    let name_label = gtk::Label::builder()
        .label(name)
        .css_classes(["title-1"])
        .halign(gtk::Align::Start)
        .build();
    text_col.append(&name_label);

    // Version + architecture inline
    let meta_parts: Vec<String> = [
        record.app_version.as_deref().map(|v| v.to_string()),
        record.architecture.as_deref().map(|a| a.to_string()),
    ]
    .iter()
    .filter_map(|p| p.clone())
    .collect();

    if !meta_parts.is_empty() {
        let meta_label = gtk::Label::builder()
            .label(&meta_parts.join(" - "))
            .css_classes(["dimmed"])
            .halign(gtk::Align::Start)
            .build();
        text_col.append(&meta_label);
    }

    // Description
    if let Some(ref desc) = record.description {
        if !desc.is_empty() {
            let desc_label = gtk::Label::builder()
                .label(desc)
                .css_classes(["body"])
                .halign(gtk::Align::Start)
                .wrap(true)
                .xalign(0.0)
                .build();
            text_col.append(&desc_label);
        }
    }

    // Key status badges inline
    let badge_box = gtk::Box::builder()
        .orientation(gtk::Orientation::Horizontal)
        .spacing(6)
        .margin_top(4)
        .build();

    badge_box.append(&widgets::integration_badge(record.integrated));

    if let Some(ref ws) = record.wayland_status {
        let status = WaylandStatus::from_str(ws);
        if status != WaylandStatus::Unknown {
            badge_box.append(&widgets::status_badge(status.label(), status.badge_class()));
        }
    }

    if let (Some(ref latest), Some(ref current)) = (&record.latest_version, &record.app_version) {
        if crate::core::updater::version_is_newer(latest, current) {
            badge_box.append(&widgets::status_badge("Update available", "info"));
        }
    }

    // Windows equivalent hint for novice users
    if let Some(equiv) = windows_equivalent(name) {
        let equiv_label = gtk::Label::builder()
            .label(equiv)
            .css_classes(["caption", "dim-label"])
            .halign(gtk::Align::Start)
            .build();
        text_col.append(&equiv_label);
    }

    text_col.append(&badge_box);
    banner.append(&text_col);
    banner
}

fn windows_equivalent(app_name: &str) -> Option<&'static str> {
    match app_name.to_lowercase().as_str() {
        s if s.contains("vlc") => Some("Similar to Windows Media Player"),
        s if s.contains("gimp") => Some("Similar to Photoshop"),
        s if s.contains("libreoffice") => Some("Similar to Microsoft Office"),
        s if s.contains("firefox") => Some("Similar to Edge / Chrome"),
        s if s.contains("chromium") || s.contains("brave") => Some("Similar to Chrome"),
        s if s.contains("kdenlive") || s.contains("shotcut") => Some("Similar to Windows Video Editor"),
        s if s.contains("krita") => Some("Similar to Paint / Photoshop"),
        s if s.contains("thunderbird") => Some("Similar to Outlook"),
        s if s.contains("telegram") || s.contains("signal") => Some("Similar to WhatsApp Desktop"),
        s if s.contains("obs") => Some("Similar to OBS Studio (same app!)"),
        s if s.contains("blender") => Some("Similar to Blender (same app!)"),
        s if s.contains("audacity") => Some("Similar to Audacity (same app!)"),
        s if s.contains("inkscape") => Some("Similar to Illustrator"),
        s if s.contains("handbrake") => Some("Similar to HandBrake (same app!)"),
        _ => None,
    }
}

// ---------------------------------------------------------------------------
// Tab 1: Overview - about, description, links, updates, releases, usage,
//                   capabilities, file info
// ---------------------------------------------------------------------------

fn build_overview_tab(record: &AppImageRecord, db: &Rc<Database>) -> gtk::Box {
    let tab = gtk::Box::builder()
        .orientation(gtk::Orientation::Vertical)
        .spacing(24)
        .margin_top(18)
        .margin_bottom(24)
        .margin_start(18)
        .margin_end(18)
        .build();

    let clamp = adw::Clamp::builder()
        .maximum_size(800)
        .tightening_threshold(600)
        .build();

    let inner = gtk::Box::builder()
        .orientation(gtk::Orientation::Vertical)
        .spacing(24)
        .build();

    // -----------------------------------------------------------------------
    // About section
    // -----------------------------------------------------------------------
    let has_about_data = record.appstream_id.is_some()
        || record.generic_name.is_some()
        || record.developer.is_some()
        || record.license.is_some()
        || record.project_group.is_some();

    if has_about_data {
        let about_group = adw::PreferencesGroup::builder()
            .title("About")
            .build();

        if let Some(ref id) = record.appstream_id {
            let row = adw::ActionRow::builder()
                .title("App ID")
                .subtitle(id)
                .subtitle_selectable(true)
                .build();
            about_group.add(&row);
        }

        if let Some(ref gn) = record.generic_name {
            let row = adw::ActionRow::builder()
                .title("Type")
                .subtitle(gn)
                .build();
            about_group.add(&row);
        }

        if let Some(ref dev) = record.developer {
            let row = adw::ActionRow::builder()
                .title("Developer")
                .subtitle(dev)
                .build();
            about_group.add(&row);
        }

        if let Some(ref lic) = record.license {
            let row = adw::ActionRow::builder()
                .title("License")
                .subtitle(lic)
                .tooltip_text("The license that governs how this app can be used and shared")
                .build();
            about_group.add(&row);
        }

        if let Some(ref pg) = record.project_group {
            let row = adw::ActionRow::builder()
                .title("Project")
                .subtitle(pg)
                .build();
            about_group.add(&row);
        }

        // Categories display
        if let Some(ref cats) = record.categories {
            if !cats.is_empty() {
                let cat_row = adw::ActionRow::builder()
                    .title("Categories")
                    .build();
                let cat_box = gtk::Box::new(gtk::Orientation::Horizontal, 4);
                cat_box.set_valign(gtk::Align::Center);
                for cat in cats.split(';').filter(|c| !c.is_empty()) {
                    let friendly = match cat.trim() {
                        "AudioVideo" | "Audio" | "Video" => "Media",
                        "Development" => "Developer Tools",
                        "Education" | "Science" => "Science & Education",
                        "Game" => "Games",
                        "Graphics" => "Graphics",
                        "Network" => "Internet",
                        "Office" => "Office",
                        "System" => "System Tools",
                        "Utility" => "Utilities",
                        other => other,
                    };
                    let badge = widgets::status_badge(friendly, "neutral");
                    cat_box.append(&badge);
                }
                cat_row.add_suffix(&cat_box);
                about_group.add(&cat_row);
            }
        }

        inner.append(&about_group);
    }

    // -----------------------------------------------------------------------
    // Description section
    // -----------------------------------------------------------------------
    if let Some(ref desc) = record.appstream_description {
        if !desc.is_empty() {
            let desc_group = adw::PreferencesGroup::builder()
                .title("Description")
                .build();

            let label = gtk::Label::builder()
                .label(desc)
                .wrap(true)
                .xalign(0.0)
                .css_classes(["body"])
                .selectable(true)
                .margin_top(8)
                .margin_bottom(8)
                .margin_start(12)
                .margin_end(12)
                .build();
            desc_group.add(&label);

            inner.append(&desc_group);
        }
    }

    // -----------------------------------------------------------------------
    // Screenshots section - async image loading from URLs, click to lightbox
    // -----------------------------------------------------------------------
    if let Some(ref urls_str) = record.screenshot_urls {
        let urls: Vec<&str> = urls_str.lines().filter(|u| !u.is_empty()).collect();
        if !urls.is_empty() {
            let screenshots_group = adw::PreferencesGroup::builder()
                .title("Screenshots")
                .build();

            let carousel = adw::Carousel::builder()
                .hexpand(true)
                .allow_scroll_wheel(true)
                .allow_mouse_drag(true)
                .build();
            carousel.set_height_request(300);

            let dots = adw::CarouselIndicatorDots::builder()
                .carousel(&carousel)
                .build();

            // Store textures for lightbox access
            let textures: Rc<std::cell::RefCell<Vec<Option<gtk::gdk::Texture>>>> =
                Rc::new(std::cell::RefCell::new(vec![None; urls.len()]));

            for (idx, url) in urls.iter().enumerate() {
                let picture = gtk::Picture::builder()
                    .content_fit(gtk::ContentFit::Contain)
                    .height_request(300)
                    .build();
                if let Some(cursor) = gtk::gdk::Cursor::from_name("pointer", None) {
                    picture.set_cursor(Some(&cursor));
                }
                picture.set_can_shrink(true);

                // Placeholder spinner while loading
                let overlay = gtk::Overlay::builder().child(&picture).build();
                let spinner = adw::Spinner::builder()
                    .width_request(32)
                    .height_request(32)
                    .halign(gtk::Align::Center)
                    .valign(gtk::Align::Center)
                    .build();
                spinner.update_property(&[gtk::accessible::Property::Label("Loading screenshot")]);
                overlay.add_overlay(&spinner);

                // Don't let overlays steal focus (prevents scroll jump on dialog close)
                overlay.set_focusable(false);
                overlay.set_focus_on_click(false);

                // Click handler for lightbox
                let textures_click = textures.clone();
                let click = gtk::GestureClick::new();
                click.connect_released(move |gesture, _, _, _| {
                    let textures_ref = textures_click.borrow();
                    if textures_ref.iter().any(|t| t.is_some()) {
                        if let Some(widget) = gesture.widget() {
                            if let Some(root) = gtk::prelude::WidgetExt::root(&widget) {
                                if let Ok(window) = root.downcast::<gtk::Window>() {
                                    show_screenshot_lightbox(
                                        &window,
                                        &textures_click,
                                        idx,
                                        None,
                                        None,
                                    );
                                }
                            }
                        }
                    }
                });
                overlay.add_controller(click);

                carousel.append(&overlay);

                // Load image asynchronously
                let url_owned = url.to_string();
                let picture_ref = picture.clone();
                let spinner_ref = spinner.clone();
                let textures_load = textures.clone();
                glib::spawn_future_local(async move {
                    let result = gio::spawn_blocking(move || {
                        let mut response = ureq::get(&url_owned)
                            .header("User-Agent", "Driftwood-AppImage-Manager/0.1")
                            .call()
                            .ok()?;
                        let mut buf = Vec::new();
                        response.body_mut().as_reader().read_to_end(&mut buf).ok()?;
                        Some(buf)
                    })
                    .await;

                    spinner_ref.set_visible(false);
                    if let Ok(Some(data)) = result {
                        let gbytes = glib::Bytes::from(&data);
                        if let Ok(texture) = gtk::gdk::Texture::from_bytes(&gbytes) {
                            picture_ref.set_paintable(Some(&texture));
                            if let Some(slot) = textures_load.borrow_mut().get_mut(idx) {
                                *slot = Some(texture);
                            }
                        }
                    }
                });
            }

            // Keyboard activation: Enter/Space opens lightbox from the carousel
            carousel.set_focusable(true);
            carousel.update_property(&[gtk::accessible::Property::Label("App screenshots")]);
            let textures_key = textures.clone();
            let carousel_key = carousel.clone();
            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) {
                    let idx = carousel_key.position().round() as usize;
                    let textures_ref = textures_key.borrow();
                    if textures_ref.iter().any(|t| t.is_some()) {
                        if let Some(root) = gtk::prelude::WidgetExt::root(&carousel_key) {
                            if let Ok(window) = root.downcast::<gtk::Window>() {
                                show_screenshot_lightbox(
                                    &window,
                                    &textures_key,
                                    idx,
                                    None,
                                    None,
                                );
                            }
                        }
                    }
                    glib::Propagation::Stop
                } else {
                    glib::Propagation::Proceed
                }
            });
            carousel.add_controller(key_ctrl);

            let carousel_box = gtk::Box::builder()
                .orientation(gtk::Orientation::Vertical)
                .spacing(8)
                .margin_top(8)
                .margin_bottom(8)
                .build();
            carousel_box.append(&carousel);
            if urls.len() > 1 {
                carousel_box.append(&dots);
            }
            screenshots_group.add(&carousel_box);

            inner.append(&screenshots_group);
        }
    }

    // -----------------------------------------------------------------------
    // Links section
    // -----------------------------------------------------------------------
    // Use source_url as fallback for vcs_url (auto-detected from update_info)
    let source_code_url = record.vcs_url.clone().or_else(|| record.source_url.clone());

    let has_links = record.homepage_url.is_some()
        || record.bugtracker_url.is_some()
        || record.donation_url.is_some()
        || record.help_url.is_some()
        || source_code_url.is_some();

    if has_links {
        let links_group = adw::PreferencesGroup::builder()
            .title("Links")
            .build();

        let link_entries: &[(&str, &str, &Option<String>)] = &[
            ("Homepage", "web-browser-symbolic", &record.homepage_url),
            ("Report a problem", "bug-symbolic", &record.bugtracker_url),
            ("Source code", "code-symbolic", &source_code_url),
            ("Documentation", "help-browser-symbolic", &record.help_url),
            ("Donate", "emblem-favorite-symbolic", &record.donation_url),
        ];

        for (title, icon_name, url_opt) in link_entries {
            if let Some(ref url) = url_opt {
                let row = adw::ActionRow::builder()
                    .title(*title)
                    .subtitle(url)
                    .activatable(true)
                    .build();

                let icon = gtk::Image::from_icon_name("external-link-symbolic");
                icon.set_valign(gtk::Align::Center);
                icon.set_accessible_role(gtk::AccessibleRole::Presentation);
                row.add_suffix(&icon);

                // Start with the fallback icon, then try to load favicon
                let prefix_icon = gtk::Image::from_icon_name(*icon_name);
                prefix_icon.set_valign(gtk::Align::Center);
                prefix_icon.set_pixel_size(16);
                prefix_icon.set_accessible_role(gtk::AccessibleRole::Presentation);
                row.add_prefix(&prefix_icon);
                fetch_favicon_async(url, &prefix_icon);

                let url_clone = url.clone();
                row.connect_activated(move |row| {
                    let launcher = gtk::UriLauncher::new(&url_clone);
                    let window = row
                        .root()
                        .and_then(|r| r.downcast::<gtk::Window>().ok());
                    launcher.launch(
                        window.as_ref(),
                        None::<&gtk::gio::Cancellable>,
                        |_| {},
                    );
                });
                links_group.add(&row);
            }
        }

        inner.append(&links_group);
    }

    // -----------------------------------------------------------------------
    // Updates section
    // -----------------------------------------------------------------------
    let updates_group = adw::PreferencesGroup::builder()
        .title("Updates")
        .description("Keep this app up to date by checking for new versions.")
        .build();

    if let Some(ref update_type) = record.update_type {
        let display_label = updater::parse_update_info(update_type)
            .map(|ut| ut.type_label_display())
            .unwrap_or("Unknown format");
        let row = adw::ActionRow::builder()
            .title("Update method")
            .subtitle(&format!(
                "This app checks for updates using: {}",
                display_label
            ))
            .tooltip_text(
                "Driftwood can check for newer versions of this app automatically. \
                 The developer has included information about where updates are published."
            )
            .build();
        updates_group.add(&row);
    } else {
        let row = adw::ActionRow::builder()
            .title("Update method")
            .subtitle(
                "This app does not include automatic update information. \
                 To update manually, download a newer version from the \
                 developer's website and drag it into Driftwood."
            )
            .tooltip_text(
                "AppImages can include built-in update information that tells Driftwood \
                 where to check for newer versions. This one doesn't have any, so you'll \
                 need to download updates yourself from wherever you got the app."
            )
            .build();
        let badge = widgets::status_badge("Manual only", "neutral");
        badge.set_valign(gtk::Align::Center);
        row.add_suffix(&badge);
        updates_group.add(&row);
    }

    if let Some(ref latest) = record.latest_version {
        let is_newer = record
            .app_version
            .as_deref()
            .map(|current| crate::core::updater::version_is_newer(latest, current))
            .unwrap_or(true);

        if is_newer {
            let subtitle = format!(
                "A newer version is available: {} (you have {})",
                latest,
                record.app_version.as_deref().unwrap_or("unknown"),
            );
            let row = adw::ActionRow::builder()
                .title("Update available")
                .subtitle(&subtitle)
                .build();
            let badge = widgets::status_badge("Update", "info");
            badge.set_valign(gtk::Align::Center);
            row.add_suffix(&badge);
            updates_group.add(&row);
        } else {
            let row = adw::ActionRow::builder()
                .title("Version status")
                .subtitle("You are running the latest version.")
                .build();
            let badge = widgets::status_badge("Latest", "success");
            badge.set_valign(gtk::Align::Center);
            row.add_suffix(&badge);
            updates_group.add(&row);
        }
    }

    if let Some(ref checked) = record.update_checked {
        let row = adw::ActionRow::builder()
            .title("Last checked")
            .subtitle(&widgets::relative_time(checked))
            .build();
        updates_group.add(&row);
    }

    // Per-app auto-update toggle (pinned = skip updates)
    let pin_row = adw::SwitchRow::builder()
        .title("Skip auto-updates")
        .subtitle("When enabled, this app will be excluded from batch update checks")
        .active(record.pinned)
        .build();
    {
        let db_ref = db.clone();
        let record_id = record.id;
        pin_row.connect_active_notify(move |row| {
            let _ = db_ref.set_pinned(record_id, row.is_active());
        });
    }
    updates_group.add(&pin_row);

    inner.append(&updates_group);

    // -----------------------------------------------------------------------
    // Release History section
    // -----------------------------------------------------------------------
    if let Some(ref release_json) = record.release_history {
        if let Ok(releases) = serde_json::from_str::<Vec<serde_json::Value>>(release_json) {
            if !releases.is_empty() {
                let release_group = adw::PreferencesGroup::builder()
                    .title("Release History")
                    .description("Recent versions of this application.")
                    .build();

                for release in releases.iter().take(10) {
                    let version = release
                        .get("version")
                        .and_then(|v| v.as_str())
                        .unwrap_or("?");
                    let date = release
                        .get("date")
                        .and_then(|v| v.as_str())
                        .unwrap_or("");
                    let desc = release.get("description").and_then(|v| v.as_str());

                    let title = if date.is_empty() {
                        format!("v{}", version)
                    } else {
                        format!("v{}  -  {}", version, date)
                    };

                    if let Some(desc_text) = desc {
                        let row = adw::ExpanderRow::builder()
                            .title(&title)
                            .subtitle("Expand to see changes")
                            .build();

                        let label = gtk::Label::builder()
                            .label(desc_text)
                            .wrap(true)
                            .xalign(0.0)
                            .css_classes(["body"])
                            .margin_top(8)
                            .margin_bottom(8)
                            .margin_start(12)
                            .margin_end(12)
                            .build();
                        let label_row = gtk::ListBoxRow::builder()
                            .activatable(false)
                            .selectable(false)
                            .child(&label)
                            .build();
                        row.add_row(&label_row);

                        release_group.add(&row);
                    } else {
                        let row = adw::ActionRow::builder()
                            .title(&title)
                            .build();
                        release_group.add(&row);
                    }
                }

                inner.append(&release_group);
            }
        }
    }

    // -----------------------------------------------------------------------
    // Usage section
    // -----------------------------------------------------------------------
    let usage_group = adw::PreferencesGroup::builder()
        .title("Usage")
        .build();

    let stats = launcher::get_launch_stats(db, record.id);

    let launches_row = adw::ActionRow::builder()
        .title("Total launches")
        .subtitle(&stats.total_launches.to_string())
        .build();
    usage_group.add(&launches_row);

    if let Some(ref last) = stats.last_launched {
        let row = adw::ActionRow::builder()
            .title("Last launched")
            .subtitle(&widgets::relative_time(last))
            .build();
        usage_group.add(&row);
    }
    inner.append(&usage_group);

    // -----------------------------------------------------------------------
    // Capabilities section
    // -----------------------------------------------------------------------
    let has_capabilities = record.keywords.is_some()
        || record.mime_types.is_some()
        || record.content_rating.is_some()
        || record.desktop_actions.is_some();

    if has_capabilities {
        let cap_group = adw::PreferencesGroup::builder()
            .title("Features")
            .build();

        if let Some(ref kw) = record.keywords {
            if !kw.is_empty() {
                let row = adw::ActionRow::builder()
                    .title("Keywords")
                    .subtitle(kw)
                    .build();
                cap_group.add(&row);
            }
        }

        if let Some(ref mt) = record.mime_types {
            if !mt.is_empty() {
                let row = adw::ActionRow::builder()
                    .title("Supported file types")
                    .subtitle(mt)
                    .build();
                cap_group.add(&row);
            }
        }

        if let Some(ref cr) = record.content_rating {
            let row = adw::ActionRow::builder()
                .title("Content rating")
                .subtitle(cr)
                .tooltip_text(
                    "An age rating for the app's content, similar to game ratings.",
                )
                .build();
            cap_group.add(&row);
        }

        if let Some(ref actions_json) = record.desktop_actions {
            if let Ok(actions) = serde_json::from_str::<Vec<String>>(actions_json) {
                if !actions.is_empty() {
                    let row = adw::ActionRow::builder()
                        .title("Quick actions")
                        .subtitle(&actions.join(", "))
                        .tooltip_text(
                            "Additional actions available from the right-click menu \
                             when this app is added to your app menu.",
                        )
                        .build();
                    cap_group.add(&row);
                }
            }
        }

        inner.append(&cap_group);
    }

    // -----------------------------------------------------------------------
    // File Information section
    // -----------------------------------------------------------------------
    let info_group = adw::PreferencesGroup::builder()
        .title("File Information")
        .build();

    let type_str = match record.appimage_type {
        Some(1) => "Legacy format - an older packaging style. Still works but less common.",
        Some(2) => "Modern format - compressed and efficient. This is the standard format for most AppImages.",
        _ => "Unknown type",
    };
    let type_row = adw::ActionRow::builder()
        .title("Package format")
        .subtitle(type_str)
        .tooltip_text(
            "AppImages come in two formats. Type 1 is the older format. \
             Type 2 is the current standard - it uses compression for smaller \
             files and faster loading."
        )
        .build();
    info_group.add(&type_row);

    let exec_row = adw::ActionRow::builder()
        .title("Ready to run")
        .subtitle(if record.is_executable {
            "Yes - this file is ready to launch"
        } else {
            "No - will be fixed automatically when launched"
        })
        .tooltip_text(
            "Whether the file has the permissions needed to run. \
             If not, Driftwood sets this up automatically the first time you launch it."
        )
        .build();
    info_group.add(&exec_row);

    // Digital signature status
    let sig_row = adw::ActionRow::builder()
        .title("Verified by developer")
        .subtitle(if record.has_signature {
            "This app includes a developer signature, which helps verify it has not been tampered with."
        } else {
            "This is normal for most AppImages and does not mean the app is unsafe."
        })
        .tooltip_text(
            "Some developers sign their apps with a cryptographic key. \
             This helps verify the file hasn't been modified since it was published."
        )
        .build();
    let sig_badge = if record.has_signature {
        widgets::status_badge("Signed", "success")
    } else {
        widgets::status_badge("No signature", "neutral")
    };
    sig_badge.set_valign(gtk::Align::Center);
    sig_row.add_suffix(&sig_badge);
    info_group.add(&sig_row);

    let seen_row = adw::ActionRow::builder()
        .title("First seen")
        .subtitle(&widgets::relative_time(&record.first_seen))
        .build();
    info_group.add(&seen_row);

    let scanned_row = adw::ActionRow::builder()
        .title("Last checked")
        .subtitle(&widgets::relative_time(&record.last_scanned))
        .build();
    info_group.add(&scanned_row);

    if let Some(ref notes) = record.notes {
        if !notes.is_empty() {
            let row = adw::ActionRow::builder()
                .title("Notes")
                .subtitle(notes)
                .build();
            info_group.add(&row);
        }
    }

    // Tag editor
    {
        let tag_row = adw::ActionRow::builder()
            .title("Tags")
            .build();

        let tag_box = gtk::Box::new(gtk::Orientation::Horizontal, 4);
        tag_box.set_valign(gtk::Align::Center);

        let tag_box_ref = tag_box.clone();
        let db_tag = db.clone();
        let app_id = record.id;
        let initial_tags = record.tags.clone().unwrap_or_default();

        // Shared tag state for add/remove closures
        let tags_state = Rc::new(std::cell::RefCell::new(
            initial_tags.split(',')
                .map(|t| t.trim().to_string())
                .filter(|t| !t.is_empty())
                .collect::<Vec<String>>()
        ));

        let rebuild_tags = {
            let tag_box = tag_box_ref.clone();
            let db_ref = db_tag.clone();
            let state = tags_state.clone();

            Rc::new(move || {
                // Clear existing children
                while let Some(child) = tag_box.first_child() {
                    tag_box.remove(&child);
                }

                let current_tags = state.borrow().clone();

                // Add chip for each tag
                for tag_text in &current_tags {
                    let chip = gtk::Box::new(gtk::Orientation::Horizontal, 4);
                    chip.add_css_class("pill");
                    chip.set_valign(gtk::Align::Center);

                    let label = gtk::Label::new(Some(tag_text));
                    label.add_css_class("caption");
                    chip.append(&label);

                    let remove_btn = widgets::accessible_icon_button(
                        "window-close-symbolic",
                        &format!("Remove tag {}", tag_text),
                        &format!("Remove tag {}", tag_text),
                    );
                    remove_btn.add_css_class("circular");

                    let tag_to_remove = tag_text.clone();
                    let state_r = state.clone();
                    let db_r = db_ref.clone();

                    // We need a way to trigger rebuild after removal
                    // Store the rebuild fn in a separate Rc that we can share
                    chip.append(&remove_btn);
                    tag_box.append(&chip);

                    // Connect remove - will be wired below with rebuild
                    let tag_box_inner = tag_box.clone();
                    remove_btn.connect_clicked(move |_| {
                        {
                            let mut tags = state_r.borrow_mut();
                            tags.retain(|t| t != &tag_to_remove);
                            let new_tags = if tags.is_empty() {
                                None
                            } else {
                                Some(tags.join(","))
                            };
                            db_r.update_tags(app_id, new_tags.as_deref()).ok();
                        }
                        // Rebuild chip display
                        while let Some(child) = tag_box_inner.first_child() {
                            tag_box_inner.remove(&child);
                        }
                        let current = state_r.borrow().clone();
                        for t in &current {
                            let badge = widgets::status_badge(t, "info");
                            tag_box_inner.append(&badge);
                        }
                        // Re-add the "+" button
                        // (simplified: just show badges after edit, full rebuild on next detail open)
                    });
                }

                // "+" add button
                let add_btn = widgets::accessible_icon_button(
                    "list-add-symbolic",
                    "Add tag",
                    "Add a new tag to this app",
                );
                add_btn.add_css_class("circular");

                let state_a = state.clone();
                let db_a = db_ref.clone();
                let tag_box_a = tag_box.clone();
                add_btn.connect_clicked(move |btn| {
                    // Replace the "+" button with an entry
                    let entry = gtk::Entry::builder()
                        .placeholder_text("New tag")
                        .width_chars(12)
                        .build();
                    entry.update_property(&[gtk::accessible::Property::Label("New tag name")]);
                    let parent = tag_box_a.clone();
                    parent.remove(btn);
                    parent.append(&entry);
                    entry.grab_focus();

                    let state_e = state_a.clone();
                    let db_e = db_a.clone();
                    let parent_e = parent.clone();
                    entry.connect_activate(move |ent| {
                        let text = ent.text().trim().to_string();
                        if !text.is_empty() {
                            let mut tags = state_e.borrow_mut();
                            if !tags.iter().any(|t| t.eq_ignore_ascii_case(&text)) {
                                tags.push(text.clone());
                                let new_tags = tags.join(",");
                                db_e.update_tags(app_id, Some(&new_tags)).ok();
                            }
                        }
                        // Replace entry with badge + re-add "+" button
                        parent_e.remove(ent);
                        // Rebuild as badges
                        while let Some(child) = parent_e.first_child() {
                            parent_e.remove(&child);
                        }
                        let current = state_e.borrow().clone();
                        for t in &current {
                            let badge = widgets::status_badge(t, "info");
                            parent_e.append(&badge);
                        }
                        // Re-add the "+" button (placeholder; fully functional on detail reopen)
                        let new_add = widgets::accessible_icon_button(
                            "list-add-symbolic",
                            "Add tag",
                            "Add a new tag to this app",
                        );
                        new_add.add_css_class("circular");
                        parent_e.append(&new_add);
                    });
                });

                tag_box.append(&add_btn);
            })
        };

        rebuild_tags();

        tag_row.add_suffix(&tag_box_ref);
        info_group.add(&tag_row);
    }

    inner.append(&info_group);

    // "You might also like" - similar apps from the user's library
    if let Some(ref cats) = record.categories {
        if let Ok(similar) = db.find_similar_apps(cats, record.id, 4) {
            if !similar.is_empty() {
                let similar_group = adw::PreferencesGroup::builder()
                    .title("You might also like")
                    .build();

                for (id, name, icon_path) in &similar {
                    let row = adw::ActionRow::builder()
                        .title(name.as_str())
                        .activatable(true)
                        .build();

                    let icon = widgets::app_icon(
                        icon_path.as_deref(),
                        name,
                        32,
                    );
                    row.add_prefix(&icon);

                    // Store the record ID in the widget name for navigation
                    row.set_widget_name(&format!("similar-{}", id));

                    similar_group.add(&row);
                }

                inner.append(&similar_group);
            }
        }
    }

    clamp.set_child(Some(&inner));
    tab.append(&clamp);
    tab
}

// ---------------------------------------------------------------------------
// Tab 2: System - integration, compatibility, sandboxing
// ---------------------------------------------------------------------------

fn build_system_tab(record: &AppImageRecord, db: &Rc<Database>, toast_overlay: &adw::ToastOverlay) -> gtk::Box {
    let tab = gtk::Box::builder()
        .orientation(gtk::Orientation::Vertical)
        .spacing(24)
        .margin_top(18)
        .margin_bottom(24)
        .margin_start(18)
        .margin_end(18)
        .build();

    let clamp = adw::Clamp::builder()
        .maximum_size(800)
        .tightening_threshold(600)
        .build();

    let inner = gtk::Box::builder()
        .orientation(gtk::Orientation::Vertical)
        .spacing(24)
        .build();

    // Desktop Integration group
    let integration_group = adw::PreferencesGroup::builder()
        .title("App Menu")
        .description(
            "Add this app to your launcher so you can find it \
             like any other installed app."
        )
        .build();

    let switch_row = adw::SwitchRow::builder()
        .title("Add to application menu")
        .subtitle("Creates a shortcut and installs the app icon")
        .active(record.integrated)
        .tooltip_text(
            "This makes the app appear in your Activities menu and app launcher, \
             just like a regular installed app. It creates a shortcut file and \
             copies the app's icon to your system."
        )
        .build();

    let record_id = record.id;
    let record_clone = record.clone();
    let db_ref = db.clone();
    let db_dialog = db.clone();
    let record_dialog = record.clone();
    let suppress = Rc::new(Cell::new(false));
    let suppress_ref = suppress.clone();
    switch_row.connect_active_notify(move |row| {
        if suppress_ref.get() {
            return;
        }
        if row.is_active() {
            let row_clone = row.clone();
            let suppress_inner = suppress_ref.clone();
            integration_dialog::show_integration_dialog(
                row,
                &record_dialog,
                &db_dialog,
                move |success| {
                    if !success {
                        suppress_inner.set(true);
                        row_clone.set_active(false);
                        suppress_inner.set(false);
                    }
                },
            );
        } else {
            integrator::undo_all_modifications(&db_ref, record_id).ok();
            integrator::remove_integration(&record_clone).ok();
            db_ref.set_integrated(record_id, false, None).ok();
        }
    });
    integration_group.add(&switch_row);

    if record.integrated {
        if let Some(ref desktop_file) = record.desktop_file {
            let row = adw::ActionRow::builder()
                .title("Shortcut file")
                .subtitle(desktop_file)
                .subtitle_selectable(true)
                .build();
            row.add_css_class("property");
            integration_group.add(&row);
        }
    }
    // Autostart toggle
    let autostart_row = adw::SwitchRow::builder()
        .title("Start at login")
        .subtitle("Launch this app automatically when you log in, like a Windows Startup program")
        .active(record.autostart)
        .tooltip_text(
            "Creates an autostart entry so this app launches \
             when you log in to your desktop."
        )
        .build();
    let record_autostart = record.clone();
    let db_autostart = db.clone();
    let toast_autostart = toast_overlay.clone();
    let record_id_as = record.id;
    autostart_row.connect_active_notify(move |row| {
        if row.is_active() {
            match integrator::enable_autostart(&db_autostart, &record_autostart) {
                Ok(path) => {
                    log::info!("Autostart enabled: {}", path.display());
                    toast_autostart.add_toast(widgets::info_toast(&i18n("Will start at login")));
                }
                Err(e) => {
                    log::error!("Failed to enable autostart: {}", e);
                    toast_autostart.add_toast(widgets::error_toast(&i18n("Failed to enable autostart")));
                }
            }
        } else {
            integrator::disable_autostart(&db_autostart, record_id_as).ok();
            toast_autostart.add_toast(widgets::info_toast(&i18n("Autostart disabled")));
        }
    });
    integration_group.add(&autostart_row);

    // StartupWMClass row with editable override - hidden in Advanced expander
    let wm_class_row = adw::EntryRow::builder()
        .title("Window class")
        .text(record.startup_wm_class.as_deref().unwrap_or(""))
        .show_apply_button(true)
        .build();
    let db_wm = db.clone();
    let record_id_wm = record.id;
    let toast_wm = toast_overlay.clone();
    wm_class_row.connect_apply(move |row| {
        let text = row.text().to_string();
        let value = if text.is_empty() { None } else { Some(text.as_str()) };
        match db_wm.set_startup_wm_class(record_id_wm, value) {
            Ok(()) => toast_wm.add_toast(widgets::info_toast(&i18n("WM class updated"))),
            Err(e) => {
                log::error!("Failed to set WM class: {}", e);
                toast_wm.add_toast(widgets::error_toast(&i18n("Failed to update WM class")));
            }
        }
    });
    let advanced_expander = adw::ExpanderRow::builder()
        .title("Advanced settings")
        .show_enable_switch(false)
        .build();
    advanced_expander.add_row(&wm_class_row);
    integration_group.add(&advanced_expander);

    // System-wide install toggle
    if record.integrated {
        let syswide_row = adw::SwitchRow::builder()
            .title("Install system-wide")
            .subtitle(if record.system_wide {
                "Installed for all users in /opt/driftwood-apps/"
            } else {
                "Make available to all users on this computer"
            })
            .active(record.system_wide)
            .tooltip_text(
                "Copies the AppImage and its shortcut to system directories \
                 so all users on this computer can access it. Requires \
                 administrator privileges."
            )
            .build();

        let record_sw = record.clone();
        let db_sw = db.clone();
        let toast_sw = toast_overlay.clone();
        let record_id_sw = record.id;
        syswide_row.connect_active_notify(move |row| {
            if row.is_active() {
                match integrator::install_system_wide(&record_sw, &db_sw) {
                    Ok(()) => {
                        toast_sw.add_toast(widgets::info_toast(&i18n("Installed system-wide")));
                    }
                    Err(e) => {
                        log::error!("System-wide install failed: {}", e);
                        toast_sw.add_toast(widgets::error_toast(&i18n("System-wide install failed")));
                        row.set_active(false);
                    }
                }
            } else {
                match integrator::remove_system_wide(&db_sw, record_id_sw) {
                    Ok(()) => {
                        toast_sw.add_toast(widgets::info_toast(&i18n("System-wide install removed")));
                    }
                    Err(e) => {
                        log::error!("Failed to remove system-wide install: {}", e);
                        toast_sw.add_toast(widgets::error_toast(&i18n("Failed to remove system-wide install")));
                        row.set_active(true);
                    }
                }
            }
        });
        integration_group.add(&syswide_row);
    }

    inner.append(&integration_group);

    // Version Rollback group
    if let Some(ref prev_path) = record.previous_version_path {
        let prev = std::path::Path::new(prev_path);
        if prev.exists() {
            let rollback_group = adw::PreferencesGroup::builder()
                .title("Version Rollback")
                .description("A previous version is available from the last update.")
                .build();
            let rollback_row = adw::ActionRow::builder()
                .title("Previous version available")
                .subtitle(prev_path.as_str())
                .build();
            let rollback_btn = gtk::Button::builder()
                .label("Rollback")
                .valign(gtk::Align::Center)
                .css_classes(["destructive-action"])
                .tooltip_text("Roll back to the previous version")
                .build();
            rollback_btn.update_property(&[gtk::accessible::Property::Description(
                "Roll back to the previous version",
            )]);
            rollback_row.add_suffix(&rollback_btn);

            let current_path = record.path.clone();
            let prev_path_owned = prev_path.clone();
            let record_id_rb = record.id;
            let db_rb = db.clone();
            let toast_rb = toast_overlay.clone();
            rollback_btn.connect_clicked(move |btn| {
                let current = std::path::Path::new(&current_path);
                let prev = std::path::Path::new(&prev_path_owned);
                let temp_path = current.with_extension("AppImage.rollback-tmp");
                // Swap: current -> tmp, prev -> current, tmp -> prev
                let result = std::fs::rename(current, &temp_path)
                    .and_then(|_| std::fs::rename(prev, current))
                    .and_then(|_| std::fs::rename(&temp_path, prev));
                match result {
                    Ok(()) => {
                        db_rb.set_previous_version(record_id_rb, Some(&prev_path_owned)).ok();
                        toast_rb.add_toast(widgets::info_toast(&i18n("Rolled back to previous version")));
                        btn.set_sensitive(false);
                    }
                    Err(e) => {
                        log::error!("Rollback failed: {}", e);
                        toast_rb.add_toast(widgets::error_toast(&i18n("Rollback failed")));
                    }
                }
            });

            rollback_group.add(&rollback_row);
            inner.append(&rollback_group);
        }
    }

    // File type associations group (grouped by category)
    if let Some(ref mime_str) = record.mime_types {
        let types: Vec<&str> = mime_str.split(';').filter(|s| !s.is_empty()).collect();
        if !types.is_empty() {
            // Group MIME types by category prefix (audio/, video/, etc.)
            let mut groups: BTreeMap<String, Vec<String>> = BTreeMap::new();
            for mime_type in &types {
                let category = mime_type
                    .split('/')
                    .next()
                    .unwrap_or("other")
                    .to_string();
                groups
                    .entry(category)
                    .or_default()
                    .push(mime_type.to_string());
            }
            // Sort MIME types within each group
            for mimes in groups.values_mut() {
                mimes.sort();
            }

            // Check which MIME types are already set as default by this app
            let existing_mods = db.get_modifications(record.id).unwrap_or_default();
            let already_set: std::collections::HashSet<String> = existing_mods
                .iter()
                .filter(|m| m.mod_type == "mime_default")
                .map(|m| m.file_path.clone())
                .collect();

            let mime_group = adw::PreferencesGroup::builder()
                .title("Opens these file types")
                .description("File types this app can handle. Set as default to always open them with this app.")
                .build();

            let app_id = integrator::make_app_id(
                record.app_name.as_deref().unwrap_or(&record.filename),
            );

            // Shared list of all buttons so "Set/Unset All" can toggle everything
            let all_buttons: Rc<RefCell<Vec<gtk::Button>>> = Rc::new(RefCell::new(Vec::new()));

            // "Set Default for All" / "Unset Default for All" header button
            let all_already_set = types.iter().all(|m| already_set.contains(*m));
            let all_btn = gtk::Button::builder()
                .label(if all_already_set { "Unset Default for All" } else { "Set Default for All" })
                .valign(gtk::Align::Center)
                .build();
            all_btn.add_css_class("flat");
            all_btn.update_property(&[gtk::accessible::Property::Description("Set or unset default handler for all file types")]);
            all_buttons.borrow_mut().push(all_btn.clone());

            let all_mimes: Vec<String> = types.iter().map(|s| s.to_string()).collect();
            let db_all = db.clone();
            let record_id_all = record.id;
            let app_id_all = app_id.clone();
            let toast_all = toast_overlay.clone();
            let all_buttons_ref = all_buttons.clone();
            all_btn.connect_clicked(move |btn| {
                let setting = btn.label().map_or(true, |l| l.as_str().starts_with("Set"));
                let mut success_count: usize = 0;
                if setting {
                    for mime in &all_mimes {
                        if integrator::set_mime_default(
                            &db_all, record_id_all, &app_id_all, mime,
                        ).is_ok() {
                            success_count += 1;
                        }
                    }
                    toast_all.add_toast(widgets::info_toast(
                        &i18n_f(
                            "Set as default for {count} file types",
                            &[("{count}", &success_count.to_string())],
                        ),
                    ));
                    for b in all_buttons_ref.borrow().iter() {
                        if b.label().map_or(false, |l| l.as_str() == "Set Default for All") {
                            b.set_label("Unset Default for All");
                        } else {
                            b.set_label("Unset Default");
                        }
                    }
                } else {
                    for mime in &all_mimes {
                        if integrator::unset_mime_default(
                            &db_all, record_id_all, mime,
                        ).is_ok() {
                            success_count += 1;
                        }
                    }
                    toast_all.add_toast(widgets::info_toast(
                        &i18n_f(
                            "Unset default for {count} file types",
                            &[("{count}", &success_count.to_string())],
                        ),
                    ));
                    for b in all_buttons_ref.borrow().iter() {
                        if b.label().map_or(false, |l| l.as_str() == "Unset Default for All") {
                            b.set_label("Set Default for All");
                        } else {
                            b.set_label("Set Default");
                        }
                    }
                }
            });
            mime_group.set_header_suffix(Some(&all_btn));

            // Build per-category expander rows
            for (category, mimes) in &groups {
                let cat_label = {
                    let mut chars = category.chars();
                    match chars.next() {
                        Some(c) => c.to_uppercase().to_string() + chars.as_str(),
                        None => category.clone(),
                    }
                };

                let expander = adw::ExpanderRow::builder()
                    .title(&cat_label)
                    .subtitle(&i18n_f("{count} file types", &[("{count}", &mimes.len().to_string())]))
                    .build();

                // Category button - check if all types in this category are already set
                let cat_already_set = mimes.iter().all(|m| already_set.contains(m));
                let cat_btn = gtk::Button::builder()
                    .label(if cat_already_set { "Unset Default" } else { "Set Default" })
                    .valign(gtk::Align::Center)
                    .build();
                cat_btn.add_css_class("flat");
                cat_btn.update_property(&[gtk::accessible::Property::Description(&format!("Set or unset default for {} file types", cat_label))]);
                all_buttons.borrow_mut().push(cat_btn.clone());

                // Collect buttons for this category so the category handler can toggle them
                let cat_buttons: Rc<RefCell<Vec<gtk::Button>>> = Rc::new(RefCell::new(Vec::new()));
                cat_buttons.borrow_mut().push(cat_btn.clone());

                // Individual MIME type rows
                for mime in mimes {
                    let row = adw::ActionRow::builder()
                        .title(mime)
                        .build();

                    let ind_already_set = already_set.contains(mime);
                    let set_btn = gtk::Button::builder()
                        .label(if ind_already_set { "Unset Default" } else { "Set Default" })
                        .valign(gtk::Align::Center)
                        .build();
                    set_btn.add_css_class("flat");
                    set_btn.update_property(&[gtk::accessible::Property::Description(&format!("Set or unset default for {}", mime))]);
                    all_buttons.borrow_mut().push(set_btn.clone());
                    cat_buttons.borrow_mut().push(set_btn.clone());

                    let db_ind = db.clone();
                    let record_id_ind = record.id;
                    let app_id_ind = app_id.clone();
                    let mime_ind = mime.clone();
                    let toast_ind = toast_overlay.clone();
                    set_btn.connect_clicked(move |btn| {
                        let setting = btn.label().map_or(true, |l| l.as_str().starts_with("Set"));
                        if setting {
                            match integrator::set_mime_default(
                                &db_ind, record_id_ind, &app_id_ind, &mime_ind,
                            ) {
                                Ok(()) => {
                                    toast_ind.add_toast(widgets::info_toast(
                                        &i18n_f("Set as default for {type}", &[("{type}", &mime_ind)]),
                                    ));
                                    btn.set_label("Unset Default");
                                }
                                Err(e) => {
                                    log::error!("Failed to set MIME default: {}", e);
                                    toast_ind.add_toast(widgets::error_toast(
                                        &i18n("Failed to set default"),
                                    ));
                                }
                            }
                        } else {
                            match integrator::unset_mime_default(
                                &db_ind, record_id_ind, &mime_ind,
                            ) {
                                Ok(()) => {
                                    toast_ind.add_toast(widgets::info_toast(
                                        &i18n_f("Unset default for {type}", &[("{type}", &mime_ind)]),
                                    ));
                                    btn.set_label("Set Default");
                                }
                                Err(e) => {
                                    log::error!("Failed to unset MIME default: {}", e);
                                    toast_ind.add_toast(widgets::error_toast(
                                        &i18n("Failed to unset default"),
                                    ));
                                }
                            }
                        }
                    });

                    row.add_suffix(&set_btn);
                    expander.add_row(&row);
                }

                // Category button click handler
                let cat_mimes: Vec<String> = mimes.clone();
                let db_cat = db.clone();
                let record_id_cat = record.id;
                let app_id_cat = app_id.clone();
                let toast_cat = toast_overlay.clone();
                let cat_label_toast = cat_label.clone();
                let cat_buttons_ref = cat_buttons.clone();
                cat_btn.connect_clicked(move |btn| {
                    let setting = btn.label().map_or(true, |l| l.as_str().starts_with("Set"));
                    let mut success_count: usize = 0;
                    if setting {
                        for mime in &cat_mimes {
                            if integrator::set_mime_default(
                                &db_cat, record_id_cat, &app_id_cat, mime,
                            ).is_ok() {
                                success_count += 1;
                            }
                        }
                        toast_cat.add_toast(widgets::info_toast(
                            &i18n_f(
                                "Set as default for {count} {category} file types",
                                &[
                                    ("{count}", &success_count.to_string()),
                                    ("{category}", &cat_label_toast),
                                ],
                            ),
                        ));
                        for b in cat_buttons_ref.borrow().iter() {
                            b.set_label("Unset Default");
                        }
                    } else {
                        for mime in &cat_mimes {
                            if integrator::unset_mime_default(
                                &db_cat, record_id_cat, mime,
                            ).is_ok() {
                                success_count += 1;
                            }
                        }
                        toast_cat.add_toast(widgets::info_toast(
                            &i18n_f(
                                "Unset default for {count} {category} file types",
                                &[
                                    ("{count}", &success_count.to_string()),
                                    ("{category}", &cat_label_toast),
                                ],
                            ),
                        ));
                        for b in cat_buttons_ref.borrow().iter() {
                            b.set_label("Set Default");
                        }
                    }
                });

                expander.add_suffix(&cat_btn);
                mime_group.add(&expander);
            }

            inner.append(&mime_group);
        }
    }

    // Default Application group
    if let Some(ref cats) = record.categories {
        let caps = integrator::detect_default_capabilities(cats);
        if !caps.is_empty() && record.integrated {
            let default_group = adw::PreferencesGroup::builder()
                .title("Default Application")
                .description(
                    "Set this app as your system default for tasks it can handle."
                )
                .build();

            let app_id = integrator::make_app_id(
                record.app_name.as_deref().unwrap_or(&record.filename),
            );

            for cap in &caps {
                let row = adw::ActionRow::builder()
                    .title(cap.label())
                    .build();

                let set_btn = gtk::Button::builder()
                    .label("Set Default")
                    .valign(gtk::Align::Center)
                    .build();
                set_btn.add_css_class("flat");
                set_btn.update_property(&[gtk::accessible::Property::Description(
                    &format!("Set as default for {}", cap.label()),
                )]);

                let db_def = db.clone();
                let record_id = record.id;
                let app_id_clone = app_id.clone();
                let cap_clone = cap.clone();
                let toast_def = toast_overlay.clone();
                set_btn.connect_clicked(move |btn| {
                    match integrator::set_default_app(
                        &db_def, record_id, &app_id_clone, &cap_clone,
                    ) {
                        Ok(()) => {
                            toast_def.add_toast(widgets::info_toast(
                                &i18n_f("Set as default {capability}", &[("{capability}", &cap_clone.label().to_lowercase())]),
                            ));
                            btn.set_sensitive(false);
                            btn.set_label("Default");
                            btn.update_property(&[gtk::accessible::Property::Label("Default (already set)")]);
                        }
                        Err(e) => {
                            log::error!("Failed to set default app: {}", e);
                            toast_def.add_toast(widgets::error_toast(&i18n("Failed to set default")));
                        }
                    }
                });

                row.add_suffix(&set_btn);
                default_group.add(&row);
            }
            inner.append(&default_group);
        }
    }

    // Runtime Compatibility group
    let compat_group = adw::PreferencesGroup::builder()
        .title("Compatibility")
        .description(
            "How well this app works with your system. \
             Most issues can be fixed with a quick install."
        )
        .build();

    let wayland_status = record
        .wayland_status
        .as_deref()
        .map(WaylandStatus::from_str)
        .unwrap_or(WaylandStatus::Unknown);

    let wayland_row = adw::ActionRow::builder()
        .title("Display compatibility")
        .subtitle(wayland_user_explanation(&wayland_status))
        .tooltip_text(
            "Wayland is the modern display system on Linux. Apps built for the \
             older system (X11) still work, but native Wayland apps look sharper, \
             especially on high-resolution screens."
        )
        .build();
    let wayland_badge = widgets::status_badge(wayland_status.label(), wayland_status.badge_class());
    wayland_badge.set_valign(gtk::Align::Center);
    wayland_row.add_suffix(&wayland_badge);
    compat_group.add(&wayland_row);

    // Analyze toolkit button
    let analyze_row = adw::ActionRow::builder()
        .title("Detect app framework")
        .subtitle("Check which technology this app is built with")
        .activatable(true)
        .tooltip_text(
            "Apps are built with different frameworks (like GTK, Qt, or Electron). \
             Knowing the framework helps predict how well the app works with \
             your display system."
        )
        .build();
    analyze_row.add_suffix(&widgets::accessible_suffix_icon("system-search-symbolic", "Analyze"));

    let record_path_wayland = record.path.clone();
    analyze_row.connect_activated(move |row| {
        row.set_sensitive(false);
        row.update_state(&[gtk::accessible::State::Busy(true)]);
        row.set_subtitle("Analyzing...");
        let row_clone = row.clone();
        let path = record_path_wayland.clone();
        glib::spawn_future_local(async move {
            let result = gio::spawn_blocking(move || {
                let appimage_path = std::path::Path::new(&path);
                wayland::analyze_appimage(appimage_path)
            })
            .await;

            row_clone.set_sensitive(true);
            row_clone.update_state(&[gtk::accessible::State::Busy(false)]);
            match result {
                Ok(analysis) => {
                    let toolkit_label = analysis.toolkit.label();
                    let _lib_count = analysis.libraries_found.len();
                    let msg = format!("Built with: {}", toolkit_label);
                    row_clone.set_subtitle(&msg);
                    widgets::announce_result(row_clone.upcast_ref::<gtk::Widget>(), true, &format!("Analysis complete: {}", toolkit_label));
                }
                Err(_) => {
                    row_clone.set_subtitle("Analysis failed - could not read the app's contents");
                    widgets::announce_result(row_clone.upcast_ref::<gtk::Widget>(), false, "Framework analysis failed");
                }
            }
        });
    });
    compat_group.add(&analyze_row);

    // Runtime Wayland status (from post-launch analysis)
    if let Some(ref runtime_status) = record.runtime_wayland_status {
        let runtime_row = adw::ActionRow::builder()
            .title("Last display mode")
            .subtitle(&format!(
                "When this app was last launched, it used: {}",
                runtime_status
            ))
            .tooltip_text(
                "How the app connected to your display the last time it was launched."
            )
            .build();
        if let Some(ref checked) = record.runtime_wayland_checked {
            let info = gtk::Label::builder()
                .label(checked)
                .css_classes(["dimmed", "caption"])
                .valign(gtk::Align::Center)
                .build();
            runtime_row.add_suffix(&info);
        }
        compat_group.add(&runtime_row);
    }

    // FUSE status - always use live system detection (the stored fuse_status
    // is per-app AppImageFuseStatus, not the system-level FuseStatus)
    let fuse_system = fuse::detect_system_fuse();
    let fuse_status = fuse_system.status.clone();

    let fuse_row = adw::ActionRow::builder()
        .title("App startup system")
        .subtitle(fuse_user_explanation(&fuse_status))
        .tooltip_text(
            "Most AppImages need a system component called FUSE to mount and run. \
             This shows whether it is available."
        )
        .build();
    let fuse_badge = widgets::status_badge_with_icon(
        if fuse_status.is_functional() { "emblem-ok-symbolic" } else { "dialog-warning-symbolic" },
        fuse_status.label(),
        fuse_status.badge_class(),
    );
    fuse_badge.set_valign(gtk::Align::Center);
    fuse_row.add_suffix(&fuse_badge);
    if let Some(cmd) = fuse_install_command(&fuse_status) {
        let copy_btn = widgets::copy_button(cmd, Some(toast_overlay));
        copy_btn.set_valign(gtk::Align::Center);
        copy_btn.set_tooltip_text(Some("Copy install command to clipboard"));
        fuse_row.add_suffix(&copy_btn);
    }
    compat_group.add(&fuse_row);

    // Per-app launch method
    let appimage_path = std::path::Path::new(&record.path);
    let app_fuse_status = fuse::determine_app_fuse_status(&fuse_system, appimage_path);
    let launch_method_subtitle = if app_fuse_status.as_str() == "extract_and_run" {
        "This app unpacks itself each time it starts. This is slower than normal but works without extra system setup.".to_string()
    } else {
        format!("This app will launch using: {}", app_fuse_status.label())
    };
    let launch_method_row = adw::ActionRow::builder()
        .title("Startup method")
        .subtitle(&launch_method_subtitle)
        .tooltip_text(
            "AppImages can start two ways: mounting (fast, instant startup) or \
             unpacking to a temporary folder first (slower, but works everywhere). \
             The method is chosen automatically based on your system."
        )
        .build();
    let launch_badge = widgets::status_badge(
        fuse_system.status.as_str(),
        app_fuse_status.badge_class(),
    );
    launch_badge.set_valign(gtk::Align::Center);
    launch_method_row.add_suffix(&launch_badge);
    compat_group.add(&launch_method_row);
    inner.append(&compat_group);

    // Sandboxing group
    let sandbox_group = adw::PreferencesGroup::builder()
        .title("Security Restrictions")
        .description(
            "Control what this app can access on your system. \
             When enabled, the app can only reach your Documents and Downloads folders."
        )
        .build();

    let current_mode = record
        .sandbox_mode
        .as_deref()
        .map(SandboxMode::from_str)
        .unwrap_or(SandboxMode::None);

    let firejail_available = launcher::has_firejail();

    let sandbox_subtitle = if firejail_available {
        format!("Currently: {}", current_mode.display_label())
    } else {
        "Not available yet. Install with one command using the button below.".to_string()
    };

    let firejail_row = adw::SwitchRow::builder()
        .title("Isolate this app")
        .subtitle(&sandbox_subtitle)
        .active(current_mode == SandboxMode::Firejail)
        .sensitive(firejail_available)
        .tooltip_text(
            "Sandboxing restricts what an app can access - files, network, \
             devices, etc. Even if an app has a security issue, it can't \
             freely access your personal data."
        )
        .build();

    // Profile status row - shows current sandbox profile info
    let profile_row = adw::ActionRow::builder()
        .title("Sandbox profile")
        .build();

    let app_name_for_sandbox = record
        .app_name
        .clone()
        .unwrap_or_else(|| {
            std::path::Path::new(&record.filename)
                .file_stem()
                .and_then(|s| s.to_str())
                .unwrap_or("unknown")
                .to_string()
        });

    // Show current profile status
    let loaded_profile = sandbox::load_profile(db, &app_name_for_sandbox)
        .ok()
        .flatten();
    let profile_badge = if loaded_profile.is_some() {
        widgets::status_badge("Active", "success")
    } else if current_mode == SandboxMode::Firejail {
        widgets::status_badge("Default", "neutral")
    } else {
        widgets::status_badge("None", "dim")
    };
    profile_badge.set_valign(gtk::Align::Center);
    profile_row.add_suffix(&profile_badge);

    // Show total profile count across all apps in group description
    let all_profiles = sandbox::list_profiles(db);
    if !all_profiles.is_empty() {
        sandbox_group.set_description(Some(&format!(
            "Restrict what this app can access on your system \
             for extra security. {} sandbox {} configured across all apps.",
            all_profiles.len(),
            if all_profiles.len() == 1 { "profile" } else { "profiles" },
        )));
    }

    let record_id = record.id;
    let db_ref = db.clone();
    let sandbox_name = app_name_for_sandbox.clone();
    let profile_row_ref = profile_row.clone();
    firejail_row.connect_active_notify(move |row| {
        let mode = if row.is_active() {
            SandboxMode::Firejail
        } else {
            SandboxMode::None
        };
        if let Err(e) = db_ref.update_sandbox_mode(record_id, Some(mode.as_str())) {
            log::warn!("Failed to update sandbox mode: {}", e);
        }
        // Auto-generate a default sandbox profile when enabling isolation
        if mode == SandboxMode::Firejail {
            let existing = sandbox::load_profile(&db_ref, &sandbox_name)
                .ok()
                .flatten();
            if existing.is_none() {
                let profile = sandbox::generate_default_profile(&sandbox_name);
                match sandbox::save_profile(&db_ref, &profile) {
                    Ok(path) => {
                        log::info!("Created default sandbox profile at {}", path.display());
                        // Update badge to show profile is active
                        while let Some(child) = profile_row_ref.last_child() {
                            profile_row_ref.remove(&child);
                        }
                        let badge = widgets::status_badge("Active", "success");
                        badge.set_valign(gtk::Align::Center);
                        profile_row_ref.add_suffix(&badge);
                    }
                    Err(e) => log::warn!("Failed to create sandbox profile: {}", e),
                }
            }
        }
    });
    sandbox_group.add(&firejail_row);

    if firejail_available && current_mode == SandboxMode::Firejail {
        // Show profile explanation when sandbox is active
        if let Some(path) = sandbox::profile_path_for_app(&app_name_for_sandbox) {
            profile_row.set_subtitle(&format!(
                "Blocks access to most of your files, camera, microphone, USB devices, and other apps. Only Documents and Downloads folders are accessible.\n{}",
                path.display()
            ));
        } else {
            profile_row.set_subtitle("Default restrictive profile will be created on next launch");
        }

        // Reset button to delete current profile and regenerate default
        if let Some(ref profile) = loaded_profile {
            if let Some(profile_id) = profile.id {
                let reset_btn = gtk::Button::builder()
                    .icon_name("edit-clear-symbolic")
                    .tooltip_text("Reset to default profile")
                    .valign(gtk::Align::Center)
                    .css_classes(["flat"])
                    .build();
                reset_btn.update_property(&[gtk::accessible::Property::Label("Reset sandbox profile to default")]);
                let db_reset = db.clone();
                let reset_name = app_name_for_sandbox.clone();
                reset_btn.connect_clicked(move |_btn| {
                    if let Err(e) = sandbox::delete_profile(&db_reset, profile_id) {
                        log::warn!("Failed to delete sandbox profile: {}", e);
                        return;
                    }
                    // Regenerate a fresh default
                    let profile = sandbox::generate_default_profile(&reset_name);
                    if let Err(e) = sandbox::save_profile(&db_reset, &profile) {
                        log::warn!("Failed to regenerate sandbox profile: {}", e);
                    } else {
                        log::info!("Reset sandbox profile for {}", reset_name);
                    }
                });
                profile_row.add_suffix(&reset_btn);
            }
        }
        sandbox_group.add(&profile_row);
    }

    if !firejail_available {
        let firejail_cmd = "sudo apt install firejail";
        let info_row = adw::ActionRow::builder()
            .title("Install app isolation")
            .subtitle("Install with one command")
            .build();
        let badge = widgets::status_badge("Missing", "warning");
        badge.set_valign(gtk::Align::Center);
        info_row.add_suffix(&badge);
        let copy_btn = widgets::copy_button(firejail_cmd, Some(toast_overlay));
        copy_btn.set_valign(gtk::Align::Center);
        copy_btn.set_tooltip_text(Some("Copy install command to clipboard"));
        info_row.add_suffix(&copy_btn);
        sandbox_group.add(&info_row);
    }
    inner.append(&sandbox_group);

    clamp.set_child(Some(&inner));
    tab.append(&clamp);
    tab
}

// ---------------------------------------------------------------------------
// Tab 3: Security - vulnerability scanning and integrity
// ---------------------------------------------------------------------------

fn build_security_tab(record: &AppImageRecord, db: &Rc<Database>) -> gtk::Box {
    let tab = gtk::Box::builder()
        .orientation(gtk::Orientation::Vertical)
        .spacing(24)
        .margin_top(18)
        .margin_bottom(24)
        .margin_start(18)
        .margin_end(18)
        .build();

    let clamp = adw::Clamp::builder()
        .maximum_size(800)
        .tightening_threshold(600)
        .build();

    let inner = gtk::Box::builder()
        .orientation(gtk::Orientation::Vertical)
        .spacing(24)
        .build();

    // Verification group
    let verify_group = adw::PreferencesGroup::builder()
        .title("Verification")
        .description("Check the integrity and authenticity of this AppImage")
        .build();

    // Signature status
    let sig_label = if record.has_signature { "Signature present" } else { "No signature" };
    let sig_badge_class = if record.has_signature { "success" } else { "neutral" };
    let sig_row = adw::ActionRow::builder()
        .title("Verified by developer")
        .subtitle(sig_label)
        .build();
    let sig_badge = widgets::status_badge(sig_label, sig_badge_class);
    sig_badge.set_valign(gtk::Align::Center);
    sig_row.add_suffix(&sig_badge);
    verify_group.add(&sig_row);

    // SHA256 hash
    if let Some(ref hash) = record.sha256 {
        let sha_row = adw::ActionRow::builder()
            .title("File checksum")
            .subtitle(hash)
            .subtitle_selectable(true)
            .build();
        verify_group.add(&sha_row);
    }

    // Verification status
    let verify_status = record.verification_status.as_deref().unwrap_or("not_checked");
    let verify_row = adw::ActionRow::builder()
        .title("Verification")
        .subtitle(match verify_status {
            "signed_valid" => "Signed and verified",
            "signed_invalid" => "Signature present but invalid",
            "checksum_match" => "Checksum verified",
            "checksum_mismatch" => "Checksum mismatch - file may be corrupted",
            _ => "Not verified",
        })
        .build();
    let verify_badge = widgets::status_badge(
        match verify_status {
            "signed_valid" | "checksum_match" => "Verified",
            "signed_invalid" | "checksum_mismatch" => "Failed",
            _ => "Unchecked",
        },
        match verify_status {
            "signed_valid" | "checksum_match" => "success",
            "signed_invalid" | "checksum_mismatch" => "error",
            _ => "neutral",
        },
    );
    verify_badge.set_valign(gtk::Align::Center);
    verify_row.add_suffix(&verify_badge);
    verify_group.add(&verify_row);

    // Manual SHA256 verification input
    let sha_input = adw::EntryRow::builder()
        .title("Verify SHA256")
        .show_apply_button(true)
        .build();
    sha_input.set_tooltip_text(Some("Paste an expected SHA256 hash to verify this file"));

    let record_path = record.path.clone();
    let record_id_v = record.id;
    let db_verify = db.clone();
    sha_input.connect_apply(move |row| {
        let expected = row.text().to_string();
        if expected.is_empty() {
            return;
        }
        let path = record_path.clone();
        let db_ref = db_verify.clone();
        let row_ref = row.clone();
        glib::spawn_future_local(async move {
            let result = gio::spawn_blocking(move || {
                let p = std::path::Path::new(&path);
                crate::core::verification::verify_sha256(p, &expected)
            })
            .await;

            if let Ok(status) = result {
                let label = status.label();
                row_ref.set_title(&format!("Verify SHA256 - {}", label));
                db_ref.set_verification_status(record_id_v, status.as_str()).ok();
                let success = status.as_str() == "verified";
                widgets::announce_result(row_ref.upcast_ref::<gtk::Widget>(), success, &format!("SHA-256 verification: {}", label));
            }
        });
    });
    verify_group.add(&sha_input);

    // Check signature button
    let check_sig_row = adw::ActionRow::builder()
        .title("Check embedded signature")
        .subtitle("Verify GPG signature if present")
        .activatable(true)
        .build();
    let sig_arrow = widgets::accessible_suffix_icon("go-next-symbolic", "Check signature");
    sig_arrow.set_valign(gtk::Align::Center);
    check_sig_row.add_suffix(&sig_arrow);
    let record_path_sig = record.path.clone();
    let record_id_sig = record.id;
    let db_sig = db.clone();
    check_sig_row.connect_activated(move |row| {
        row.set_sensitive(false);
        row.set_subtitle("Checking...");
        row.update_state(&[gtk::accessible::State::Busy(true)]);
        let path = record_path_sig.clone();
        let db_ref = db_sig.clone();
        let row_ref = row.clone();
        glib::spawn_future_local(async move {
            let result = gio::spawn_blocking(move || {
                let p = std::path::Path::new(&path);
                crate::core::verification::check_embedded_signature(p)
            })
            .await;

            if let Ok(status) = result {
                let label = status.label();
                row_ref.set_subtitle(&label);
                db_ref.set_verification_status(record_id_sig, status.as_str()).ok();
                let badge_text = match status.badge_class() {
                    "success" => "Verified",
                    "error" => "Failed",
                    _ => "Unknown",
                };
                let result_badge = widgets::status_badge(badge_text, status.badge_class());
                result_badge.set_valign(gtk::Align::Center);
                row_ref.add_suffix(&result_badge);
                let success = status.badge_class() == "success";
                widgets::announce_result(row_ref.upcast_ref::<gtk::Widget>(), success, &format!("Signature check: {}", badge_text));
            }
            row_ref.update_state(&[gtk::accessible::State::Busy(false)]);
            row_ref.set_sensitive(true);
        });
    });
    verify_group.add(&check_sig_row);
    inner.append(&verify_group);

    let group = adw::PreferencesGroup::builder()
        .title("Security Check")
        .description(
            "Check this app for known security issues."
        )
        .build();

    let libs = db.get_bundled_libraries(record.id).unwrap_or_default();
    let summary = db.get_cve_summary(record.id).unwrap_or_default();

    if libs.is_empty() {
        let row = adw::ActionRow::builder()
            .title("Security scan")
            .subtitle(
                "This app has not been scanned yet. Use the button below \
                 to check for known vulnerabilities."
            )
            .build();
        let badge = widgets::status_badge("Not scanned", "neutral");
        badge.set_valign(gtk::Align::Center);
        row.add_suffix(&badge);
        group.add(&row);
    } else {
        let lib_row = adw::ActionRow::builder()
            .title("Included components")
            .subtitle(&format!(
                "{} components found inside this app",
                libs.len()
            ))
            .tooltip_text(
                "Apps bundle their own copies of system components (libraries). \
                 These can sometimes contain known security issues if they're outdated."
            )
            .build();
        group.add(&lib_row);

        if summary.total() == 0 {
            let row = adw::ActionRow::builder()
                .title("Vulnerabilities")
                .subtitle("No known security issues found.")
                .build();
            let badge = widgets::status_badge("Clean", "success");
            badge.set_valign(gtk::Align::Center);
            row.add_suffix(&badge);
            group.add(&row);
        } else {
            let row = adw::ActionRow::builder()
                .title("Vulnerabilities")
                .subtitle(&format!(
                    "{} known issue{} found. Consider updating this app if a newer version is available.",
                    summary.total(),
                    if summary.total() == 1 { "" } else { "s" },
                ))
                .build();
            let badge = widgets::status_badge(summary.max_severity(), summary.badge_class());
            badge.set_valign(gtk::Align::Center);
            row.add_suffix(&badge);
            group.add(&row);
        }
    }

    // Scan button
    let scan_row = adw::ActionRow::builder()
        .title("Run security check")
        .subtitle("Check for known security issues in this app")
        .activatable(true)
        .tooltip_text(
            "This checks the components inside this app against a public database \
             of known security issues to see if any are outdated or vulnerable."
        )
        .build();
    scan_row.add_suffix(&widgets::accessible_suffix_icon("security-medium-symbolic", "Scan"));

    let record_id = record.id;
    let record_path = record.path.clone();
    scan_row.connect_activated(move |row| {
        row.set_sensitive(false);
        row.update_state(&[gtk::accessible::State::Busy(true)]);
        row.set_subtitle("Scanning - this may take a moment...");
        let row_clone = row.clone();
        let path = record_path.clone();
        glib::spawn_future_local(async move {
            let result = gio::spawn_blocking(move || {
                let bg_db = Database::open().expect("Failed to open database");
                let appimage_path = std::path::Path::new(&path);
                security::scan_and_store(&bg_db, record_id, appimage_path)
            })
            .await;

            row_clone.set_sensitive(true);
            row_clone.update_state(&[gtk::accessible::State::Busy(false)]);
            match result {
                Ok(scan_result) => {
                    let total = scan_result.total_cves();
                    if total == 0 {
                        row_clone.set_subtitle("No vulnerabilities found - looking good!");
                        widgets::announce_result(row_clone.upcast_ref::<gtk::Widget>(), true, "Security scan complete: no vulnerabilities found");
                    } else {
                        row_clone.set_subtitle(&format!(
                            "Found {} known issue{}. Check for app updates.",
                            total,
                            if total == 1 { "" } else { "s" },
                        ));
                        widgets::announce_result(row_clone.upcast_ref::<gtk::Widget>(), false, &format!("Security scan complete: {} issues found", total));
                    }
                }
                Err(_) => {
                    row_clone.set_subtitle("Check failed - could not read the app's contents");
                    widgets::announce_result(row_clone.upcast_ref::<gtk::Widget>(), false, "Security scan failed");
                }
            }
        });
    });
    group.add(&scan_row);
    inner.append(&group);

    // Integrity group
    if record.sha256.is_some() {
        let integrity_group = adw::PreferencesGroup::builder()
            .title("Integrity")
            .description("Verify that the file has not been modified or corrupted.")
            .build();

        if let Some(ref hash) = record.sha256 {
            let hash_row = adw::ActionRow::builder()
                .title("File fingerprint")
                .subtitle(hash)
                .subtitle_selectable(true)
                .tooltip_text(
                    "A unique code (SHA256 checksum) generated from the file's contents. \
                     If the file changes in any way, this code changes too. You can compare \
                     it against the developer's published fingerprint to verify nothing \
                     was altered."
                )
                .build();
            hash_row.add_css_class("property");
            integrity_group.add(&hash_row);
        }
        inner.append(&integrity_group);
    }

    clamp.set_child(Some(&inner));
    tab.append(&clamp);
    tab
}

// ---------------------------------------------------------------------------
// Tab 4: Storage - disk usage, data paths, file location
// ---------------------------------------------------------------------------

fn build_storage_tab(
    record: &AppImageRecord,
    db: &Rc<Database>,
    toast_overlay: &adw::ToastOverlay,
) -> gtk::Box {
    let tab = gtk::Box::builder()
        .orientation(gtk::Orientation::Vertical)
        .spacing(24)
        .margin_top(18)
        .margin_bottom(24)
        .margin_start(18)
        .margin_end(18)
        .build();

    let clamp = adw::Clamp::builder()
        .maximum_size(800)
        .tightening_threshold(600)
        .build();

    let inner = gtk::Box::builder()
        .orientation(gtk::Orientation::Vertical)
        .spacing(24)
        .build();

    // Disk usage group
    let size_group = adw::PreferencesGroup::builder()
        .title("Disk Usage")
        .description(
            "Disk space used by this app, including any configuration, \
             cache, or data files it may have created."
        )
        .build();

    let fp = footprint::get_footprint(db, record.id, record.size_bytes as u64);

    let appimage_row = adw::ActionRow::builder()
        .title("AppImage file")
        .subtitle(&widgets::format_size(record.size_bytes))
        .build();
    size_group.add(&appimage_row);

    if record.is_portable {
        let location_label = if let Some(ref mp) = record.mount_point {
            format!("Portable ({})", mp)
        } else {
            "Portable (removable media)".to_string()
        };
        let location_row = adw::ActionRow::builder()
            .title("Location")
            .subtitle(&location_label)
            .build();
        let badge = widgets::status_badge("Portable", "info");
        badge.set_valign(gtk::Align::Center);
        location_row.add_suffix(&badge);
        size_group.add(&location_row);
    }

    if !fp.paths.is_empty() {
        let categories: &[(&str, u64, &str)] = &[
            ("Configuration", fp.config_size, "Your preferences and settings for this app"),
            ("Application data", fp.data_size, "Files and data saved by this app"),
            ("Cache", fp.cache_size, "Temporary files that can be safely deleted"),
            ("State", fp.state_size, "Runtime state like window positions and recent files"),
            ("Other", fp.other_size, "Other files associated with this app"),
        ];
        for (label, size, tooltip) in categories {
            if *size > 0 {
                let row = adw::ActionRow::builder()
                    .title(*label)
                    .subtitle(&widgets::format_size(*size as i64))
                    .tooltip_text(*tooltip)
                    .build();
                size_group.add(&row);
            }
        }
        let data_total = fp.data_total();
        if data_total > 0 {
            let total_row = adw::ActionRow::builder()
                .title("Total disk usage")
                .subtitle(&format!(
                    "{} (AppImage) + {} (app data) = {}",
                    widgets::format_size(record.size_bytes),
                    widgets::format_size(data_total as i64),
                    widgets::format_size(fp.total_size() as i64),
                ))
                .build();
            size_group.add(&total_row);
        }
    }
    inner.append(&size_group);

    // Data paths group
    let paths_group = adw::PreferencesGroup::builder()
        .title("App Data")
        .description("Settings, cache, and data this app may have saved to your system.")
        .build();

    // Discover button
    let discover_row = adw::ActionRow::builder()
        .title("Find app data")
        .subtitle("Search for files this app has saved")
        .activatable(true)
        .build();
    discover_row.add_suffix(&widgets::accessible_suffix_icon("folder-saved-search-symbolic", "Search"));

    let record_clone = record.clone();
    let record_id = record.id;
    discover_row.connect_activated(move |row| {
        row.set_sensitive(false);
        row.set_subtitle("Searching...");
        row.update_state(&[gtk::accessible::State::Busy(true)]);
        let row_clone = row.clone();
        let rec = record_clone.clone();
        glib::spawn_future_local(async move {
            let result = gio::spawn_blocking(move || {
                let bg_db = Database::open().expect("Failed to open database");
                footprint::discover_and_store(&bg_db, record_id, &rec);
                footprint::get_footprint(&bg_db, record_id, rec.size_bytes as u64)
            })
            .await;

            row_clone.set_sensitive(true);
            row_clone.update_state(&[gtk::accessible::State::Busy(false)]);
            match result {
                Ok(fp) => {
                    let count = fp.paths.len();
                    if count == 0 {
                        row_clone.set_subtitle("No saved data found");
                    } else {
                        row_clone.set_subtitle(&format!(
                            "Found {} path{} using {}",
                            count,
                            if count == 1 { "" } else { "s" },
                            widgets::format_size(fp.data_total() as i64),
                        ));
                    }
                }
                Err(_) => {
                    row_clone.set_subtitle("Search failed");
                }
            }
        });
    });
    paths_group.add(&discover_row);

    // Individual discovered paths
    for dp in &fp.paths {
        if dp.exists {
            let row = adw::ActionRow::builder()
                .title(dp.path_type.label())
                .subtitle(&*dp.path.to_string_lossy())
                .subtitle_selectable(true)
                .build();
            let icon = gtk::Image::from_icon_name(dp.path_type.icon_name());
            icon.set_pixel_size(16);
            icon.set_accessible_role(gtk::AccessibleRole::Presentation);
            row.add_prefix(&icon);
            let conf_badge = widgets::status_badge(
                dp.confidence.as_str(),
                dp.confidence.badge_class(),
            );
            conf_badge.set_valign(gtk::Align::Center);
            row.add_suffix(&conf_badge);
            let size_label = gtk::Label::builder()
                .label(&widgets::format_size(dp.size_bytes as i64))
                .css_classes(["dimmed", "caption"])
                .valign(gtk::Align::Center)
                .build();
            row.add_suffix(&size_label);

            // Open folder button
            let open_btn = widgets::accessible_icon_button(
                "folder-open-symbolic",
                "Open in file manager",
                "Open in file manager",
            );
            let path_str = dp.path.to_string_lossy().to_string();
            open_btn.connect_clicked(move |_| {
                let file = gio::File::for_path(&path_str);
                let launcher = gtk::FileLauncher::new(Some(&file));
                launcher.open_containing_folder(gtk::Window::NONE, None::<&gio::Cancellable>, |_| {});
            });
            row.add_suffix(&open_btn);

            paths_group.add(&row);
        }
    }
    inner.append(&paths_group);

    // Backup estimate
    let est_size = fp.config_size + fp.data_size + fp.state_size;
    if est_size > 0 {
        let estimate_row = adw::ActionRow::builder()
            .title("Backup estimate")
            .subtitle(&format!(
                "Estimated backup size: {} (settings and data files)",
                widgets::format_size(est_size as i64)
            ))
            .build();
        paths_group.add(&estimate_row);
    }

    // Backups group
    inner.append(&build_backup_group(record.id, toast_overlay));

    // Uninstall group
    let uninstall_group = adw::PreferencesGroup::builder()
        .title("Uninstall")
        .description("Remove this AppImage and optionally clean up its data")
        .build();

    let uninstall_btn = gtk::Button::builder()
        .label("Uninstall AppImage...")
        .halign(gtk::Align::Start)
        .margin_top(6)
        .margin_bottom(6)
        .build();
    uninstall_btn.add_css_class("destructive-action");
    uninstall_btn.add_css_class("pill");

    let record_uninstall = record.clone();
    let db_uninstall = db.clone();
    let toast_uninstall = toast_overlay.clone();
    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;
    uninstall_btn.connect_clicked(move |_btn| {
        show_uninstall_dialog(
            &toast_uninstall,
            &record_uninstall,
            &db_uninstall,
            is_integrated,
            &fp_paths,
        );
    });
    uninstall_group.add(&adw::ActionRow::builder()
        .child(&uninstall_btn)
        .build());
    inner.append(&uninstall_group);

    // File location group
    let location_group = adw::PreferencesGroup::builder()
        .title("File Location")
        .build();

    let path_row = adw::ActionRow::builder()
        .title("File location")
        .subtitle(&record.path)
        .subtitle_selectable(true)
        .build();
    path_row.add_css_class("property");
    let copy_path_btn = widgets::copy_button(&record.path, Some(toast_overlay));
    copy_path_btn.set_valign(gtk::Align::Center);
    path_row.add_suffix(&copy_path_btn);

    // Open folder button
    let folder_path = std::path::Path::new(&record.path)
        .parent()
        .map(|p| p.to_string_lossy().to_string())
        .unwrap_or_default();
    if !folder_path.is_empty() {
        let open_folder_btn = gtk::Button::builder()
            .icon_name("folder-open-symbolic")
            .tooltip_text("Open containing folder")
            .valign(gtk::Align::Center)
            .build();
        open_folder_btn.add_css_class("flat");
        open_folder_btn.update_property(&[
            gtk::accessible::Property::Label("Open containing folder"),
        ]);
        let folder = folder_path.clone();
        open_folder_btn.connect_clicked(move |_| {
            let file = gio::File::for_path(&folder);
            let launcher = gtk::FileLauncher::new(Some(&file));
            launcher.open_containing_folder(gtk::Window::NONE, None::<&gio::Cancellable>, |_| {});
        });
        path_row.add_suffix(&open_folder_btn);
    }
    location_group.add(&path_row);
    inner.append(&location_group);

    clamp.set_child(Some(&inner));
    tab.append(&clamp);
    tab
}

fn build_backup_group(record_id: i64, toast_overlay: &adw::ToastOverlay) -> adw::PreferencesGroup {
    let group = adw::PreferencesGroup::builder()
        .title("Backups")
        .description("Save and restore this app's settings and data files")
        .build();

    // Fetch existing backups
    let db = Database::open().ok();
    let backups = db.as_ref()
        .map(|d| backup::list_backups(d, Some(record_id)))
        .unwrap_or_default();

    if backups.is_empty() {
        let empty_row = adw::ActionRow::builder()
            .title("No backups yet")
            .subtitle("Create a backup to save this app's settings and data")
            .build();
        let empty_icon = gtk::Image::from_icon_name("document-open-symbolic");
        empty_icon.set_valign(gtk::Align::Center);
        empty_icon.add_css_class("dim-label");
        empty_icon.set_accessible_role(gtk::AccessibleRole::Presentation);
        empty_row.add_prefix(&empty_icon);
        group.add(&empty_row);
    } else {
        for b in &backups {
            log::debug!(
                "Listing backup id={} for appimage_id={} at {}",
                b.id, b.appimage_id, b.archive_path,
            );
            let expander = adw::ExpanderRow::builder()
                .title(&b.created_at)
                .subtitle(&format!(
                    "v{} - {} - {} file{}",
                    b.app_version.as_deref().unwrap_or("unknown"),
                    widgets::format_size(b.archive_size),
                    b.path_count,
                    if b.path_count == 1 { "" } else { "s" },
                ))
                .build();

            // Exists/missing badge using icon + text (not color-only)
            let badge = if b.exists {
                let bx = gtk::Box::builder()
                    .orientation(gtk::Orientation::Horizontal)
                    .spacing(4)
                    .valign(gtk::Align::Center)
                    .build();
                let icon = gtk::Image::from_icon_name("emblem-ok-symbolic");
                icon.add_css_class("success");
                icon.set_accessible_role(gtk::AccessibleRole::Presentation);
                let label = gtk::Label::new(Some("Exists"));
                label.add_css_class("caption");
                label.add_css_class("success");
                bx.append(&icon);
                bx.append(&label);
                bx
            } else {
                let bx = gtk::Box::builder()
                    .orientation(gtk::Orientation::Horizontal)
                    .spacing(4)
                    .valign(gtk::Align::Center)
                    .build();
                let icon = gtk::Image::from_icon_name("dialog-warning-symbolic");
                icon.add_css_class("warning");
                icon.set_accessible_role(gtk::AccessibleRole::Presentation);
                let label = gtk::Label::new(Some("Missing"));
                label.add_css_class("caption");
                label.add_css_class("warning");
                bx.append(&icon);
                bx.append(&label);
                bx
            };
            expander.add_suffix(&badge);

            // Restore row
            let restore_row = adw::ActionRow::builder()
                .title("Restore")
                .subtitle("Restore settings and data from this backup")
                .activatable(true)
                .tooltip_text("Overwrite current settings with this backup")
                .build();
            let restore_icon = gtk::Image::from_icon_name("edit-undo-symbolic");
            restore_icon.set_valign(gtk::Align::Center);
            restore_icon.set_accessible_role(gtk::AccessibleRole::Presentation);
            restore_row.add_prefix(&restore_icon);
            restore_row.update_property(&[
                gtk::accessible::Property::Label("Restore this backup"),
            ]);

            let archive_path = b.archive_path.clone();
            let toast_restore = toast_overlay.clone();
            restore_row.connect_activated(move |row| {
                row.set_sensitive(false);
                row.set_subtitle("Restoring...");
                let row_clone = row.clone();
                let path = archive_path.clone();
                let toast = toast_restore.clone();
                glib::spawn_future_local(async move {
                    let result = gio::spawn_blocking(move || {
                        backup::restore_backup(std::path::Path::new(&path))
                    })
                    .await;

                    row_clone.set_sensitive(true);
                    match result {
                        Ok(Ok(res)) => {
                            let skip_note = if res.paths_skipped > 0 {
                                format!(" ({} skipped)", res.paths_skipped)
                            } else {
                                String::new()
                            };
                            row_clone.set_subtitle(&format!(
                                "Restored {} path{}{}",
                                res.paths_restored,
                                if res.paths_restored == 1 { "" } else { "s" },
                                skip_note,
                            ));
                            let toast_msg = format!(
                                "Restored {} path{}{}",
                                res.paths_restored,
                                if res.paths_restored == 1 { "" } else { "s" },
                                skip_note,
                            );
                            toast.add_toast(widgets::info_toast(&toast_msg));
                            widgets::announce_result(row_clone.upcast_ref::<gtk::Widget>(), true, &toast_msg);
                            log::info!(
                                "Backup restored: app={}, paths_restored={}, paths_skipped={}",
                                res.manifest.app_name, res.paths_restored, res.paths_skipped,
                            );
                        }
                        _ => {
                            row_clone.set_subtitle("Restore failed");
                            toast.add_toast(widgets::error_toast(&i18n("Failed to restore backup")));
                            widgets::announce_result(row_clone.upcast_ref::<gtk::Widget>(), false, "Restore failed");
                        }
                    }
                });
            });
            expander.add_row(&restore_row);

            // Delete row
            let delete_row = adw::ActionRow::builder()
                .title("Delete")
                .subtitle("Permanently remove this backup")
                .activatable(true)
                .tooltip_text("Delete this backup archive from disk")
                .build();
            let delete_icon = gtk::Image::from_icon_name("edit-delete-symbolic");
            delete_icon.set_valign(gtk::Align::Center);
            delete_icon.set_accessible_role(gtk::AccessibleRole::Presentation);
            delete_row.add_prefix(&delete_icon);
            delete_row.update_property(&[
                gtk::accessible::Property::Label("Delete this backup"),
            ]);

            let backup_id = b.id;
            let toast_delete = toast_overlay.clone();
            let group_ref = group.clone();
            let expander_ref = expander.clone();
            delete_row.connect_activated(move |row| {
                row.set_sensitive(false);
                row.set_subtitle("Deleting...");
                let row_clone = row.clone();
                let toast = toast_delete.clone();
                let group_del = group_ref.clone();
                let expander_del = expander_ref.clone();
                glib::spawn_future_local(async move {
                    let result = gio::spawn_blocking(move || {
                        let bg_db = Database::open().expect("Failed to open database");
                        backup::delete_backup(&bg_db, backup_id)
                    })
                    .await;

                    match result {
                        Ok(Ok(())) => {
                            group_del.remove(&expander_del);
                            toast.add_toast(widgets::info_toast(&i18n("Backup deleted")));
                        }
                        _ => {
                            row_clone.set_sensitive(true);
                            row_clone.set_subtitle("Delete failed");
                            toast.add_toast(widgets::error_toast(&i18n("Failed to delete backup")));
                            widgets::announce_result(row_clone.upcast_ref::<gtk::Widget>(), false, "Delete failed");
                        }
                    }
                });
            });
            expander.add_row(&delete_row);

            group.add(&expander);
        }
    }

    // Create backup row (always shown at bottom)
    let create_row = adw::ActionRow::builder()
        .title("Create backup")
        .subtitle("Save a snapshot of this app's settings and data")
        .activatable(true)
        .tooltip_text("Create a new backup of this app's configuration files")
        .build();
    let create_icon = gtk::Image::from_icon_name("list-add-symbolic");
    create_icon.set_valign(gtk::Align::Center);
    create_icon.set_accessible_role(gtk::AccessibleRole::Presentation);
    create_row.add_prefix(&create_icon);
    create_row.update_property(&[
        gtk::accessible::Property::Label("Create a new backup"),
    ]);

    let toast_create = toast_overlay.clone();
    create_row.connect_activated(move |row| {
        row.set_sensitive(false);
        row.set_subtitle("Creating backup...");
        let row_clone = row.clone();
        let toast = toast_create.clone();
        glib::spawn_future_local(async move {
            let result = gio::spawn_blocking(move || {
                let bg_db = Database::open().expect("Failed to open database");
                backup::create_backup(&bg_db, record_id)
            })
            .await;

            row_clone.set_sensitive(true);
            match result {
                Ok(Ok(path)) => {
                    let filename = path.file_name()
                        .and_then(|n| n.to_str())
                        .unwrap_or("backup");
                    row_clone.set_subtitle(&format!("Created {}", filename));
                    toast.add_toast(widgets::info_toast(&i18n("Backup created")));
                }
                Ok(Err(backup::BackupError::NoPaths)) => {
                    row_clone.set_subtitle("Try discovering app data first");
                    toast.add_toast(widgets::error_toast(&i18n("No data paths found to back up")));
                }
                _ => {
                    row_clone.set_subtitle("Backup failed");
                    toast.add_toast(widgets::error_toast(&i18n("Failed to create backup")));
                }
            }
        });
    });
    group.add(&create_row);

    group
}

// ---------------------------------------------------------------------------
// User-friendly explanations
// ---------------------------------------------------------------------------

fn wayland_user_explanation(status: &WaylandStatus) -> &'static str {
    match status {
        WaylandStatus::Native =>
            "Fully compatible - the best experience on your system.",
        WaylandStatus::XWayland =>
            "Works through a compatibility layer. May appear slightly \
             blurry on high-resolution screens.",
        WaylandStatus::Possible =>
            "Might work well. Try launching it to find out.",
        WaylandStatus::X11Only =>
            "Built for an older display system. It will run automatically, \
             but you may notice minor visual quirks.",
        WaylandStatus::Unknown =>
            "Not yet determined. Launch the app or use 'Detect app framework' to check.",
    }
}

fn fuse_user_explanation(status: &FuseStatus) -> &'static str {
    match status {
        FuseStatus::FullyFunctional =>
            "This app can start normally using your system's app loader.",
        FuseStatus::Fuse3Only =>
            "A system component is needed for this app to start normally. \
             Without it, the app uses a slower startup method.",
        FuseStatus::NoFusermount =>
            "A system component is needed for this app to start normally. \
             Without it, the app uses a slower startup method.",
        FuseStatus::NoDevFuse =>
            "A system component is needed for this app to start normally. \
             Without it, the app uses a slower startup method.",
        FuseStatus::MissingLibfuse2 =>
            "A system component is needed for this app to start normally. \
             Without it, the app uses a slower startup method.",
    }
}

/// Return an install command for a FUSE status that needs one, or None.
fn fuse_install_command(status: &FuseStatus) -> Option<&'static str> {
    match status {
        FuseStatus::Fuse3Only | FuseStatus::MissingLibfuse2 => {
            Some("sudo apt install libfuse2")
        }
        _ => None,
    }
}

/// Show a screenshot in a fullscreen lightbox window with prev/next navigation.
/// Uses a separate gtk::Window to avoid parent scroll position interference.
/// Pass `screenshot_paths` and `app_name` for catalog detail views to preload
/// screenshots that haven't been loaded yet (paged carousel lazy-loading).
pub fn show_screenshot_lightbox(
    parent: &gtk::Window,
    textures: &Rc<std::cell::RefCell<Vec<Option<gtk::gdk::Texture>>>>,
    initial_index: usize,
    screenshot_paths: Option<&Rc<Vec<String>>>,
    app_name: Option<&str>,
) {
    let current = Rc::new(std::cell::Cell::new(initial_index));
    let textures = textures.clone();
    let count = textures.borrow().len();
    let has_multiple = count > 1;

    // Fullscreen modal window with dark background
    let win = gtk::Window::builder()
        .transient_for(parent)
        .modal(true)
        .decorated(false)
        .title("Screenshot viewer")
        .default_width(parent.width())
        .default_height(parent.height())
        .build();
    win.add_css_class("lightbox");

    // Image with generous margins so there's a dark border to click on
    let picture = gtk::Picture::builder()
        .content_fit(gtk::ContentFit::Contain)
        .halign(gtk::Align::Center)
        .valign(gtk::Align::Center)
        .margin_start(72)
        .margin_end(72)
        .margin_top(56)
        .margin_bottom(56)
        .alternative_text(&format!("Screenshot {} of {}", initial_index + 1, count))
        .build();
    picture.set_can_shrink(true);

    // Set initial texture
    {
        let t = textures.borrow();
        if let Some(Some(ref tex)) = t.get(initial_index) {
            picture.set_paintable(Some(tex));
        }
    }

    // Preload unloaded screenshots (for paged carousel that lazy-loads)
    if let (Some(paths), Some(name)) = (screenshot_paths, app_name) {
        let textures_preload = textures.clone();
        let picture_preload = picture.clone();
        let current_preload = current.clone();
        let paths = paths.clone();
        let name = name.to_string();
        for i in 0..count {
            let is_none = textures_preload.borrow().get(i).is_some_and(|t| t.is_none());
            if !is_none {
                continue;
            }
            let tex_ref = textures_preload.clone();
            let pic_ref = picture_preload.clone();
            let cur_ref = current_preload.clone();
            let path = paths[i].clone();
            let app = name.clone();
            let idx = i;
            glib::spawn_future_local(async move {
                let p = path.clone();
                let a = app.clone();
                let result = gio::spawn_blocking(move || {
                    catalog::cache_screenshot(&a, &p, idx)
                        .map_err(|e| e.to_string())
                }).await;
                if let Ok(Ok(local_path)) = result {
                    if let Ok(texture) = gtk::gdk::Texture::from_filename(&local_path) {
                        tex_ref.borrow_mut()[idx] = Some(texture.clone());
                        if cur_ref.get() == idx {
                            pic_ref.set_paintable(Some(&texture));
                        }
                    }
                }
            });
        }
    }

    // Navigation buttons
    let prev_btn = gtk::Button::builder()
        .icon_name("go-previous-symbolic")
        .css_classes(["circular", "osd", "lightbox-nav"])
        .valign(gtk::Align::Center)
        .halign(gtk::Align::Start)
        .margin_start(16)
        .tooltip_text("Previous screenshot")
        .build();
    prev_btn.update_property(&[gtk::accessible::Property::Label("Previous screenshot")]);

    let next_btn = gtk::Button::builder()
        .icon_name("go-next-symbolic")
        .css_classes(["circular", "osd", "lightbox-nav"])
        .valign(gtk::Align::Center)
        .halign(gtk::Align::End)
        .margin_end(16)
        .tooltip_text("Next screenshot")
        .build();
    next_btn.update_property(&[gtk::accessible::Property::Label("Next screenshot")]);

    // Counter label (e.g. "2 / 5") - Status role for live region announcements
    let counter = gtk::Label::builder()
        .label(&format!("{} / {}", initial_index + 1, count))
        .css_classes(["lightbox-counter"])
        .halign(gtk::Align::Center)
        .valign(gtk::Align::End)
        .margin_bottom(16)
        .accessible_role(gtk::AccessibleRole::Status)
        .build();

    // Close button (top-right)
    let close_btn = gtk::Button::builder()
        .icon_name("window-close-symbolic")
        .css_classes(["circular", "osd"])
        .halign(gtk::Align::End)
        .valign(gtk::Align::Start)
        .margin_top(16)
        .margin_end(16)
        .build();
    close_btn.update_property(&[gtk::accessible::Property::Label("Close lightbox")]);

    // Build overlay: picture as child, buttons + counter as overlays
    let overlay = gtk::Overlay::builder()
        .child(&picture)
        .hexpand(true)
        .vexpand(true)
        .build();

    overlay.add_overlay(&close_btn);
    if has_multiple {
        overlay.add_overlay(&prev_btn);
        overlay.add_overlay(&next_btn);
        overlay.add_overlay(&counter);
    }

    win.set_child(Some(&overlay));

    // --- Click outside image to close ---
    // Picture's gesture claims clicks on the image, preventing close.
    let pic_gesture = gtk::GestureClick::new();
    pic_gesture.connect_released(|gesture, _, _, _| {
        gesture.set_state(gtk::EventSequenceState::Claimed);
    });
    picture.add_controller(pic_gesture);

    // Window gesture fires for clicks on the dark margin area.
    {
        let win_ref = win.clone();
        let bg_gesture = gtk::GestureClick::new();
        bg_gesture.connect_released(move |_, _, _, _| {
            win_ref.close();
        });
        win.add_controller(bg_gesture);
    }

    // --- Close button ---
    {
        let win_ref = win.clone();
        close_btn.connect_clicked(move |_| {
            win_ref.close();
        });
    }

    // --- Navigation ---
    let make_navigate = |direction: i32| {
        let textures_ref = textures.clone();
        let current_ref = current.clone();
        let picture_ref = picture.clone();
        let counter_ref = counter.clone();
        move || {
            let idx = current_ref.get();
            let new_idx = if direction < 0 {
                if idx == 0 { count - 1 } else { idx - 1 }
            } else {
                if idx + 1 >= count { 0 } else { idx + 1 }
            };
            let t = textures_ref.borrow();
            if let Some(Some(ref tex)) = t.get(new_idx) {
                picture_ref.set_paintable(Some(tex));
                current_ref.set(new_idx);
                counter_ref.set_label(&format!("{} / {}", new_idx + 1, count));
            }
        }
    };

    if has_multiple {
        let go_prev = make_navigate(-1);
        prev_btn.connect_clicked(move |_| go_prev());

        let go_next = make_navigate(1);
        next_btn.connect_clicked(move |_| go_next());
    }

    // --- Keyboard: Escape to close, Left/Right to navigate ---
    {
        let win_ref = win.clone();
        let go_prev_key = make_navigate(-1);
        let go_next_key = make_navigate(1);
        let key_ctl = gtk::EventControllerKey::new();
        key_ctl.connect_key_pressed(move |_, key, _, _| {
            match key {
                gtk::gdk::Key::Escape => {
                    win_ref.close();
                    return glib::Propagation::Stop;
                }
                gtk::gdk::Key::Left if has_multiple => {
                    go_prev_key();
                    return glib::Propagation::Stop;
                }
                gtk::gdk::Key::Right if has_multiple => {
                    go_next_key();
                    return glib::Propagation::Stop;
                }
                _ => {}
            }
            glib::Propagation::Proceed
        });
        win.add_controller(key_ctl);
    }

    win.present();
}

/// Fetch a favicon for a URL and set it on an image widget.
fn fetch_favicon_async(url: &str, image: &gtk::Image) {
    // Extract domain from URL for favicon service
    let domain = url
        .trim_start_matches("https://")
        .trim_start_matches("http://")
        .split('/')
        .next()
        .unwrap_or("")
        .to_string();

    if domain.is_empty() {
        return;
    }

    let image_ref = image.clone();
    glib::spawn_future_local(async move {
        let favicon_url = format!(
            "https://www.google.com/s2/favicons?domain={}&sz=32",
            domain
        );
        let result = gio::spawn_blocking(move || {
            let mut response = ureq::get(&favicon_url)
                .header("User-Agent", "Driftwood-AppImage-Manager/0.1")
                .call()
                .ok()?;
            let mut buf = Vec::new();
            response.body_mut().as_reader().read_to_end(&mut buf).ok()?;
            if buf.len() > 100 {
                Some(buf)
            } else {
                None
            }
        })
        .await;

        if let Ok(Some(data)) = result {
            let gbytes = glib::Bytes::from(&data);
            if let Ok(texture) = gtk::gdk::Texture::from_bytes(&gbytes) {
                image_ref.set_paintable(Some(&texture));
            }
        }
    });
}

pub fn show_uninstall_dialog(
    toast_overlay: &adw::ToastOverlay,
    record: &AppImageRecord,
    db: &Rc<Database>,
    is_integrated: bool,
    data_paths: &[(String, String, u64)],
) {
    show_uninstall_dialog_with_callback(toast_overlay, record, db, is_integrated, data_paths, None);
}

pub fn show_uninstall_dialog_with_callback(
    toast_overlay: &adw::ToastOverlay,
    record: &AppImageRecord,
    db: &Rc<Database>,
    is_integrated: bool,
    data_paths: &[(String, String, u64)],
    on_complete: Option<Rc<dyn Fn() + 'static>>,
) {
    let name = record.app_name.as_deref().unwrap_or(&record.filename).to_string();
    let dialog = adw::AlertDialog::builder()
        .heading(&format!("Uninstall {}?", name))
        .body("Select what to remove:")
        .build();
    dialog.add_response("cancel", "Cancel");
    dialog.add_response("uninstall", "Uninstall");
    dialog.set_response_appearance("uninstall", adw::ResponseAppearance::Destructive);
    dialog.set_default_response(Some("cancel"));
    dialog.set_close_response("cancel");

    let extra = gtk::Box::builder()
        .orientation(gtk::Orientation::Vertical)
        .spacing(8)
        .margin_top(12)
        .build();

    // Checkbox: AppImage file (always checked, not unchecked - it's the main thing)
    let appimage_check = gtk::CheckButton::builder()
        .label(&format!("AppImage file ({})", widgets::format_size(record.size_bytes)))
        .active(true)
        .build();
    extra.append(&appimage_check);

    // Checkbox: Desktop integration
    let integration_check = if is_integrated {
        let check = gtk::CheckButton::builder()
            .label("Remove desktop integration")
            .active(true)
            .build();
        extra.append(&check);
        Some(check)
    } else {
        None
    };

    // Checkboxes for each discovered data path
    let mut path_checks: Vec<(gtk::CheckButton, String)> = Vec::new();
    for (path, label, size) in data_paths {
        let check = gtk::CheckButton::builder()
            .label(&format!("{} - {} ({})", label, path, widgets::format_size(*size as i64)))
            .active(true)
            .build();
        extra.append(&check);
        path_checks.push((check, path.clone()));
    }

    dialog.set_extra_child(Some(&extra));

    let record_snapshot = record.clone();
    let record_id = record.id;
    let record_path = record.path.clone();
    let db_ref = db.clone();
    let toast_ref = toast_overlay.clone();
    dialog.connect_response(Some("uninstall"), move |_dlg, _response| {
        // Capture deletion choices from checkboxes before dialog closes
        let delete_file = appimage_check.is_active();
        let should_remove_integration = integration_check.as_ref().map_or(false, |c| c.is_active());
        let paths_to_delete: Vec<String> = path_checks.iter()
            .filter(|(check, _)| check.is_active())
            .map(|(_, path)| path.clone())
            .collect();

        // Remove integration immediately (before DB delete, since CASCADE
        // removes system_modifications entries we need for undo_all_modifications)
        if should_remove_integration {
            integrator::undo_all_modifications(&db_ref, record_id).ok();
            if let Ok(Some(rec)) = db_ref.get_appimage_by_id(record_id) {
                integrator::remove_integration(&rec).ok();
            }
        }

        // Remove from database (so item vanishes from lists)
        db_ref.remove_appimage(record_id).ok();

        // Run the completion callback to refresh the library view
        if let Some(ref cb) = on_complete {
            cb();
        }

        // Navigate back (the detail view is now stale)
        if let Some(nav) = toast_ref.ancestor(adw::NavigationView::static_type()) {
            let nav: adw::NavigationView = nav.downcast().unwrap();
            nav.pop();
        }

        // Show undo toast - file deletion is deferred until toast dismisses
        let toast = adw::Toast::builder()
            .title(i18n_f("{name} uninstalled", &[("{name}", &name)]))
            .button_label(i18n("Undo"))
            .timeout(7)
            .build();

        let undo_clicked = Rc::new(Cell::new(false));

        // On Undo: restore record to DB, re-integrate if needed, refresh
        {
            let undo_flag = undo_clicked.clone();
            let db_undo = db_ref.clone();
            let snapshot = record_snapshot.clone();
            let toast_undo = toast_ref.clone();
            let on_complete_undo = on_complete.clone();
            let name_undo = name.clone();
            let was_integrated = should_remove_integration;
            toast.connect_button_clicked(move |_| {
                undo_flag.set(true);
                db_undo.restore_appimage_record(&snapshot).ok();
                // Re-integrate if it was previously integrated
                if was_integrated {
                    integrator::integrate(&snapshot).ok();
                }
                if let Some(ref cb) = on_complete_undo {
                    cb();
                }
                toast_undo.add_toast(widgets::info_toast(&i18n_f("{name} restored", &[("{name}", &name_undo)])));
            });
        }

        // On dismiss (timeout or any reason): perform actual file deletions if not undone
        {
            let undo_flag = undo_clicked.clone();
            let path = record_path.clone();
            toast.connect_dismissed(move |_| {
                if undo_flag.get() {
                    return; // Undo was clicked, nothing to delete
                }

                // Remove data paths
                for data_path in &paths_to_delete {
                    let p = std::path::Path::new(data_path);
                    if p.is_dir() {
                        std::fs::remove_dir_all(p).ok();
                    } else if p.is_file() {
                        std::fs::remove_file(p).ok();
                    }
                }

                // Remove AppImage file
                if delete_file {
                    std::fs::remove_file(&path).ok();
                }
            });
        }

        toast_ref.add_toast(toast);
    });

    dialog.present(Some(toast_overlay));
}

fn do_launch(
    btn: &gtk::Button,
    record_id: i64,
    path: &str,
    app_name: &str,
    launch_args: Vec<String>,
    db: &Rc<Database>,
    toast_overlay: &adw::ToastOverlay,
) {
    btn.set_sensitive(false);
    let btn_ref = btn.clone();
    let path = path.to_string();
    let app_name = app_name.to_string();
    let db_launch = db.clone();
    let toast_ref = toast_overlay.clone();
    glib::spawn_future_local(async move {
        let path_bg = path.clone();
        let result = gio::spawn_blocking(move || {
            let appimage_path = std::path::Path::new(&path_bg);
            launcher::launch_appimage(
                &Database::open().expect("DB open"),
                record_id,
                appimage_path,
                "gui_detail",
                &launch_args,
                &[],
            )
        }).await;

        btn_ref.set_sensitive(true);
        match result {
            Ok(launcher::LaunchResult::Started { pid, method }) => {
                log::info!("Launched: {} (PID: {}, method: {})", path, pid, method.as_str());

                let db_wayland = db_launch.clone();
                let path_clone = path.clone();
                glib::spawn_future_local(async move {
                    glib::timeout_future(std::time::Duration::from_secs(3)).await;
                    let analysis_result = gio::spawn_blocking(move || {
                        wayland::analyze_running_process(pid)
                    }).await;
                    if let Ok(Ok(analysis)) = analysis_result {
                        let status_str = analysis.as_status_str();
                        log::info!(
                            "Runtime Wayland: {} -> {} (pid={}, env: {:?})",
                            path_clone, analysis.status_label(), analysis.pid, analysis.env_vars,
                        );
                        db_wayland.update_runtime_wayland_status(record_id, status_str).ok();
                    }
                });
            }
            Ok(launcher::LaunchResult::Crashed { exit_code, stderr, method }) => {
                log::error!("App crashed on launch (exit {}, method: {}): {}", exit_code.unwrap_or(-1), method.as_str(), stderr);
                widgets::show_crash_dialog(&btn_ref, &app_name, exit_code, &stderr);
                if let Some(app) = gtk::gio::Application::default() {
                    notification::send_system_notification(
                        &app,
                        &format!("crash-{}", record_id),
                        &format!("{} crashed", app_name),
                        &stderr.chars().take(200).collect::<String>(),
                        gtk::gio::NotificationPriority::Urgent,
                    );
                }
            }
            Ok(launcher::LaunchResult::Failed(msg)) => {
                log::error!("Failed to launch: {}", msg);
                toast_ref.add_toast(widgets::error_toast(&i18n_f("Could not launch: {error}", &[("{error}", &msg)])));
            }
            Err(_) => {
                log::error!("Launch task panicked");
            }
        }
    });
}

