use adw::prelude::*;
use gtk::accessible::Property as AccessibleProperty;
use std::cell::{Cell, RefCell};
use std::collections::HashSet;
use std::rc::Rc;

use crate::core::database::AppImageRecord;
use crate::i18n::{i18n, i18n_f, ni18n_f};
use super::app_card;
use super::widgets;

#[derive(Debug, Clone, Copy, PartialEq)]
pub enum ViewMode {
    Grid,
    List,
}

#[derive(Debug, Clone, Copy, PartialEq)]
pub enum SortMode {
    NameAsc,
    RecentlyAdded,
    Size,
}

#[derive(Debug, Clone, Copy, PartialEq)]
pub enum LibraryState {
    Loading,
    Empty,
    Populated,
    SearchEmpty,
}

pub struct LibraryView {
    pub page: adw::NavigationPage,
    pub header_bar: adw::HeaderBar,
    stack: gtk::Stack,
    flow_box: gtk::FlowBox,
    list_box: gtk::ListBox,
    search_bar: gtk::SearchBar,
    search_entry: gtk::SearchEntry,
    title_widget: adw::WindowTitle,
    view_mode: Rc<Cell<ViewMode>>,
    sort_mode: Rc<Cell<SortMode>>,
    grid_button: gtk::ToggleButton,
    list_button: gtk::ToggleButton,
    records: Rc<RefCell<Vec<AppImageRecord>>>,
    search_empty_page: adw::StatusPage,
    update_banner: adw::Banner,
    // Tag filtering
    tag_bar: gtk::Box,
    tag_scroll: gtk::ScrolledWindow,
    active_tag: Rc<RefCell<Option<String>>>,
    // Batch selection
    selection_mode: Rc<Cell<bool>>,
    selected_ids: Rc<RefCell<HashSet<i64>>>,
    _action_bar: gtk::ActionBar,
    select_button: gtk::ToggleButton,
    selection_label: gtk::Label,
}

