use adw::prelude::*;
use std::cell::RefCell;
use std::collections::HashSet;
use std::rc::Rc;

use gtk::gio;

use crate::core::catalog;
use crate::core::database::{CatalogApp, CatalogSortOrder, Database};
use crate::i18n::i18n;
use super::catalog_detail;
use super::catalog_tile;
use super::widgets;

/// Build the catalog page with an internal NavigationView for drill-down.
/// Returns (NavigationView, enrichment_banner) so the window can control the banner.
pub fn build_catalog_page(db: &Rc<Database>) -> (adw::NavigationView, gtk::Box) {
    let nav_view = adw::NavigationView::new();
    let (browse_page, enrichment_banner) = build_browse_page(db, &nav_view);
    nav_view.push(&browse_page);
    (nav_view, enrichment_banner)
}

/// Build the main browse page with featured carousel + tile grid.
/// Returns (NavigationPage, enrichment_banner).
fn build_browse_page(db: &Rc<Database>, nav_view: &adw::NavigationView) -> (adw::NavigationPage, gtk::Box) {
    let toast_overlay = adw::ToastOverlay::new();

    let header = adw::HeaderBar::new();
    let title = adw::WindowTitle::builder()
        .title(&i18n("App Catalog"))
        .subtitle(&i18n("Browse available AppImages"))
        .build();
    header.set_title_widget(Some(&title));

    // Search entry
    let search_entry = gtk::SearchEntry::builder()
        .placeholder_text(&i18n("Search apps..."))
        .hexpand(true)
        .build();
    search_entry.update_property(&[gtk::accessible::Property::Label("Search catalog apps")]);

    let search_bar = gtk::SearchBar::builder()
        .child(&search_entry)
        .search_mode_enabled(true)
        .build();

    // --- Featured section (AdwCarousel with swipe support) ---
    let featured_label = gtk::Label::builder()
        .label(&i18n("Featured"))
        .css_classes(["title-2"])
        .xalign(0.0)
        .halign(gtk::Align::Start)
        .margin_start(18)
        .build();

    let featured_carousel = adw::Carousel::builder()
        .hexpand(true)
        .allow_scroll_wheel(true)
        .allow_long_swipes(false)
        .build();

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

    // State: all featured apps (for later population)
    let featured_apps: Rc<RefCell<Vec<CatalogApp>>> = Rc::new(RefCell::new(Vec::new()));

    // Wrapping container for featured section
    let featured_section = gtk::Box::builder()
        .orientation(gtk::Orientation::Vertical)
        .spacing(8)
        .accessible_role(gtk::AccessibleRole::Region)
        .build();
    featured_section.update_property(&[gtk::accessible::Property::Label("Featured apps")]);
    featured_section.append(&featured_label);
    featured_section.append(&featured_carousel);
    featured_section.append(&carousel_dots);

    // --- Category filter tiles (wrapping grid) ---
    let category_box = gtk::FlowBox::builder()
        .selection_mode(gtk::SelectionMode::None)
        .homogeneous(true)
        .row_spacing(8)
        .column_spacing(8)
        .margin_start(18)
        .margin_end(18)
        .min_children_per_line(3)
        .max_children_per_line(20)
        .build();

    // --- "All Apps" section header with sort dropdown ---
    let all_label = gtk::Label::builder()
        .label(&i18n("All Apps"))
        .css_classes(["title-2"])
        .xalign(0.0)
        .halign(gtk::Align::Start)
        .hexpand(true)
        .build();

    let sort_options = [
        ("Popularity (most first)", CatalogSortOrder::PopularityDesc),
        ("Popularity (least first)", CatalogSortOrder::PopularityAsc),
        ("Name (A-Z)", CatalogSortOrder::NameAsc),
        ("Name (Z-A)", CatalogSortOrder::NameDesc),
        ("Release date (newest first)", CatalogSortOrder::ReleaseDateDesc),
        ("Release date (oldest first)", CatalogSortOrder::ReleaseDateAsc),
    ];

    let sort_model = gtk::StringList::new(
        &sort_options.iter().map(|(label, _)| *label).collect::<Vec<_>>(),
    );
    let sort_dropdown = gtk::DropDown::builder()
        .model(&sort_model)
        .selected(0)
        .valign(gtk::Align::Center)
        .tooltip_text(&i18n("Popularity is based on download count, GitHub stars, and community activity"))
        .build();
    sort_dropdown.add_css_class("flat");
    sort_dropdown.update_property(&[gtk::accessible::Property::Label("Sort catalog apps")]);

    let sort_icon = gtk::Image::builder()
        .icon_name("view-sort-descending-symbolic")
        .margin_end(4)
        .accessible_role(gtk::AccessibleRole::Presentation)
        .build();

    let sort_row = gtk::Box::builder()
        .orientation(gtk::Orientation::Horizontal)
        .spacing(8)
        .valign(gtk::Align::Center)
        .build();
    sort_row.append(&sort_icon);
    sort_row.append(&sort_dropdown);

    // View mode toggle (grid vs list)
    let view_toggle = gtk::ToggleButton::builder()
        .icon_name("view-list-symbolic")
        .tooltip_text(&i18n("Compact list view"))
        .valign(gtk::Align::Center)
        .css_classes(["flat", "circular"])
        .build();
    view_toggle.update_property(&[gtk::accessible::Property::Label("Switch to compact list view")]);
    widgets::set_pointer_cursor(&view_toggle);

    let compact_mode: Rc<std::cell::Cell<bool>> = Rc::new(std::cell::Cell::new(false));

    let all_header = gtk::Box::builder()
        .orientation(gtk::Orientation::Horizontal)
        .spacing(12)
        .margin_start(18)
        .margin_end(18)
        .build();
    all_header.append(&all_label);
    all_header.append(&sort_row);
    all_header.append(&view_toggle);

    // Sort state
    let active_sort: Rc<std::cell::Cell<CatalogSortOrder>> =
        Rc::new(std::cell::Cell::new(CatalogSortOrder::PopularityDesc));

    // Pagination state
    let current_page: Rc<std::cell::Cell<i32>> = Rc::new(std::cell::Cell::new(0));

    // FlowBox grid
    let flow_box = gtk::FlowBox::builder()
        .homogeneous(true)
        .min_children_per_line(2)
        .max_children_per_line(5)
        .selection_mode(gtk::SelectionMode::None)
        .margin_start(18)
        .margin_end(18)
        .row_spacing(12)
        .column_spacing(12)
        .build();

    // Pagination controls
    let page_prev_btn = gtk::Button::builder()
        .icon_name("go-previous-symbolic")
        .tooltip_text(&i18n("Previous page"))
        .sensitive(false)
        .css_classes(["flat", "circular"])
        .build();
    page_prev_btn.update_property(&[gtk::accessible::Property::Label("Previous page")]);
    let page_next_btn = gtk::Button::builder()
        .icon_name("go-next-symbolic")
        .tooltip_text(&i18n("Next page"))
        .sensitive(false)
        .css_classes(["flat", "circular"])
        .build();
    page_next_btn.update_property(&[gtk::accessible::Property::Label("Next page")]);
    let page_label = gtk::Label::builder()
        .label("Page 1")
        .css_classes(["dim-label"])
        .accessible_role(gtk::AccessibleRole::Status)
        .build();
    let page_bar = gtk::Box::builder()
        .orientation(gtk::Orientation::Horizontal)
        .halign(gtk::Align::Center)
        .spacing(12)
        .margin_bottom(24)
        .visible(false)
        .build();
    page_bar.append(&page_prev_btn);
    page_bar.append(&page_label);
    page_bar.append(&page_next_btn);

    let clamp = adw::Clamp::builder()
        .maximum_size(1200)
        .tightening_threshold(900)
        .build();

    let content = gtk::Box::builder()
        .orientation(gtk::Orientation::Vertical)
        .spacing(48)
        .build();

    // Enrichment banner (hidden by default, shown by background enrichment)
    let enrichment_banner = gtk::Box::builder()
        .orientation(gtk::Orientation::Horizontal)
        .spacing(8)
        .margin_start(18)
        .margin_end(18)
        .visible(false)
        .accessible_role(gtk::AccessibleRole::Status)
        .build();
    enrichment_banner.add_css_class("card");
    enrichment_banner.set_halign(gtk::Align::Fill);
    enrichment_banner.update_property(&[gtk::accessible::Property::Label("Loading app details from GitHub")]);

    let enrich_spinner = gtk::Spinner::builder()
        .spinning(true)
        .width_request(16)
        .height_request(16)
        .margin_start(12)
        .valign(gtk::Align::Center)
        .build();
    enrich_spinner.set_widget_name("enrich-spinner");
    enrich_spinner.update_property(&[gtk::accessible::Property::Label("Loading app details")]);
    enrichment_banner.append(&enrich_spinner);

    let enrich_label = gtk::Label::builder()
        .label(&i18n("Enriching app data from GitHub..."))
        .css_classes(["dim-label"])
        .hexpand(true)
        .xalign(0.0)
        .margin_top(8)
        .margin_bottom(8)
        .accessible_role(gtk::AccessibleRole::Status)
        .build();
    enrich_label.set_widget_name("enrich-label");
    enrichment_banner.append(&enrich_label);

    // Stale catalog banner - shown when catalog hasn't been refreshed in 7+ days
    let stale_banner = adw::Banner::builder()
        .title(&i18n("Catalog data may be outdated - tap to refresh"))
        .button_label(&i18n("Refresh"))
        .revealed(false)
        .accessible_role(gtk::AccessibleRole::Alert)
        .build();
    {
        let settings = gio::Settings::new(crate::config::APP_ID);
        let last_refreshed = settings.string("catalog-last-refreshed").to_string();
        if !last_refreshed.is_empty() {
            if let Ok(then) = chrono::DateTime::parse_from_rfc3339(&last_refreshed) {
                let days = (chrono::Utc::now() - then.with_timezone(&chrono::Utc)).num_days();
                if days >= 7 {
                    stale_banner.set_revealed(true);
                }
            }
        }
    }

    // Layout order: search (full-width) -> stale banner -> enrichment banner -> featured carousel -> categories -> all apps grid
    content.append(&stale_banner);
    content.append(&enrichment_banner);
    content.append(&featured_section);
    content.append(&category_box);
    content.append(&all_header);
    content.append(&flow_box);
    content.append(&page_bar);
    clamp.set_child(Some(&content));

    // Search bar sits outside the clamp so it stretches edge-to-edge
    let scroll_content = gtk::Box::builder()
        .orientation(gtk::Orientation::Vertical)
        .build();
    scroll_content.append(&search_bar);
    scroll_content.append(&clamp);

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

    // Empty state
    let empty_page = adw::StatusPage::builder()
        .icon_name("system-software-install-symbolic")
        .title(&i18n("App Catalog"))
        .description(&i18n("Refresh the catalog to browse available AppImages"))
        .build();

    let refresh_btn = gtk::Button::builder()
        .label(&i18n("Refresh Catalog"))
        .halign(gtk::Align::Center)
        .css_classes(["suggested-action", "pill"])
        .build();
    empty_page.set_child(Some(&refresh_btn));

    let stack = gtk::Stack::new();
    stack.add_named(&empty_page, Some("empty"));
    stack.add_named(&scrolled, Some("results"));

    let app_count = db.catalog_app_count().unwrap_or(0);
    if app_count > 0 {
        stack.set_visible_child_name("results");
        update_catalog_subtitle(&title, db);
    } else {
        stack.set_visible_child_name("empty");
    }

    toast_overlay.set_child(Some(&stack));

    // Progress bar for catalog sync
    let progress_bar = gtk::ProgressBar::builder()
        .show_text(true)
        .visible(false)
        .margin_start(18)
        .margin_end(18)
        .margin_top(6)
        .margin_bottom(6)
        .build();
    progress_bar.update_property(&[gtk::accessible::Property::Label("Catalog sync progress")]);

    let toolbar_content = gtk::Box::builder()
        .orientation(gtk::Orientation::Vertical)
        .build();
    toolbar_content.append(&progress_bar);
    toolbar_content.append(&toast_overlay);

    let toolbar_view = adw::ToolbarView::new();
    toolbar_view.add_top_bar(&header);
    toolbar_view.set_content(Some(&toolbar_content));

    // Refresh button in header
    let refresh_header_btn = widgets::accessible_icon_button(
        "view-refresh-symbolic",
        "Refresh catalog",
        &i18n("Refresh catalog"),
    );
    header.pack_end(&refresh_header_btn);

    // Category chips state
    let active_category: Rc<RefCell<Option<String>>> = Rc::new(RefCell::new(None));

    // Populate categories
    populate_categories(
        db, &category_box, &active_category, &active_sort, &current_page,
        &flow_box, &search_entry, &featured_section, &all_label,
        &page_bar, &page_label, &page_prev_btn, &page_next_btn, &scrolled,
        &compact_mode,
        &featured_apps, &featured_carousel, nav_view, &toast_overlay,
    );

    // Initial population
    populate_featured(
        db, &featured_apps, &featured_carousel, nav_view, &toast_overlay, None,
    );
    populate_grid(
        db, "", None, active_sort.get(), 0,
        &flow_box, &all_label, &page_bar, &page_label, &page_prev_btn, &page_next_btn, &scrolled,
        compact_mode.get(),
    );

    // View toggle handler
    {
        let db_ref = db.clone();
        let flow_ref = flow_box.clone();
        let cat_ref = active_category.clone();
        let sort_ref = active_sort.clone();
        let page_ref = current_page.clone();
        let search_ref = search_entry.clone();
        let compact_ref = compact_mode.clone();
        let all_label_ref = all_label.clone();
        let page_bar_ref = page_bar.clone();
        let page_label_ref = page_label.clone();
        let page_prev_ref = page_prev_btn.clone();
        let page_next_ref = page_next_btn.clone();
        let scrolled_ref = scrolled.clone();
        view_toggle.connect_toggled(move |btn| {
            let is_compact = btn.is_active();
            compact_ref.set(is_compact);
            if is_compact {
                btn.set_icon_name("view-grid-symbolic");
                btn.set_tooltip_text(Some(&i18n("Grid view")));
            } else {
                btn.set_icon_name("view-list-symbolic");
                btn.set_tooltip_text(Some(&i18n("Compact list view")));
            }
            let query = search_ref.text().to_string();
            let cat = cat_ref.borrow().clone();
            populate_grid(
                &db_ref, &query, cat.as_deref(), sort_ref.get(), page_ref.get(),
                &flow_ref, &all_label_ref, &page_bar_ref, &page_label_ref,
                &page_prev_ref, &page_next_ref, &scrolled_ref,
                is_compact,
            );
        });
    }

    // Sort dropdown handler
    {
        let db_ref = db.clone();
        let flow_ref = flow_box.clone();
        let cat_ref = active_category.clone();
        let sort_ref = active_sort.clone();
        let page_ref = current_page.clone();
        let search_ref = search_entry.clone();
        let compact_ref = compact_mode.clone();
        let all_label_ref = all_label.clone();
        let page_bar_ref = page_bar.clone();
        let page_label_ref = page_label.clone();
        let page_prev_ref = page_prev_btn.clone();
        let page_next_ref = page_next_btn.clone();
        let scrolled_ref = scrolled.clone();
        sort_dropdown.connect_selected_notify(move |dd| {
            let idx = dd.selected() as usize;
            let sort_options_local = [
                CatalogSortOrder::PopularityDesc,
                CatalogSortOrder::PopularityAsc,
                CatalogSortOrder::NameAsc,
                CatalogSortOrder::NameDesc,
                CatalogSortOrder::ReleaseDateDesc,
                CatalogSortOrder::ReleaseDateAsc,
            ];
            let sort = sort_options_local.get(idx).copied().unwrap_or(CatalogSortOrder::PopularityDesc);
            sort_ref.set(sort);
            page_ref.set(0);
            let query = search_ref.text().to_string();
            let cat = cat_ref.borrow().clone();
            populate_grid(
                &db_ref, &query, cat.as_deref(), sort, 0,
                &flow_ref, &all_label_ref, &page_bar_ref, &page_label_ref,
                &page_prev_ref, &page_next_ref, &scrolled_ref,
                compact_ref.get(),
            );
        });
    }

    // Search handler
    {
        let db_ref = db.clone();
        let flow_ref = flow_box.clone();
        let cat_ref = active_category.clone();
        let sort_ref = active_sort.clone();
        let page_ref = current_page.clone();
        let compact_ref = compact_mode.clone();
        let all_label_ref = all_label.clone();
        let featured_section_ref = featured_section.clone();
        let page_bar_ref = page_bar.clone();
        let page_label_ref = page_label.clone();
        let page_prev_ref = page_prev_btn.clone();
        let page_next_ref = page_next_btn.clone();
        let scrolled_ref = scrolled.clone();
        search_entry.connect_search_changed(move |entry| {
            let query = entry.text().to_string();
            let cat = cat_ref.borrow().clone();
            let is_searching = !query.is_empty() || cat.is_some();
            featured_section_ref.set_visible(!is_searching);
            page_ref.set(0);
            populate_grid(
                &db_ref, &query, cat.as_deref(), sort_ref.get(), 0,
                &flow_ref, &all_label_ref, &page_bar_ref, &page_label_ref,
                &page_prev_ref, &page_next_ref, &scrolled_ref,
                compact_ref.get(),
            );
        });
    }

    // Pagination: Previous page
    {
        let db_ref = db.clone();
        let flow_ref = flow_box.clone();
        let cat_ref = active_category.clone();
        let sort_ref = active_sort.clone();
        let page_ref = current_page.clone();
        let search_ref = search_entry.clone();
        let compact_ref = compact_mode.clone();
        let all_label_ref = all_label.clone();
        let page_bar_ref = page_bar.clone();
        let page_label_ref = page_label.clone();
        let page_prev_ref = page_prev_btn.clone();
        let page_next_ref = page_next_btn.clone();
        let scrolled_ref = scrolled.clone();
        page_prev_btn.connect_clicked(move |_| {
            let new_page = (page_ref.get() - 1).max(0);
            page_ref.set(new_page);
            let query = search_ref.text().to_string();
            let cat = cat_ref.borrow().clone();
            populate_grid(
                &db_ref, &query, cat.as_deref(), sort_ref.get(), new_page,
                &flow_ref, &all_label_ref, &page_bar_ref, &page_label_ref,
                &page_prev_ref, &page_next_ref, &scrolled_ref,
                compact_ref.get(),
            );
        });
    }

    // Pagination: Next page
    {
        let db_ref = db.clone();
        let flow_ref = flow_box.clone();
        let cat_ref = active_category.clone();
        let sort_ref = active_sort.clone();
        let page_ref = current_page.clone();
        let search_ref = search_entry.clone();
        let compact_ref = compact_mode.clone();
        let all_label_ref = all_label.clone();
        let page_bar_ref = page_bar.clone();
        let page_label_ref = page_label.clone();
        let page_prev_ref = page_prev_btn.clone();
        let page_next_ref = page_next_btn.clone();
        let scrolled_ref = scrolled.clone();
        page_next_btn.connect_clicked(move |_| {
            let new_page = page_ref.get() + 1;
            page_ref.set(new_page);
            let query = search_ref.text().to_string();
            let cat = cat_ref.borrow().clone();
            populate_grid(
                &db_ref, &query, cat.as_deref(), sort_ref.get(), new_page,
                &flow_ref, &all_label_ref, &page_bar_ref, &page_label_ref,
                &page_prev_ref, &page_next_ref, &scrolled_ref,
                compact_ref.get(),
            );
        });
    }

    // Tile click handler for grid
    {
        let db_ref = db.clone();
        let nav_ref = nav_view.clone();
        let toast_ref = toast_overlay.clone();
        flow_box.connect_child_activated(move |_, child| {
            if let Some(app_id) = child.widget_name().strip_prefix("catalog-app-") {
                if let Ok(id) = app_id.parse::<i64>() {
                    if let Ok(Some(app)) = db_ref.get_catalog_app(id) {
                        let detail = catalog_detail::build_catalog_detail_page(
                            &app, &db_ref, &toast_ref,
                        );
                        nav_ref.push(&detail);
                    }
                }
            }
        });
    }

    // Refresh handler
    let wire_refresh = |btn: &gtk::Button| {
        let db_ref = db.clone();
        let stack_ref = stack.clone();
        let flow_ref = flow_box.clone();
        let toast_ref = toast_overlay.clone();
        let title_ref = title.clone();
        let cat_box_ref = category_box.clone();
        let active_cat_ref = active_category.clone();
        let active_sort_ref = active_sort.clone();
        let page_ref = current_page.clone();
        let search_ref = search_entry.clone();
        let compact_ref = compact_mode.clone();
        let featured_apps_ref = featured_apps.clone();
        let featured_carousel_ref = featured_carousel.clone();
        let all_label_ref = all_label.clone();
        let featured_section_ref = featured_section.clone();
        let nav_ref = nav_view.clone();
        let progress_ref = progress_bar.clone();
        let page_bar_ref = page_bar.clone();
        let page_label_ref = page_label.clone();
        let page_prev_ref = page_prev_btn.clone();
        let page_next_ref = page_next_btn.clone();
        let scrolled_ref = scrolled.clone();

        btn.connect_clicked(move |btn| {
            btn.set_sensitive(false);
            let db_c = db_ref.clone();
            let stack_c = stack_ref.clone();
            let flow_c = flow_ref.clone();
            let toast_c = toast_ref.clone();
            let title_c = title_ref.clone();
            let btn_c = btn.clone();
            let cat_box_c = cat_box_ref.clone();
            let active_cat_c = active_cat_ref.clone();
            let active_sort_c = active_sort_ref.clone();
            let search_c = search_ref.clone();
            let featured_apps_c = featured_apps_ref.clone();
            let featured_carousel_c = featured_carousel_ref.clone();
            let all_label_c = all_label_ref.clone();
            let featured_section_c = featured_section_ref.clone();
            let nav_c = nav_ref.clone();
            let progress_c = progress_ref.clone();
            let page_bar_c = page_bar_ref.clone();
            let page_label_c = page_label_ref.clone();
            let page_prev_c = page_prev_ref.clone();
            let page_next_c = page_next_ref.clone();
            let scrolled_c = scrolled_ref.clone();
            let page_c = page_ref.clone();
            let compact_c = compact_ref.clone();

            // Capture app count before refresh for delta calculation
            let count_before = db_c.catalog_app_count().unwrap_or(0);

            // Show progress bar
            progress_c.set_visible(true);
            progress_c.set_fraction(0.0);
            progress_c.set_text(Some("Fetching catalog..."));

            // Show skeleton placeholder cards while syncing
            if count_before == 0 {
                show_skeleton(&flow_c);
            }

            // Channel for progress updates from background thread
            let (tx, rx) = std::sync::mpsc::channel::<catalog::SyncProgress>();

            // Listen for progress on the main thread
            // Track current source info for progress text
            let progress_listen = progress_c.clone();
            let current_source_name: Rc<RefCell<String>> = Rc::new(RefCell::new(String::new()));
            let current_source_base: Rc<std::cell::Cell<f64>> = Rc::new(std::cell::Cell::new(0.0));
            let current_source_span: Rc<std::cell::Cell<f64>> = Rc::new(std::cell::Cell::new(1.0));
            glib::timeout_add_local(std::time::Duration::from_millis(50), {
                let src_name = current_source_name.clone();
                let src_base = current_source_base.clone();
                let src_span = current_source_span.clone();
                move || {
                while let Ok(progress) = rx.try_recv() {
                    match progress {
                        catalog::SyncProgress::SourceStarted { ref name, source_index, source_count } => {
                            *src_name.borrow_mut() = name.clone();
                            // Divide progress bar evenly across sources
                            let span = 1.0 / source_count.max(1) as f64;
                            src_base.set(source_index as f64 * span);
                            src_span.set(span);
                            let frac = src_base.get();
                            progress_listen.set_fraction(frac);
                            progress_listen.set_text(Some(
                                &format!("Syncing {}...", name),
                            ));
                        }
                        catalog::SyncProgress::FetchingFeed => {
                            let name = src_name.borrow();
                            progress_listen.set_fraction(src_base.get());
                            progress_listen.set_text(Some(
                                &format!("{}: Fetching feed...", &*name),
                            ));
                        }
                        catalog::SyncProgress::FeedFetched { total } => {
                            let name = src_name.borrow();
                            progress_listen.set_fraction(src_base.get() + src_span.get() * 0.05);
                            progress_listen.set_text(Some(
                                &format!("{}: Found {} apps", &*name, total),
                            ));
                        }
                        catalog::SyncProgress::CachingIcon { current, total, ref app_name } => {
                            let name = src_name.borrow();
                            let inner = 0.05 + 0.85 * (current as f64 / total.max(1) as f64);
                            progress_listen.set_fraction(src_base.get() + src_span.get() * inner);
                            progress_listen.set_text(Some(
                                &format!("{}: Caching icon for {} ({}/{})", &*name, app_name, current, total),
                            ));
                        }
                        catalog::SyncProgress::SavingApps { current, total } => {
                            let name = src_name.borrow();
                            let inner = 0.90 + 0.10 * (current as f64 / total.max(1) as f64);
                            progress_listen.set_fraction(src_base.get() + src_span.get() * inner);
                            progress_listen.set_text(Some(
                                &format!("{}: Saving ({}/{})", &*name, current, total),
                            ));
                        }
                        catalog::SyncProgress::Done { total } => {
                            // Single source done - don't break, more sources may follow
                            let name = src_name.borrow();
                            progress_listen.set_fraction(src_base.get() + src_span.get());
                            progress_listen.set_text(Some(
                                &format!("{}: {} apps synced", &*name, total),
                            ));
                        }
                        catalog::SyncProgress::AllDone => {
                            progress_listen.set_fraction(1.0);
                            progress_listen.set_text(Some("Complete"));
                            return glib::ControlFlow::Break;
                        }
                    }
                }
                if !progress_listen.is_visible() {
                    return glib::ControlFlow::Break;
                }
                glib::ControlFlow::Continue
            }});

            glib::spawn_future_local(async move {
                let result = gio::spawn_blocking(move || {
                    let db_bg = Database::open().ok();
                    if let Some(ref db) = db_bg {
                        catalog::ensure_default_sources(db);
                        let sources = catalog::get_sources(db);
                        if sources.is_empty() {
                            return Err("No catalog sources configured".to_string());
                        }
                        // Sync all sources in order (OCS first as primary, then secondary)
                        let enabled_sources: Vec<_> = sources.iter()
                            .filter(|s| s.enabled)
                            .collect();
                        let source_count = enabled_sources.len() as u32;
                        let mut total_count = 0u32;
                        for (i, source) in enabled_sources.iter().enumerate() {
                            tx.send(catalog::SyncProgress::SourceStarted {
                                name: source.name.clone(),
                                source_index: i as u32,
                                source_count,
                            }).ok();
                            let tx_ref = tx.clone();
                            match catalog::sync_catalog_with_progress(db, source, &move |p| {
                                tx_ref.send(p).ok();
                            }) {
                                Ok(count) => total_count += count,
                                Err(e) => eprintln!("Failed to sync source '{}': {}", source.name, e),
                            }
                        }
                        tx.send(catalog::SyncProgress::AllDone).ok();
                        Ok(total_count)
                    } else {
                        Err("Failed to open database".to_string())
                    }
                }).await;

                btn_c.set_sensitive(true);
                progress_c.set_visible(false);

                match result {
                    Ok(Ok(_count)) => {
                        let count_after = db_c.catalog_app_count().unwrap_or(0);
                        let new_apps = count_after - count_before;
                        let toast_msg = if new_apps > 0 {
                            format!("Catalog refreshed: {} new apps added", new_apps)
                        } else {
                            "Catalog refreshed, no new apps".to_string()
                        };
                        toast_c.add_toast(widgets::info_toast(&toast_msg));
                        update_catalog_subtitle(&title_c, &db_c);
                        stack_c.set_visible_child_name("results");
                        populate_categories(
                            &db_c, &cat_box_c, &active_cat_c, &active_sort_c, &page_c,
                            &flow_c, &search_c, &featured_section_c, &all_label_c,
                            &page_bar_c, &page_label_c, &page_prev_c, &page_next_c, &scrolled_c,
                            &compact_c,
                            &featured_apps_c, &featured_carousel_c, &nav_c, &toast_c,
                        );
                        populate_featured(
                            &db_c, &featured_apps_c, &featured_carousel_c,
                            &nav_c, &toast_c, None,
                        );
                        page_c.set(0);
                        populate_grid(
                            &db_c, "", None, active_sort_c.get(), 0,
                            &flow_c, &all_label_c, &page_bar_c, &page_label_c,
                            &page_prev_c, &page_next_c, &scrolled_c,
                            compact_c.get(),
                        );

                        let settings = gio::Settings::new(crate::config::APP_ID);
                        let now = chrono::Utc::now()
                            .to_rfc3339_opts(chrono::SecondsFormat::Secs, true);
                        settings.set_string("catalog-last-refreshed", &now).ok();
                    }
                    Ok(Err(e)) => {
                        log::error!("Catalog refresh failed: {}", e);
                        toast_c.add_toast(widgets::error_toast(&i18n("Catalog refresh failed")));
                    }
                    Err(_) => {
                        log::error!("Catalog refresh thread panicked");
                        toast_c.add_toast(widgets::error_toast(&i18n("Catalog refresh failed")));
                    }
                }
            });
        });
    };

    wire_refresh(&refresh_btn);
    wire_refresh(&refresh_header_btn);

    // Stale banner button triggers a refresh
    {
        let refresh_btn_ref = refresh_header_btn.clone();
        let stale_ref = stale_banner.clone();
        stale_banner.connect_button_clicked(move |_| {
            stale_ref.set_revealed(false);
            refresh_btn_ref.emit_clicked();
        });
    }

    // Auto-refresh on first visit when catalog is empty
    if app_count == 0 {
        refresh_btn.emit_clicked();
    }

    widgets::apply_pointer_cursors(&toolbar_view);

    let page = adw::NavigationPage::builder()
        .title(&i18n("Catalog"))
        .tag("catalog-browse")
        .child(&toolbar_view)
        .build();

    (page, enrichment_banner)
}