impl LibraryView {
    pub fn new(menu: &gtk::gio::Menu) -> Self {
        let records: Rc<RefCell<Vec<AppImageRecord>>> = Rc::new(RefCell::new(Vec::new()));

        // Read initial view mode from GSettings
        let settings = gtk::gio::Settings::new(crate::config::APP_ID);
        let saved_view = settings.string("view-mode");
        let initial_mode = if saved_view.as_str() == "list" {
            ViewMode::List
        } else {
            ViewMode::Grid
        };
        let view_mode = Rc::new(Cell::new(initial_mode));

        // Sort mode from settings
        let saved_sort = settings.string("sort-mode");
        let initial_sort = match saved_sort.as_str() {
            "recently-added" => SortMode::RecentlyAdded,
            "size" => SortMode::Size,
            _ => SortMode::NameAsc,
        };
        let sort_mode = Rc::new(Cell::new(initial_sort));

        // --- Header bar ---
        let menu_button = gtk::MenuButton::builder()
            .icon_name("open-menu-symbolic")
            .menu_model(menu)
            .tooltip_text(&i18n("Menu"))
            .primary(true)
            .build();
        menu_button.add_css_class("flat");
        menu_button.update_property(&[AccessibleProperty::Label("Main menu")]);

        let search_button = gtk::ToggleButton::builder()
            .icon_name("system-search-symbolic")
            .tooltip_text(&i18n("Search (Ctrl+F)"))
            .build();
        search_button.add_css_class("flat");
        search_button.update_property(&[AccessibleProperty::Label("Toggle search")]);

        // Linked view toggle (segmented control)
        let grid_button = gtk::ToggleButton::builder()
            .icon_name("view-grid-symbolic")
            .tooltip_text(&i18n("Grid view"))
            .active(initial_mode == ViewMode::Grid)
            .build();
        grid_button.update_property(&[AccessibleProperty::Label("Switch to grid view")]);

        let list_button = gtk::ToggleButton::builder()
            .icon_name("view-list-symbolic")
            .tooltip_text(&i18n("List view"))
            .active(initial_mode == ViewMode::List)
            .group(&grid_button)
            .build();
        list_button.update_property(&[AccessibleProperty::Label("Switch to list view")]);

        let view_toggle_box = gtk::Box::builder()
            .orientation(gtk::Orientation::Horizontal)
            .build();
        view_toggle_box.add_css_class("linked");
        view_toggle_box.append(&grid_button);
        view_toggle_box.append(&list_button);

        let title_widget = adw::WindowTitle::builder()
            .title("Driftwood")
            .build();

        // Scan button
        let scan_button = gtk::Button::builder()
            .icon_name("view-refresh-symbolic")
            .tooltip_text(&i18n("Scan for AppImages (Ctrl+R)"))
            .build();
        scan_button.add_css_class("flat");
        scan_button.set_action_name(Some("win.scan"));
        scan_button.update_property(&[AccessibleProperty::Label("Scan for AppImages")]);

        // Sort dropdown
        let sort_menu = gtk::gio::Menu::new();
        sort_menu.append(Some(&i18n("Name A-Z")), Some("win.sort-library::name"));
        sort_menu.append(Some(&i18n("Recently Added")), Some("win.sort-library::recent"));
        sort_menu.append(Some(&i18n("Size")), Some("win.sort-library::size"));

        let sort_button = gtk::MenuButton::builder()
            .icon_name("view-sort-descending-symbolic")
            .menu_model(&sort_menu)
            .tooltip_text(&i18n("Sort"))
            .build();
        sort_button.add_css_class("flat");
        sort_button.update_property(&[AccessibleProperty::Label("Sort library")]);

        // Add button (shows drop overlay)
        let add_button_icon = gtk::Image::from_icon_name("list-add-symbolic");
        add_button_icon.set_accessible_role(gtk::AccessibleRole::Presentation);
        let add_button_label = gtk::Label::new(Some(&i18n("Add app")));
        let add_button_content = gtk::Box::builder()
            .orientation(gtk::Orientation::Horizontal)
            .spacing(6)
            .build();
        add_button_content.append(&add_button_icon);
        add_button_content.append(&add_button_label);

        let add_button = gtk::Button::builder()
            .child(&add_button_content)
            .tooltip_text(&i18n("Add AppImage"))
            .build();
        add_button.add_css_class("flat");
        add_button.set_action_name(Some("win.show-drop-hint"));
        add_button.update_property(&[AccessibleProperty::Label("Add AppImage")]);

        let select_button = gtk::ToggleButton::builder()
            .icon_name("selection-mode-symbolic")
            .tooltip_text(&i18n("Select multiple"))
            .build();
        select_button.add_css_class("flat");
        select_button.update_property(&[AccessibleProperty::Label("Toggle selection mode")]);

        let header_bar = adw::HeaderBar::builder()
            .title_widget(&title_widget)
            .build();
        header_bar.pack_start(&scan_button);
        header_bar.pack_start(&add_button);
        header_bar.pack_start(&select_button);
        header_bar.pack_end(&menu_button);
        header_bar.pack_end(&search_button);
        header_bar.pack_end(&sort_button);
        header_bar.pack_end(&view_toggle_box);

        // --- Search bar ---
        let search_entry = gtk::SearchEntry::builder()
            .placeholder_text(&i18n("Search AppImages..."))
            .hexpand(true)
            .build();
        search_entry.update_property(&[gtk::accessible::Property::Label("Search installed AppImages")]);

        let search_clamp = adw::Clamp::builder()
            .maximum_size(500)
            .child(&search_entry)
            .build();

        let search_bar = gtk::SearchBar::builder()
            .child(&search_clamp)
            .search_mode_enabled(false)
            .build();
        search_bar.connect_entry(&search_entry);
        search_bar.set_accessible_role(gtk::AccessibleRole::Search);

        // Bind search button to search bar
        search_button
            .bind_property("active", &search_bar, "search-mode-enabled")
            .bidirectional()
            .build();

        // --- Content stack ---
        let stack = gtk::Stack::builder()
            .transition_type(gtk::StackTransitionType::Crossfade)
            .vexpand(true)
            .build();

        // Loading state
        let loading_page = adw::StatusPage::builder()
            .title(&i18n("Scanning for AppImages..."))
            .build();
        let spinner = adw::Spinner::builder()
            .width_request(32)
            .height_request(32)
            .halign(gtk::Align::Center)
            .build();
        spinner.update_property(&[gtk::accessible::Property::Label("Scanning for AppImages")]);
        loading_page.set_child(Some(&spinner));
        stack.add_named(&loading_page, Some("loading"));

        // Empty state
        let empty_button_box = gtk::Box::builder()
            .orientation(gtk::Orientation::Horizontal)
            .halign(gtk::Align::Center)
            .spacing(12)
            .build();

        let scan_now_btn = gtk::Button::builder()
            .label(&i18n("Scan for AppImages"))
            .build();
        scan_now_btn.add_css_class("suggested-action");
        scan_now_btn.add_css_class("pill");
        scan_now_btn.update_property(&[AccessibleProperty::Label("Scan for AppImages")]);

        let browse_catalog_btn = gtk::Button::builder()
            .label(&i18n("Browse Catalog"))
            .build();
        browse_catalog_btn.add_css_class("flat");
        browse_catalog_btn.add_css_class("pill");
        browse_catalog_btn.set_action_name(Some("win.catalog"));
        browse_catalog_btn.update_property(&[AccessibleProperty::Label("Browse app catalog")]);

        let learn_btn = gtk::Button::builder()
            .label(&i18n("What is an AppImage?"))
            .build();
        learn_btn.add_css_class("flat");
        learn_btn.add_css_class("pill");
        learn_btn.connect_clicked(|btn| {
            let dialog = adw::AlertDialog::builder()
                .heading(&i18n("What is an AppImage?"))
                .body(&i18n(
                    "AppImages are self-contained app files for Linux, similar to .exe files on Windows or .dmg files on Mac.\n\n\
                     Key differences from traditional Linux packages:\n\
                     - No installation needed - just download and run\n\
                     - One file per app - easy to back up and share\n\
                     - Works on most Linux distributions\n\
                     - Does not require admin/root access\n\n\
                     Driftwood helps you discover, organize, and keep your AppImages up to date."
                ))
                .build();
            dialog.add_response("learn-more", &i18n("Learn More Online"));
            dialog.add_response("ok", &i18n("Got It"));
            dialog.set_default_response(Some("ok"));
            dialog.set_close_response("ok");
            dialog.connect_response(Some("learn-more"), |_, _| {
                gtk::UriLauncher::new("https://appimage.org")
                    .launch(gtk::Window::NONE, gtk::gio::Cancellable::NONE, |_| {});
            });
            dialog.present(Some(btn));
        });

        empty_button_box.append(&scan_now_btn);
        empty_button_box.append(&browse_catalog_btn);
        empty_button_box.append(&learn_btn);

        let empty_page = adw::StatusPage::builder()
            .icon_name("application-x-executable-symbolic")
            .title(&i18n("No AppImages Yet"))
            .description(&i18n(
                "AppImages are portable apps for Linux - like .exe files, but they run without installation. Drag one here, scan your system, or browse the catalog to get started.",
            ))
            .child(&empty_button_box)
            .build();
        stack.add_named(&empty_page, Some("empty"));

        // Search empty state
        let search_empty_page = adw::StatusPage::builder()
            .icon_name("system-search-symbolic")
            .title(&i18n("No Results"))
            .description(&i18n("No AppImages match your search. Try a different search term."))
            .build();
        stack.add_named(&search_empty_page, Some("search-empty"));

        // Grid view
        let flow_box = gtk::FlowBox::builder()
            .valign(gtk::Align::Start)
            .selection_mode(gtk::SelectionMode::None)
            .homogeneous(true)
            .min_children_per_line(2)
            .max_children_per_line(5)
            .row_spacing(12)
            .column_spacing(12)
            .margin_top(12)
            .margin_bottom(12)
            .margin_start(12)
            .margin_end(12)
            .build();
        flow_box.update_property(&[AccessibleProperty::Label("AppImage library grid")]);

        let grid_scroll = gtk::ScrolledWindow::builder()
            .child(&flow_box)
            .vexpand(true)
            .build();
        stack.add_named(&grid_scroll, Some("grid"));

        // List view
        let list_box = gtk::ListBox::builder()
            .selection_mode(gtk::SelectionMode::None)
            .build();
        list_box.add_css_class("boxed-list");
        list_box.add_css_class("rich-list");
        list_box.update_property(&[AccessibleProperty::Label("AppImage library list")]);

        let list_clamp = adw::Clamp::builder()
            .maximum_size(900)
            .child(&list_box)
            .margin_top(12)
            .margin_bottom(12)
            .margin_start(12)
            .margin_end(12)
            .build();

        let list_scroll = gtk::ScrolledWindow::builder()
            .child(&list_clamp)
            .vexpand(true)
            .build();
        stack.add_named(&list_scroll, Some("list"));

        // --- Batch selection state ---
        let selection_mode = Rc::new(Cell::new(false));
        let selected_ids: Rc<RefCell<HashSet<i64>>> = Rc::new(RefCell::new(HashSet::new()));

        // --- Bottom action bar (hidden until selection mode) ---
        let action_bar = gtk::ActionBar::new();
        action_bar.set_visible(false);

        let selection_label = gtk::Label::builder()
            .label("0 selected")
            .accessible_role(gtk::AccessibleRole::Status)
            .build();
        action_bar.set_center_widget(Some(&selection_label));

        let integrate_btn = gtk::Button::builder()
            .label(&i18n("Integrate"))
            .tooltip_text(&i18n("Add selected to app menu"))
            .build();
        integrate_btn.add_css_class("suggested-action");
        integrate_btn.set_action_name(Some("win.batch-integrate"));
        action_bar.pack_start(&integrate_btn);

        let delete_btn = gtk::Button::builder()
            .label(&i18n("Delete"))
            .tooltip_text(&i18n("Delete selected AppImages"))
            .build();
        delete_btn.add_css_class("destructive-action");
        delete_btn.set_action_name(Some("win.batch-delete"));
        action_bar.pack_end(&delete_btn);

        // --- Updates-available banner ---
        let update_banner = adw::Banner::builder()
            .title(&i18n("Updates available"))
            .button_label(&i18n("View Updates"))
            .revealed(false)
            .accessible_role(gtk::AccessibleRole::Alert)
            .build();
        update_banner.set_action_name(Some("win.show-updates"));

        // --- Tag filter bar ---
        let tag_bar = gtk::Box::builder()
            .orientation(gtk::Orientation::Horizontal)
            .spacing(6)
            .margin_start(12)
            .margin_end(12)
            .margin_top(6)
            .margin_bottom(2)
            .visible(false)
            .accessible_role(gtk::AccessibleRole::Navigation)
            .build();
        tag_bar.update_property(&[AccessibleProperty::Label("Tag filter")]);
        let active_tag: Rc<RefCell<Option<String>>> = Rc::new(RefCell::new(None));

        let tag_scroll = gtk::ScrolledWindow::builder()
            .child(&tag_bar)
            .hscrollbar_policy(gtk::PolicyType::Automatic)
            .vscrollbar_policy(gtk::PolicyType::Never)
            .visible(false)
            .build();

        // --- Assemble toolbar view ---
        let content_box = gtk::Box::builder()
            .orientation(gtk::Orientation::Vertical)
            .build();
        content_box.append(&update_banner);
        content_box.append(&search_bar);
        content_box.append(&tag_scroll);
        content_box.append(&stack);
        content_box.append(&action_bar);

        let toolbar_view = adw::ToolbarView::new();
        toolbar_view.add_top_bar(&header_bar);
        toolbar_view.set_content(Some(&content_box));
        widgets::apply_pointer_cursors(&toolbar_view);

        // Enable type-to-search: any keypress in the view opens the search bar
        search_bar.set_key_capture_widget(Some(&toolbar_view));

        let page = adw::NavigationPage::builder()
            .title("Driftwood")
            .tag("library")
            .child(&toolbar_view)
            .build();

        // --- Wire up view toggle (linked buttons) ---
        {
            let stack_ref = stack.clone();
            let view_mode_ref = view_mode.clone();
            let settings_ref = settings.clone();
            grid_button.connect_toggled(move |btn| {
                if btn.is_active() {
                    view_mode_ref.set(ViewMode::Grid);
                    stack_ref.set_visible_child_name("grid");
                    settings_ref.set_string("view-mode", "grid").ok();
                }
            });
        }
        {
            let stack_ref = stack.clone();
            let view_mode_ref = view_mode.clone();
            let settings_ref = settings;
            list_button.connect_toggled(move |btn| {
                if btn.is_active() {
                    view_mode_ref.set(ViewMode::List);
                    stack_ref.set_visible_child_name("list");
                    settings_ref.set_string("view-mode", "list").ok();
                }
            });
        }

        // --- Wire up search filtering (debounced at 150ms) ---
        {
            let flow_box_ref = flow_box.clone();
            let list_box_ref = list_box.clone();
            let records_ref = records.clone();
            let stack_ref = stack.clone();
            let view_mode_ref = view_mode.clone();
            let search_empty_ref = search_empty_page.clone();
            let debounce_source: Rc<Cell<Option<glib::SourceId>>> = Rc::new(Cell::new(None));

            search_entry.connect_search_changed(move |entry| {
                // Cancel any pending debounce timer
                if let Some(source_id) = debounce_source.take() {
                    source_id.remove();
                }

                let query = entry.text().to_string().to_lowercase();

                // Immediate clear when search is empty (no debounce needed)
                if query.is_empty() {
                    flow_box_ref.set_filter_func(|_| true);
                    let mut i = 0;
                    while let Some(row) = list_box_ref.row_at_index(i) {
                        row.set_visible(true);
                        i += 1;
                    }
                    if !records_ref.borrow().is_empty() {
                        let view_name = if view_mode_ref.get() == ViewMode::Grid {
                            "grid"
                        } else {
                            "list"
                        };
                        stack_ref.set_visible_child_name(view_name);
                    }
                    return;
                }

                // Debounce: schedule filter after 150ms of no typing
                let flow_box_d = flow_box_ref.clone();
                let list_box_d = list_box_ref.clone();
                let records_d = records_ref.clone();
                let stack_d = stack_ref.clone();
                let view_mode_d = view_mode_ref.clone();
                let search_empty_d = search_empty_ref.clone();

                let debounce_clear = debounce_source.clone();
                let source_id = glib::timeout_add_local_once(
                    std::time::Duration::from_millis(150),
                    move || {
                        // Clear the stored SourceId so nobody tries to remove a fired timer
                        debounce_clear.set(None);

                        let recs = records_d.borrow();
                        let match_flags: Vec<bool> = recs
                            .iter()
                            .map(|rec| {
                                let name = rec.app_name.as_deref().unwrap_or(&rec.filename).to_lowercase();
                                let path = rec.path.to_lowercase();
                                name.contains(&query) || path.contains(&query)
                            })
                            .collect();

                        let flags_clone = match_flags.clone();
                        flow_box_d.set_filter_func(move |child| {
                            let idx = child.index() as usize;
                            flags_clone.get(idx).copied().unwrap_or(false)
                        });

                        let mut visible_count = 0;
                        for (i, matches) in match_flags.iter().enumerate() {
                            if let Some(row) = list_box_d.row_at_index(i as i32) {
                                row.set_visible(*matches);
                            }
                            if *matches {
                                visible_count += 1;
                            }
                        }

                        if visible_count == 0 && !recs.is_empty() {
                            search_empty_d.set_description(Some(
                                &i18n_f("No AppImages match '{}'. Try a different search term.", &[("{}", &query)])
                            ));
                            stack_d.set_visible_child_name("search-empty");
                            widgets::announce(flow_box_d.upcast_ref::<gtk::Widget>(), "No results found");
                        } else {
                            let view_name = if view_mode_d.get() == ViewMode::Grid {
                                "grid"
                            } else {
                                "list"
                            };
                            stack_d.set_visible_child_name(view_name);
                            widgets::announce(flow_box_d.upcast_ref::<gtk::Widget>(), &format!("{} results", visible_count));
                        }
                    },
                );
                debounce_source.set(Some(source_id));
            });
        }

        // --- Wire up selection mode toggle ---
        {
            let selection_mode_ref = selection_mode.clone();
            let selected_ids_ref = selected_ids.clone();
            let action_bar_ref = action_bar.clone();
            let selection_label_ref = selection_label.clone();
            let flow_box_ref = flow_box.clone();
            let list_box_ref = list_box.clone();
            let stack_announce = stack.clone();
            select_button.connect_toggled(move |btn| {
                let active = btn.is_active();
                selection_mode_ref.set(active);
                action_bar_ref.set_visible(active);
                let msg = if active { "Selection mode enabled" } else { "Selection mode disabled" };
                widgets::announce(&stack_announce, msg);
                if !active {
                    selected_ids_ref.borrow_mut().clear();
                    selection_label_ref.set_label("0 selected");
                    // Clear all selection visuals
                    let mut i = 0;
                    while let Some(child) = flow_box_ref.child_at_index(i) {
                        child.remove_css_class("selected");
                        i += 1;
                    }
                    let mut i = 0;
                    while let Some(row) = list_box_ref.row_at_index(i) {
                        row.remove_css_class("selected");
                        i += 1;
                    }
                }
            });
        }

        // --- Wire up empty state buttons ---
        scan_now_btn.set_action_name(Some("win.scan"));

        Self {
            page,
            header_bar,
            stack,
            flow_box,
            list_box,
            search_bar,
            search_entry,
            title_widget,
            view_mode,
            sort_mode,
            grid_button,
            list_button,
            records,
            search_empty_page,
            update_banner,
            tag_bar,
            tag_scroll,
            active_tag,
            selection_mode,
            selected_ids,
            _action_bar: action_bar,
            select_button,
            selection_label,
        }
    }

    pub fn set_state(&self, state: LibraryState) {
        match state {
            LibraryState::Loading => {
                self.stack.set_visible_child_name("loading");
                widgets::announce(&self.stack, "Scanning for AppImages");
            }
            LibraryState::Empty => {
                self.stack.set_visible_child_name("empty");
                self.title_widget.set_subtitle("");
            }
            LibraryState::Populated => {
                let view_name = if self.view_mode.get() == ViewMode::Grid {
                    "grid"
                } else {
                    "list"
                };
                self.stack.set_visible_child_name(view_name);
            }
            LibraryState::SearchEmpty => {
                self.search_empty_page.set_title(&i18n("No Results"));
                self.stack.set_visible_child_name("search-empty");
            }
        }
    }

    pub fn populate(&self, new_records: Vec<AppImageRecord>) {
        // Clear existing
        while let Some(child) = self.flow_box.first_child() {
            self.flow_box.remove(&child);
        }
        while let Some(row) = self.list_box.row_at_index(0) {
            self.list_box.remove(&row);
        }

        // Reset active tag filter
        *self.active_tag.borrow_mut() = None;

        // Sort records based on current sort mode
        let mut new_records = new_records;
        self.sort_records(&mut new_records);

        // Collect all unique tags for the tag filter bar
        let mut all_tags = std::collections::BTreeSet::new();
        for record in &new_records {
            if let Some(ref tags) = record.tags {
                for tag in tags.split(',') {
                    let trimmed = tag.trim();
                    if !trimmed.is_empty() {
                        all_tags.insert(trimmed.to_string());
                    }
                }
            }
        }

        // Build tag filter chip bar
        self.build_tag_chips(&all_tags);

        // Build cards and list rows
        for record in &new_records {
            // Grid card
            let card = app_card::build_app_card(record);
            let card_menu = build_context_menu(record);
            attach_context_menu(&card, &card_menu, record.id);
            self.flow_box.append(&card);

            // List row
            let row = self.build_list_row(record);
            let row_menu = build_context_menu(record);
            attach_context_menu(&row, &row_menu, record.id);
            self.list_box.append(&row);
        }

        *self.records.borrow_mut() = new_records;
        let count = self.records.borrow().len();

        // Update subtitle with count using i18n plurals
        if count == 0 {
            self.title_widget.set_subtitle("");
            self.set_state(LibraryState::Empty);
        } else {
            let subtitle = ni18n_f(
                "{} AppImage",
                "{} AppImages",
                count as u32,
                &[("{}", &count.to_string())],
            );
            self.title_widget.set_subtitle(&subtitle);
            self.set_state(LibraryState::Populated);
        }
    }