fn update_catalog_subtitle(title: &adw::WindowTitle, db: &Database) {
    let app_count = db.catalog_app_count().unwrap_or(0);
    let sources = catalog::get_sources(db);
    // Use the most recent last_synced across all sources
    let last_synced = sources.iter()
        .filter_map(|s| s.last_synced.as_deref())
        .max();
    // Show per-source app counts in tooltip-friendly format
    let source_summary: String = sources.iter()
        .filter(|s| s.app_count > 0)
        .map(|s| format!("{}: {}", s.name, s.app_count))
        .collect::<Vec<_>>()
        .join(", ");
    if let Some(synced) = last_synced {
        let relative = widgets::relative_time(synced);
        title.set_subtitle(&format!("{} apps ({}) - Refreshed {}", app_count, source_summary, relative));
    } else {
        title.set_subtitle(&format!("{} apps available", app_count));
    }
}

const CARDS_PER_PAGE: usize = 3;

/// Populate featured apps into the AdwCarousel, one page of 3 cards each.
fn populate_featured(
    db: &Rc<Database>,
    featured_apps: &Rc<RefCell<Vec<CatalogApp>>>,
    carousel: &adw::Carousel,
    nav_view: &adw::NavigationView,
    toast_overlay: &adw::ToastOverlay,
    category: Option<&str>,
) {
    // Remove old pages
    while carousel.n_pages() > 0 {
        let child = carousel.nth_page(0);
        carousel.remove(&child);
    }

    let apps = match category {
        Some(cat) => db.get_featured_catalog_apps_by_category(30, cat).unwrap_or_default(),
        None => db.get_featured_catalog_apps(30).unwrap_or_default(),
    };
    if apps.is_empty() {
        *featured_apps.borrow_mut() = apps;
        return;
    }

    let total_pages = (apps.len() + CARDS_PER_PAGE - 1) / CARDS_PER_PAGE;

    for page_idx in 0..total_pages {
        let start = page_idx * CARDS_PER_PAGE;
        let end = (start + CARDS_PER_PAGE).min(apps.len());

        let page_box = gtk::Box::builder()
            .orientation(gtk::Orientation::Horizontal)
            .spacing(12)
            .homogeneous(true)
            .hexpand(true)
            .margin_start(18)
            .margin_end(18)
            .build();

        for app in &apps[start..end] {
            let tile = catalog_tile::build_featured_tile(app);

            // Load screenshot or icon fallback into the frame
            let first_screenshot = app.screenshots.as_deref()
                .filter(|s| !s.is_empty())
                .and_then(|s| s.split(';').find(|p| !p.is_empty()))
                .map(|s| s.to_string());
            let frame = tile.first_child()
                .and_then(|w| w.downcast::<gtk::Frame>().ok());
            if let Some(frame) = frame {
                if let Some(screenshot_path) = first_screenshot {
                    let app_name = app.name.clone();
                    let frame_ref = frame.clone();
                    glib::spawn_future_local(async move {
                        let name = app_name.clone();
                        let sp = screenshot_path.clone();
                        let result = gio::spawn_blocking(move || {
                            catalog::cache_screenshot(&name, &sp, 0)
                                .map_err(|e| e.to_string())
                        }).await;

                        match result {
                            Ok(Ok(local_path)) => {
                                if let Ok(texture) = gtk::gdk::Texture::from_filename(&local_path) {
                                    let picture = gtk::Picture::builder()
                                        .paintable(&texture)
                                        .content_fit(gtk::ContentFit::Cover)
                                        .halign(gtk::Align::Fill)
                                        .valign(gtk::Align::Fill)
                                        .build();
                                    picture.update_property(&[gtk::accessible::Property::Label(
                                        &format!("Screenshot of {}", app_name),
                                    )]);
                                    frame_ref.set_child(Some(&picture));
                                }
                            }
                            _ => {
                                let icon = widgets::app_icon(None, &app_name, 64);
                                icon.set_halign(gtk::Align::Center);
                                icon.set_valign(gtk::Align::Center);
                                frame_ref.set_child(Some(&icon));
                            }
                        }
                    });
                } else {
                    // No screenshots - show the app icon centered
                    let icon = widgets::app_icon(None, &app.name, 64);
                    icon.set_halign(gtk::Align::Center);
                    icon.set_valign(gtk::Align::Center);
                    frame.set_child(Some(&icon));
                }
            }

            // Click handler
            let db_ref = db.clone();
            let nav_ref = nav_view.clone();
            let toast_ref = toast_overlay.clone();
            let app_id = app.id;
            let click = gtk::GestureClick::new();
            click.connect_released(move |gesture, _, _, _| {
                gesture.set_state(gtk::EventSequenceState::Claimed);
                if let Ok(Some(catalog_app)) = db_ref.get_catalog_app(app_id) {
                    let detail = catalog_detail::build_catalog_detail_page(
                        &catalog_app, &db_ref, &toast_ref,
                    );
                    nav_ref.push(&detail);
                }
            });
            tile.add_controller(click);

            page_box.append(&tile);
        }

        carousel.append(&page_box);
    }

    *featured_apps.borrow_mut() = apps;
}