    fn sort_records(&self, records: &mut Vec<AppImageRecord>) {
        match self.sort_mode.get() {
            SortMode::NameAsc => {
                records.sort_by(|a, b| {
                    let name_a = a.app_name.as_deref().unwrap_or(&a.filename).to_lowercase();
                    let name_b = b.app_name.as_deref().unwrap_or(&b.filename).to_lowercase();
                    name_a.cmp(&name_b)
                });
            }
            SortMode::RecentlyAdded => {
                records.sort_by(|a, b| b.first_seen.cmp(&a.first_seen));
            }
            SortMode::Size => {
                records.sort_by(|a, b| b.size_bytes.cmp(&a.size_bytes));
            }
        }
    }

    fn build_tag_chips(&self, all_tags: &std::collections::BTreeSet<String>) {
        // Clear existing chips
        while let Some(child) = self.tag_bar.first_child() {
            self.tag_bar.remove(&child);
        }

        if all_tags.is_empty() {
            self.tag_scroll.set_visible(false);
            self.tag_bar.set_visible(false);
            return;
        }

        self.tag_scroll.set_visible(true);
        self.tag_bar.set_visible(true);

        // "All" chip
        let all_chip = gtk::ToggleButton::builder()
            .label(&i18n("All"))
            .active(true)
            .css_classes(["pill"])
            .build();
        widgets::set_pointer_cursor(&all_chip);
        self.tag_bar.append(&all_chip);

        // Tag chips
        let mut chips: Vec<gtk::ToggleButton> = vec![all_chip.clone()];
        for tag in all_tags {
            let chip = gtk::ToggleButton::builder()
                .label(tag)
                .css_classes(["pill"])
                .group(&all_chip)
                .build();
            widgets::set_pointer_cursor(&chip);
            self.tag_bar.append(&chip);
            chips.push(chip);
        }

        // Connect "All" chip
        {
            let active_tag = self.active_tag.clone();
            let flow_box = self.flow_box.clone();
            let list_box = self.list_box.clone();
            let records = self.records.clone();
            all_chip.connect_toggled(move |btn| {
                if btn.is_active() {
                    *active_tag.borrow_mut() = None;
                    apply_tag_filter(&flow_box, &list_box, &records.borrow(), None);
                }
            });
        }

        // Connect each tag chip
        for chip in &chips[1..] {
            let tag_name = chip.label().map(|l| l.to_string()).unwrap_or_default();
            let active_tag = self.active_tag.clone();
            let flow_box = self.flow_box.clone();
            let list_box = self.list_box.clone();
            let records = self.records.clone();
            chip.connect_toggled(move |btn| {
                if btn.is_active() {
                    *active_tag.borrow_mut() = Some(tag_name.clone());
                    apply_tag_filter(&flow_box, &list_box, &records.borrow(), Some(&tag_name));
                }
            });
        }
    }

    fn build_list_row(&self, record: &AppImageRecord) -> adw::ActionRow {
        let name = record.app_name.as_deref().unwrap_or(&record.filename);

        // Structured two-line subtitle:
        // Line 1: Description snippet or file path (or "Analyzing..." if pending)
        // Line 2: Version + size
        let is_analyzing = record.app_name.is_none()
            && record.analysis_status.as_deref() != Some("complete");

        let line1 = if is_analyzing {
            i18n("Analyzing...")
        } else if let Some(ref desc) = record.description {
            if !desc.is_empty() {
                let snippet: String = desc.chars().take(60).collect();
                if snippet.len() < desc.len() {
                    format!("{}...", snippet.trim_end())
                } else {
                    snippet
                }
            } else {
                record.path.clone()
            }
        } else {
            record.path.clone()
        };

        let mut meta_parts = Vec::new();
        if let Some(ref ver) = record.app_version {
            meta_parts.push(ver.clone());
        }
        meta_parts.push(widgets::format_size(record.size_bytes));
        let line2 = meta_parts.join(" - ");

        let subtitle = format!("{}\n{}", line1, line2);

        let row = adw::ActionRow::builder()
            .title(name)
            .subtitle(&subtitle)
            .subtitle_lines(2)
            .activatable(true)
            .build();

        // Icon prefix with rounded clipping and letter fallback
        let icon = widgets::app_icon(
            record.icon_path.as_deref(),
            name,
            48,
        );
        icon.add_css_class("icon-rounded");
        row.add_prefix(&icon);

        // Quick launch button
        let launch_btn = widgets::accessible_icon_button(
            "media-playback-start-symbolic",
            &format!("Launch {}", name),
            &i18n("Launch"),
        );
        launch_btn.add_css_class("circular");
        launch_btn.set_action_name(Some("win.launch-appimage"));
        launch_btn.set_action_target_value(Some(&record.id.to_variant()));
        widgets::set_pointer_cursor(&launch_btn);
        row.add_suffix(&launch_btn);

        // Single most important badge as suffix (same priority as cards)
        if let Some(badge) = app_card::build_priority_badge(record) {
            badge.set_valign(gtk::Align::Center);
            row.add_suffix(&badge);
        }

        // Navigate arrow
        row.add_suffix(&widgets::accessible_suffix_icon("go-next-symbolic", &i18n("Open details")));

        row
    }