const PAGE_SIZE: i32 = 100;

/// Populate the main grid with catalog tiles.
fn populate_grid(
    db: &Rc<Database>,
    query: &str,
    category: Option<&str>,
    sort: CatalogSortOrder,
    page: i32,
    flow_box: &gtk::FlowBox,
    all_label: &gtk::Label,
    page_bar: &gtk::Box,
    page_label: &gtk::Label,
    page_prev: &gtk::Button,
    page_next: &gtk::Button,
    scrolled: &gtk::ScrolledWindow,
    compact: bool,
) {
    // Clear existing
    while let Some(child) = flow_box.first_child() {
        flow_box.remove(&child);
    }

    let total = db.count_catalog_matches(query, category).unwrap_or(0);
    let total_pages = ((total as f64) / (PAGE_SIZE as f64)).ceil() as i32;
    let page = page.clamp(0, (total_pages - 1).max(0));
    let offset = page * PAGE_SIZE;

    let results = db.search_catalog(query, category, PAGE_SIZE, offset, sort).unwrap_or_default();

    if results.is_empty() {
        all_label.set_label(&i18n("No results"));
        page_bar.set_visible(false);
        widgets::announce(flow_box.upcast_ref::<gtk::Widget>(), "No results found");
        return;
    }

    let start = offset + 1;
    let end = offset + results.len() as i32;
    let label_text = if query.is_empty() && category.is_none() {
        format!("{} ({}-{} of {})", i18n("All Apps"), start, end, total)
    } else {
        format!("{} ({}-{} of {})", i18n("Results"), start, end, total)
    };
    all_label.set_label(&label_text);

    // Announce result count to screen readers (only for active searches)
    if !query.is_empty() || category.is_some() {
        widgets::announce(flow_box.upcast_ref::<gtk::Widget>(), &format!("{} apps found", total));
    }

    // Build set of installed app names for badge display
    let installed_names: HashSet<String> = db.get_all_appimages()
        .unwrap_or_default()
        .into_iter()
        .filter_map(|r| r.app_name.map(|n| n.to_lowercase()))
        .collect();

    // Adjust grid layout for compact mode
    if compact {
        flow_box.set_min_children_per_line(1);
        flow_box.set_max_children_per_line(1);
    } else {
        flow_box.set_min_children_per_line(2);
        flow_box.set_max_children_per_line(5);
    }

    for app in &results {
        let is_installed = installed_names.contains(&app.name.to_lowercase());
        let tile = if compact {
            catalog_tile::build_catalog_row(app, is_installed)
        } else {
            catalog_tile::build_catalog_tile(app, is_installed)
        };
        tile.set_widget_name(&format!("catalog-app-{}", app.id));
        flow_box.append(&tile);
    }

    // Update pagination controls
    if total_pages > 1 {
        page_bar.set_visible(true);
        page_label.set_label(&format!("Page {} of {}", page + 1, total_pages));
        page_prev.set_sensitive(page > 0);
        page_next.set_sensitive(page < total_pages - 1);
    } else {
        page_bar.set_visible(false);
    }

    // Scroll to top when changing pages
    scrolled.vadjustment().set_value(0.0);
}