    /// Get the record ID at a given flow box index.
    pub fn record_at_grid_index(&self, index: usize) -> Option<i64> {
        self.records.borrow().get(index).map(|r| r.id)
    }

    /// Get the record ID at a given list box index.
    pub fn record_at_list_index(&self, index: i32) -> Option<i64> {
        self.records.borrow().get(index as usize).map(|r| r.id)
    }

    /// Connect a callback for when a grid card is activated.
    pub fn connect_grid_activated<F: Fn(i64) + 'static>(&self, f: F) {
        let records = self.records.clone();
        self.flow_box.connect_child_activated(move |_, child| {
            let idx = child.index() as usize;
            if let Some(record) = records.borrow().get(idx) {
                f(record.id);
            }
        });
    }

    /// Connect a callback for when a list row is activated.
    pub fn connect_list_activated<F: Fn(i64) + 'static>(&self, f: F) {
        let records = self.records.clone();
        self.list_box.connect_row_activated(move |_, row| {
            let idx = row.index() as usize;
            if let Some(record) = records.borrow().get(idx) {
                f(record.id);
            }
        });
    }

    pub fn current_view_mode(&self) -> ViewMode {
        self.view_mode.get()
    }

    pub fn toggle_search(&self) {
        let active = self.search_bar.is_search_mode();
        self.search_bar.set_search_mode(!active);
        if !active {
            self.search_entry.grab_focus();
        }
    }

    /// Get the set of currently selected record IDs.
    pub fn selected_ids(&self) -> HashSet<i64> {
        self.selected_ids.borrow().clone()
    }

    /// Toggle selection of a record ID (used by card click in selection mode).
    pub fn toggle_selection(&self, id: i64) {
        let mut ids = self.selected_ids.borrow_mut();
        let was_selected = ids.contains(&id);
        if was_selected {
            ids.remove(&id);
        } else {
            ids.insert(id);
        }
        let count = ids.len();
        self.selection_label.set_label(&ni18n_f(
            "{} selected", "{} selected", count as u32,
            &[("{}", &count.to_string())],
        ));

        // Update visual highlight on the toggled widget
        if let Some(idx) = self.records.borrow().iter().position(|r| r.id == id) {
            if let Some(child) = self.flow_box.child_at_index(idx as i32) {
                if was_selected {
                    child.remove_css_class("selected");
                } else {
                    child.add_css_class("selected");
                }
            }
            if let Some(row) = self.list_box.row_at_index(idx as i32) {
                if was_selected {
                    row.remove_css_class("selected");
                } else {
                    row.add_css_class("selected");
                }
            }
        }
    }

    /// Whether the library is in selection mode.
    pub fn in_selection_mode(&self) -> bool {
        self.selection_mode.get()
    }

    /// Exit selection mode.
    pub fn exit_selection_mode(&self) {
        self.select_button.set_active(false);
    }

    /// Set the sort mode and re-populate with current records.
    pub fn set_sort_mode(&self, mode: SortMode) {
        self.sort_mode.set(mode);
        let records = self.records.borrow().clone();
        if !records.is_empty() {
            self.populate(records);
        }
    }

    /// Update the "updates available" banner.
    pub fn set_update_count(&self, count: i64) {
        self.update_banner.set_revealed(count > 0);
        if count > 0 {
            let text = ni18n_f(
                "{} update available",
                "{} updates available",
                count as u32,
                &[("{}", &count.to_string())],
            );
            self.update_banner.set_title(&text);
        }
    }

    /// Programmatically set the view mode by toggling the linked buttons.
    pub fn set_view_mode(&self, mode: ViewMode) {
        match mode {
            ViewMode::Grid => self.grid_button.set_active(true),
            ViewMode::List => self.list_button.set_active(true),
        }
    }
}

/// Build the right-click context menu model for an AppImage.
fn build_context_menu(record: &AppImageRecord) -> gtk::gio::Menu {
    let menu = gtk::gio::Menu::new();

    // Section 1: Launch
    let section1 = gtk::gio::Menu::new();
    section1.append(Some("Open"), Some(&format!("win.launch-appimage(int64 {})", record.id)));
    menu.append_section(None, &section1);

    // Section 2: Actions
    let section2 = gtk::gio::Menu::new();
    section2.append(Some("Check for Updates"), Some(&format!("win.check-update(int64 {})", record.id)));
    section2.append(Some("Security check"), Some(&format!("win.scan-security(int64 {})", record.id)));
    menu.append_section(None, &section2);

    // Section 3: Integration + folder
    let section3 = gtk::gio::Menu::new();
    let integrate_label = if record.integrated { "Remove from launcher" } else { "Add to launcher" };
    section3.append(Some(integrate_label), Some(&format!("win.toggle-integration(int64 {})", record.id)));
    section3.append(Some("Show in file manager"), Some(&format!("win.open-folder(int64 {})", record.id)));
    menu.append_section(None, &section3);

    // Section 4: Clipboard
    let section4 = gtk::gio::Menu::new();
    section4.append(Some("Copy file location"), Some(&format!("win.copy-path(int64 {})", record.id)));
    menu.append_section(None, &section4);

    // Section 5: Destructive actions
    let section5 = gtk::gio::Menu::new();
    let uninstall_item = gtk::gio::MenuItem::new(None, Some(&format!("win.uninstall-appimage(int64 {})", record.id)));
    uninstall_item.set_attribute_value("custom", Some(&"uninstall".to_variant()));
    section5.append_item(&uninstall_item);
    menu.append_section(None, &section5);

    menu
}