/// Show skeleton placeholder cards while the catalog is syncing.
fn show_skeleton(flow_box: &gtk::FlowBox) {
    while let Some(child) = flow_box.first_child() {
        flow_box.remove(&child);
    }
    for _ in 0..12 {
        let placeholder = gtk::Box::builder()
            .css_classes(["skeleton-card"])
            .build();
        let child = gtk::FlowBoxChild::builder()
            .child(&placeholder)
            .focusable(false)
            .build();
        flow_box.append(&child);
    }
}

/// Map a FreeDesktop category name to (icon_name, color_css_class).
fn category_meta(name: &str) -> (&'static str, &'static str) {
    match name.to_lowercase().as_str() {
        "audio" => ("audio-x-generic-symbolic", "cat-purple"),
        "development" => ("utilities-terminal-symbolic", "cat-blue"),
        "finance" => ("x-office-spreadsheet-symbolic", "cat-emerald"),
        "game" => ("input-gaming-symbolic", "cat-green"),
        "graphics" => ("image-x-generic-symbolic", "cat-orange"),
        "network" => ("network-workgroup-symbolic", "cat-teal"),
        "office" => ("x-office-document-symbolic", "cat-brown"),
        "science" => ("accessories-calculator-symbolic", "cat-lime"),
        "system" => ("computer-symbolic", "cat-slate"),
        "utility" => ("document-properties-symbolic", "cat-pink"),
        "video" => ("camera-video-symbolic", "cat-red"),
        _ => ("application-x-executable-symbolic", "cat-neutral"),
    }
}