/// Attach a right-click context menu to a widget.
fn attach_context_menu(widget: &impl gtk::prelude::IsA<gtk::Widget>, menu_model: &gtk::gio::Menu, record_id: i64) {
    let popover = gtk::PopoverMenu::from_model_full(menu_model, gtk::PopoverMenuFlags::NESTED);
    popover.set_parent(widget.as_ref());
    popover.set_has_arrow(false);

    // Add custom destructive-styled uninstall button
    let uninstall_btn = gtk::Button::builder()
        .label("Uninstall")
        .build();
    uninstall_btn.add_css_class("destructive-context-item");
    // Left-align the label to match other menu items
    if let Some(label) = uninstall_btn.child().and_then(|c| c.downcast::<gtk::Label>().ok()) {
        label.set_halign(gtk::Align::Start);
    }
    uninstall_btn.set_action_name(Some("win.uninstall-appimage"));
    uninstall_btn.set_action_target_value(Some(&record_id.to_variant()));
    let popover_ref = popover.clone();
    uninstall_btn.connect_clicked(move |_| {
        popover_ref.popdown();
    });
    popover.add_child(&uninstall_btn, "uninstall");

    // Unparent the popover when the widget is destroyed to avoid GTK warnings
    let popover_cleanup = popover.clone();
    widget.as_ref().connect_destroy(move |_| {
        popover_cleanup.unparent();
    });

    // Right-click
    let click = gtk::GestureClick::new();
    click.set_button(3);
    let popover_ref = popover.clone();
    click.connect_pressed(move |gesture, _, x, y| {
        gesture.set_state(gtk::EventSequenceState::Claimed);
        popover_ref.set_pointing_to(Some(&gtk::gdk::Rectangle::new(x as i32, y as i32, 1, 1)));
        popover_ref.popup();
    });
    widget.as_ref().add_controller(click);

    // Long press for touch
    let long_press = gtk::GestureLongPress::new();
    let popover_ref = popover;
    long_press.connect_pressed(move |gesture, x, y| {
        gesture.set_state(gtk::EventSequenceState::Claimed);
        popover_ref.set_pointing_to(Some(&gtk::gdk::Rectangle::new(x as i32, y as i32, 1, 1)));
        popover_ref.popup();
    });
    widget.as_ref().add_controller(long_press);
}

/// Apply tag filtering to both flow_box (grid) and list_box (list).
fn apply_tag_filter(
    flow_box: &gtk::FlowBox,
    list_box: &gtk::ListBox,
    records: &[AppImageRecord],
    tag: Option<&str>,
) {
    let match_flags: Vec<bool> = records
        .iter()
        .map(|rec| {
            match tag {
                None => true, // "All" - show everything
                Some(filter_tag) => {
                    rec.tags.as_ref().map_or(false, |tags| {
                        tags.split(',')
                            .any(|t| t.trim().eq_ignore_ascii_case(filter_tag))
                    })
                }
            }
        })
        .collect();

    // Filter grid view
    let flags_grid = match_flags.clone();
    flow_box.set_filter_func(move |child| {
        let idx = child.index() as usize;
        flags_grid.get(idx).copied().unwrap_or(false)
    });

    // Filter list view
    for (i, visible) in match_flags.iter().enumerate() {
        if let Some(row) = list_box.row_at_index(i as i32) {
            row.set_visible(*visible);
        }
    }

    // Announce filter result to screen readers
    let visible_count = match_flags.iter().filter(|&&m| m).count();
    let msg = match tag {
        None => format!("{} apps shown", visible_count),
        Some(t) => format!("Filtered by tag: {}, {} apps", t, visible_count),
    };
    widgets::announce(flow_box.upcast_ref::<gtk::Widget>(), &msg);
}