/// Build a category tile toggle button with icon, label, and colored background.
fn build_category_chip(label_text: &str, icon_name: &str, color_class: &str, active: bool) -> gtk::ToggleButton {
    let icon = gtk::Image::from_icon_name(icon_name);
    icon.set_pixel_size(20);
    icon.set_accessible_role(gtk::AccessibleRole::Presentation);

    let label = gtk::Label::new(Some(label_text));

    let inner = gtk::Box::builder()
        .orientation(gtk::Orientation::Horizontal)
        .spacing(8)
        .build();
    inner.append(&icon);
    inner.append(&label);

    let btn = gtk::ToggleButton::builder()
        .child(&inner)
        .active(active)
        .css_classes(["category-tile", color_class])
        .build();
    btn.update_property(&[gtk::accessible::Property::Label(label_text)]);
    widgets::set_pointer_cursor(&btn);
    btn
}

fn populate_categories(
    db: &Rc<Database>,
    category_box: &gtk::FlowBox,
    active_category: &Rc<RefCell<Option<String>>>,
    active_sort: &Rc<std::cell::Cell<CatalogSortOrder>>,
    current_page: &Rc<std::cell::Cell<i32>>,
    flow_box: &gtk::FlowBox,
    search_entry: &gtk::SearchEntry,
    featured_section: &gtk::Box,
    all_label: &gtk::Label,
    page_bar: &gtk::Box,
    page_label: &gtk::Label,
    page_prev: &gtk::Button,
    page_next: &gtk::Button,
    scrolled: &gtk::ScrolledWindow,
    compact_mode: &Rc<std::cell::Cell<bool>>,
    featured_apps: &Rc<RefCell<Vec<CatalogApp>>>,
    featured_carousel: &adw::Carousel,
    nav_view: &adw::NavigationView,
    toast_overlay: &adw::ToastOverlay,
) {
    // Clear existing
    category_box.remove_all();

    let mut categories = db.get_catalog_categories().unwrap_or_default();
    if categories.is_empty() {
        return;
    }
    categories.sort_by(|a, b| a.0.to_lowercase().cmp(&b.0.to_lowercase()));

    let all_btn = build_category_chip(
        &i18n("All"), "view-grid-symbolic", "cat-accent", true,
    );
    category_box.append(&all_btn);

    let buttons: Rc<RefCell<Vec<gtk::ToggleButton>>> =
        Rc::new(RefCell::new(vec![all_btn.clone()]));

    for (cat, _count) in categories.iter() {
        let (icon_name, color_class) = category_meta(cat);
        let btn = build_category_chip(cat, icon_name, color_class, false);
        category_box.append(&btn);
        buttons.borrow_mut().push(btn.clone());

        let cat_str = cat.clone();
        let active_ref = active_category.clone();
        let sort_ref = active_sort.clone();
        let page_ref = current_page.clone();
        let flow_ref = flow_box.clone();
        let search_ref = search_entry.clone();
        let db_ref = db.clone();
        let buttons_ref = buttons.clone();
        let compact_ref = compact_mode.clone();
        let featured_section_ref = featured_section.clone();
        let all_label_ref = all_label.clone();
        let page_bar_ref = page_bar.clone();
        let page_label_ref = page_label.clone();
        let page_prev_ref = page_prev.clone();
        let page_next_ref = page_next.clone();
        let scrolled_ref = scrolled.clone();
        let featured_apps_ref = featured_apps.clone();
        let carousel_ref = featured_carousel.clone();
        let nav_ref = nav_view.clone();
        let toast_ref = toast_overlay.clone();
        btn.connect_toggled(move |btn| {
            if btn.is_active() {
                for other in buttons_ref.borrow().iter() {
                    if other != btn {
                        other.set_active(false);
                    }
                }
                *active_ref.borrow_mut() = Some(cat_str.clone());
                populate_featured(&db_ref, &featured_apps_ref, &carousel_ref, &nav_ref, &toast_ref, Some(&cat_str));
                featured_section_ref.set_visible(true);
                page_ref.set(0);
                let query = search_ref.text().to_string();
                populate_grid(
                    &db_ref, &query, Some(&cat_str), sort_ref.get(), 0,
                    &flow_ref, &all_label_ref, &page_bar_ref, &page_label_ref,
                    &page_prev_ref, &page_next_ref, &scrolled_ref,
                    compact_ref.get(),
                );
            }
        });
    }

    {
        let active_ref = active_category.clone();
        let sort_ref = active_sort.clone();
        let page_ref = current_page.clone();
        let flow_ref = flow_box.clone();
        let search_ref = search_entry.clone();
        let db_ref = db.clone();
        let buttons_ref = buttons.clone();
        let compact_ref = compact_mode.clone();
        let featured_section_ref = featured_section.clone();
        let all_label_ref = all_label.clone();
        let page_bar_ref = page_bar.clone();
        let page_label_ref = page_label.clone();
        let page_prev_ref = page_prev.clone();
        let page_next_ref = page_next.clone();
        let scrolled_ref = scrolled.clone();
        let featured_apps_ref = featured_apps.clone();
        let carousel_ref = featured_carousel.clone();
        let nav_ref = nav_view.clone();
        let toast_ref = toast_overlay.clone();
        all_btn.connect_toggled(move |btn| {
            if btn.is_active() {
                for other in buttons_ref.borrow().iter() {
                    if other != btn {
                        other.set_active(false);
                    }
                }
                *active_ref.borrow_mut() = None;
                populate_featured(&db_ref, &featured_apps_ref, &carousel_ref, &nav_ref, &toast_ref, None);
                featured_section_ref.set_visible(true);
                page_ref.set(0);
                let query = search_ref.text().to_string();
                populate_grid(
                    &db_ref, &query, None, sort_ref.get(), 0,
                    &flow_ref, &all_label_ref, &page_bar_ref, &page_label_ref,
                    &page_prev_ref, &page_next_ref, &scrolled_ref,
                    compact_ref.get(),
                );
            }
        });
    }
}
