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

use gtk::gio;

use crate::core::catalog;
use crate::core::database::{CatalogApp, Database};
use crate::core::fuse;
use crate::core::github_enrichment;
use crate::core::github_enrichment::AppImageAsset;
use crate::i18n::i18n;
use super::catalog_tile;
use super::detail_view;
use super::widgets;

use crate::config::APP_ID;

/// Build a catalog app detail page for the given CatalogApp.
/// Returns an adw::NavigationPage that can be pushed onto a NavigationView.
pub fn build_catalog_detail_page(
    app: &CatalogApp,
    db: &Rc<Database>,
    toast_overlay: &adw::ToastOverlay,
) -> adw::NavigationPage {
    let page_title = app.name.clone();

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

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

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

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

    // --- Header section: icon + name + author + buttons ---
    let header_box = gtk::Box::builder()
        .orientation(gtk::Orientation::Horizontal)
        .spacing(18)
        .build();

    let icon = widgets::app_icon(None, &app.name, 96);
    icon.add_css_class("icon-dropshadow");
    icon.set_valign(gtk::Align::Start);
    header_box.append(&icon);

    let info_box = gtk::Box::builder()
        .orientation(gtk::Orientation::Vertical)
        .spacing(6)
        .valign(gtk::Align::Center)
        .hexpand(true)
        .build();

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

    // Author (prefer OCS personid, then homepage URL domain)
    let author_text = app.ocs_personid.as_deref()
        .filter(|p| !p.is_empty())
        .map(|p| p.to_string())
        .or_else(|| app.homepage.as_deref().map(|h| extract_author(h)));
    if let Some(author) = author_text {
        let author_label = gtk::Label::builder()
            .label(&format!("by {}", author))
            .css_classes(["dim-label"])
            .xalign(0.0)
            .halign(gtk::Align::Start)
            .build();
        info_box.append(&author_label);
    }

    // Button row
    let button_box = gtk::Box::builder()
        .orientation(gtk::Orientation::Horizontal)
        .spacing(8)
        .margin_top(8)
        .build();

    // Check early if GitHub data needs loading (affects install button behavior)
    let has_github = app.github_owner.is_some() && app.github_repo.is_some();
    let needs_enrichment = has_github
        && (app.latest_version.is_none() || is_enrichment_stale(app.github_enriched_at.as_deref()));
    let awaiting_github = needs_enrichment && app.github_download_url.is_none();

    // Check if already installed (map name -> record id for launching)
    let installed_map: std::collections::HashMap<String, i64> = db
        .get_all_appimages()
        .unwrap_or_default()
        .iter()
        .filter_map(|r| r.app_name.as_ref().map(|n| (n.to_lowercase(), r.id)))
        .collect();
    let installed_record_id = installed_map.get(&app.name.to_lowercase()).copied();
    let is_installed = installed_record_id.is_some();

    let install_slot = gtk::Box::new(gtk::Orientation::Horizontal, 0);

    // Shared OCS download files, populated by async fetch
    let ocs_files_shared: Rc<RefCell<Vec<catalog::OcsDownloadFile>>> =
        Rc::new(RefCell::new(Vec::new()));

    // Whether we need to load OCS download files
    let has_ocs = app.ocs_id.is_some();
    let awaiting_ocs = has_ocs && !is_installed;

    if is_installed {
        // Show Launch button for installed apps
        if let Some(record_id) = installed_record_id {
            let launch_btn = gtk::Button::builder()
                .label(&i18n("Launch"))
                .css_classes(["suggested-action", "pill"])
                .build();
            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);
            button_box.append(&launch_btn);
        }
        let installed_badge = widgets::status_badge(&i18n("Installed"), "success");
        installed_badge.set_valign(gtk::Align::Center);
        button_box.append(&installed_badge);
    } else {
        button_box.append(&install_slot);

        if awaiting_github || awaiting_ocs {
            // Data not yet loaded - show disabled placeholder
            let placeholder = gtk::Button::builder()
                .label(&i18n("Loading..."))
                .css_classes(["suggested-action", "pill"])
                .sensitive(false)
                .build();
            install_slot.append(&placeholder);
        } else {
            populate_install_slot(
                &install_slot,
                &app.name,
                &app.download_url,
                app.github_download_url.as_deref(),
                app.github_release_assets.as_deref(),
                toast_overlay,
                db,
                app.homepage.as_deref(),
                app.ocs_id,
                &[],
            );
        }
    }

    // Homepage button
    if let Some(ref homepage) = app.homepage {
        let homepage_btn = gtk::Button::builder()
            .label(&i18n("Homepage"))
            .css_classes(["flat", "pill"])
            .build();
        let hp_clone = homepage.clone();
        homepage_btn.connect_clicked(move |btn| {
            let launcher = gtk::UriLauncher::new(&hp_clone);
            let root = btn.root().and_downcast::<gtk::Window>();
            launcher.launch(root.as_ref(), gio::Cancellable::NONE, |_| {});
        });
        button_box.append(&homepage_btn);
    }

    info_box.append(&button_box);

    // "What you'll get" info and compatibility check for install
    if !is_installed {
        let size_hint = app.ocs_downloadsize.filter(|&s| s > 0)
            .map(|s| format!(" ({})", widgets::format_size(s)))
            .unwrap_or_default();
        let install_info = gtk::Label::builder()
            .label(&format!(
                "Downloads to ~/Applications and adds to your app launcher{}",
                size_hint
            ))
            .css_classes(["caption", "dim-label"])
            .wrap(true)
            .xalign(0.0)
            .halign(gtk::Align::Start)
            .build();
        info_box.append(&install_info);

        // System compatibility check
        let fuse_info = fuse::detect_system_fuse();
        let (compat_text, compat_class) = if fuse_info.status.is_functional() {
            ("Works with your system", "success")
        } else {
            ("May need additional setup to run", "warning")
        };
        let compat_badge = widgets::status_badge(compat_text, compat_class);
        compat_badge.set_halign(gtk::Align::Start);
        compat_badge.set_margin_top(2);
        info_box.append(&compat_badge);
    }

    header_box.append(&info_box);
    content.append(&header_box);

    // --- Stat cards row (between header and screenshots) ---
    // Prefer OCS data where available, fall back to GitHub data
    let display_downloads = app.ocs_downloads.filter(|&d| d > 0)
        .or(app.github_downloads.filter(|&d| d > 0));
    let display_version = app.ocs_version.as_deref()
        .filter(|v| !v.is_empty())
        .or(app.latest_version.as_deref());
    let has_ocs = app.ocs_id.is_some();
    let has_stats = has_github || has_ocs;

    let stars_value_label = gtk::Label::builder()
        .label(app.github_stars.filter(|&s| s > 0).map(|s| format_count(s)).as_deref().unwrap_or("-"))
        .css_classes(["stat-value"])
        .xalign(0.0)
        .build();
    let version_value_label = gtk::Label::builder()
        .label(display_version.unwrap_or("-"))
        .css_classes(["stat-value"])
        .xalign(0.0)
        .ellipsize(gtk::pango::EllipsizeMode::End)
        .max_width_chars(14)
        .build();
    let downloads_value_label = gtk::Label::builder()
        .label(display_downloads.map(|d| format_count(d)).as_deref().unwrap_or("-"))
        .css_classes(["stat-value"])
        .xalign(0.0)
        .build();
    let released_value_label = gtk::Label::builder()
        .label(app.release_date.as_deref().map(|d| widgets::relative_time(d)).as_deref().unwrap_or("-"))
        .css_classes(["stat-value"])
        .xalign(0.0)
        .build();

    if has_stats {
        let stats_row = gtk::Box::builder()
            .orientation(gtk::Orientation::Horizontal)
            .spacing(10)
            .homogeneous(true)
            .build();

        // Show stars card if we have GitHub data, or score if we have OCS data
        if has_github {
            let stars_card = build_stat_card("starred-symbolic", &stars_value_label, &i18n("Stars"));
            stars_card.add_css_class("stat-stars");
            stats_row.append(&stars_card);
        } else if let Some(score) = app.ocs_score.filter(|&s| s > 0) {
            let score_label = gtk::Label::builder()
                .label(&format!("{}%", score))
                .css_classes(["stat-value"])
                .xalign(0.0)
                .build();
            let score_card = build_stat_card("starred-symbolic", &score_label, &i18n("Score"));
            score_card.add_css_class("stat-stars");
            stats_row.append(&score_card);
        }

        let version_card = build_stat_card("tag-symbolic", &version_value_label, &i18n("Latest"));
        version_card.add_css_class("stat-version");
        stats_row.append(&version_card);

        let downloads_card = build_stat_card("folder-download-symbolic", &downloads_value_label, &i18n("Downloads"));
        downloads_card.add_css_class("stat-downloads");
        stats_row.append(&downloads_card);

        if has_github {
            let released_card = build_stat_card("month-symbolic", &released_value_label, &i18n("Released"));
            released_card.add_css_class("stat-released");
            stats_row.append(&released_card);
        } else if let Some(ref changed) = app.ocs_changed {
            if !changed.is_empty() {
                let changed_label = gtk::Label::builder()
                    .label(&widgets::relative_time(changed))
                    .css_classes(["stat-value"])
                    .xalign(0.0)
                    .build();
                let changed_card = build_stat_card("month-symbolic", &changed_label, &i18n("Updated"));
                changed_card.add_css_class("stat-released");
                stats_row.append(&changed_card);
            }
        }

        content.append(&stats_row);
    }

    // Enrichment spinner (small, shown next to stats row while loading)
    let enrich_spinner = gtk::Spinner::builder()
        .spinning(false)
        .visible(false)
        .halign(gtk::Align::Start)
        .width_request(16)
        .height_request(16)
        .build();
    enrich_spinner.update_property(&[gtk::accessible::Property::Label("Loading additional details")]);
    if has_github && needs_enrichment {
        content.append(&enrich_spinner);
    }

    // --- About section container (created early so enrichment callback can update it) ---
    let about_container = gtk::Box::builder()
        .orientation(gtk::Orientation::Vertical)
        .spacing(0)
        .build();
    {
        // Prefer long description for detail page (ocs_description is full HTML with
        // features, changelog etc.; ocs_summary and description are short one-liners)
        let display_desc = app.ocs_description.as_deref()
            .filter(|d| !d.is_empty())
            .or(app.github_description.as_deref().filter(|d| !d.is_empty()))
            .or(app.description.as_deref().filter(|d| !d.is_empty()));
        if let Some(desc) = display_desc {
            let about_group = adw::PreferencesGroup::builder()
                .title(&i18n("About"))
                .build();
            let plain_desc = catalog_tile::html_to_description(desc);
            let desc_label = gtk::Label::builder()
                .label(&plain_desc)
                .wrap(true)
                .xalign(0.0)
                .selectable(true)
                .margin_top(8)
                .margin_bottom(8)
                .margin_start(12)
                .margin_end(12)
                .build();
            let row = adw::ActionRow::new();
            row.set_child(Some(&desc_label));
            about_group.add(&row);
            about_container.append(&about_group);
        }
    }

    // --- README section container (created early so enrichment callback can update it) ---
    let readme_container = gtk::Box::builder()
        .orientation(gtk::Orientation::Vertical)
        .spacing(8)
        .build();
    if let Some(ref readme) = app.github_readme {
        if !readme.is_empty() {
            let readme_label = gtk::Label::builder()
                .label(&i18n("README"))
                .css_classes(["title-2"])
                .xalign(0.0)
                .halign(gtk::Align::Start)
                .build();
            readme_container.append(&readme_label);
            let rendered = widgets::build_markdown_view(readme);
            readme_container.append(&rendered);
        }
    }

    // On-demand enrichment: fetch release info if stale or missing
    if has_github && needs_enrichment {
        let app_id = app.id;
        let owner = app.github_owner.clone().unwrap_or_default();
        let repo = app.github_repo.clone().unwrap_or_default();
        let db_ref = db.clone();
        let stars_ref = stars_value_label.clone();
        let version_ref = version_value_label.clone();
        let downloads_ref = downloads_value_label.clone();
        let released_ref = released_value_label.clone();
        let spinner_ref = enrich_spinner.clone();
        let install_slot_ref = install_slot.clone();
        let toast_ref = toast_overlay.clone();
        let about_ref = about_container.clone();
        let readme_ref = readme_container.clone();
        let ocs_files_ref = ocs_files_shared.clone();

        spinner_ref.set_visible(true);
        spinner_ref.set_spinning(true);

        let settings = gio::Settings::new(APP_ID);
        let token = settings.string("github-token").to_string();

        glib::spawn_future_local(async move {
            let owner_c = owner.clone();
            let repo_c = repo.clone();
            let token_c = token.clone();

            let result = gio::spawn_blocking(move || {
                let bg_db = Database::open().ok();
                if let Some(ref db) = bg_db {
                    let _ = github_enrichment::enrich_app_release_info(
                        db, app_id, &owner_c, &repo_c, &token_c,
                    );
                    let _ = github_enrichment::enrich_app_repo_info(
                        db, app_id, &owner_c, &repo_c, &token_c,
                    );
                    let _ = github_enrichment::enrich_app_readme(
                        db, app_id, &owner_c, &repo_c, &token_c,
                    );
                    let _ = github_enrichment::enrich_app_release_history(
                        db, app_id, &owner_c, &repo_c, &token_c,
                    );
                }
            }).await;

            spinner_ref.set_spinning(false);
            spinner_ref.set_visible(false);
            widgets::announce(spinner_ref.upcast_ref::<gtk::Widget>(), "App details loaded");

            if let Ok(Some(updated)) = db_ref.get_catalog_app(app_id) {
                if let Some(stars) = updated.github_stars.filter(|&s| s > 0) {
                    stars_ref.set_label(&format_count(stars));
                }
                if let Some(ref ver) = updated.latest_version {
                    version_ref.set_label(ver);
                }
                if let Some(downloads) = updated.github_downloads.filter(|&d| d > 0) {
                    downloads_ref.set_label(&format_count(downloads));
                }
                if let Some(ref date) = updated.release_date {
                    released_ref.set_label(&widgets::relative_time(date));
                }

                // Rebuild install button now that GitHub data is available
                if awaiting_github && !is_installed {
                    let files = ocs_files_ref.borrow();
                    populate_install_slot(
                        &install_slot_ref,
                        &updated.name,
                        &updated.download_url,
                        updated.github_download_url.as_deref(),
                        updated.github_release_assets.as_deref(),
                        &toast_ref,
                        &db_ref,
                        updated.homepage.as_deref(),
                        updated.ocs_id,
                        &files,
                    );
                }

                // Update about description if GitHub description is now available
                if let Some(ref desc) = updated.github_description {
                    if !desc.is_empty() && about_ref.first_child().is_none() {
                        let about_group = adw::PreferencesGroup::builder()
                            .title(&i18n("About"))
                            .build();
                        let desc_label = gtk::Label::builder()
                            .label(desc)
                            .wrap(true)
                            .xalign(0.0)
                            .selectable(true)
                            .margin_top(8)
                            .margin_bottom(8)
                            .margin_start(12)
                            .margin_end(12)
                            .build();
                        let row = adw::ActionRow::new();
                        row.set_child(Some(&desc_label));
                        about_group.add(&row);
                        about_ref.append(&about_group);
                    }
                }

                // Populate README if now available
                if let Some(ref readme) = updated.github_readme {
                    if !readme.is_empty() && readme_ref.first_child().is_none() {
                        let readme_heading = gtk::Label::builder()
                            .label(&i18n("README"))
                            .css_classes(["title-2"])
                            .xalign(0.0)
                            .halign(gtk::Align::Start)
                            .build();
                        readme_ref.append(&readme_heading);
                        let rendered = widgets::build_markdown_view(readme);
                        readme_ref.append(&rendered);
                    }
                }
            }

            if result.is_err() {
                log::warn!("On-demand enrichment thread error");
            }
        });
    }

    // On-demand OCS download files fetch: get all available versions for the install dropdown
    if awaiting_ocs {
        let ocs_id_val = app.ocs_id.unwrap();
        let install_slot_ocs = install_slot.clone();
        let toast_ocs = toast_overlay.clone();
        let db_ocs = db.clone();
        let app_name_ocs = app.name.clone();
        let dl_url_ocs = app.download_url.clone();
        let gh_dl_url = app.github_download_url.clone();
        let gh_assets = app.github_release_assets.clone();
        let hp_ocs = app.homepage.clone();
        let ocs_id_opt = app.ocs_id;
        let ocs_files_ocs = ocs_files_shared.clone();

        glib::spawn_future_local(async move {
            let id = ocs_id_val;
            let result = gio::spawn_blocking(move || {
                catalog::fetch_ocs_download_files(id)
            }).await;

            match result {
                Ok(Ok(files)) => {
                    *ocs_files_ocs.borrow_mut() = files.clone();
                    populate_install_slot(
                        &install_slot_ocs,
                        &app_name_ocs,
                        &dl_url_ocs,
                        gh_dl_url.as_deref(),
                        gh_assets.as_deref(),
                        &toast_ocs,
                        &db_ocs,
                        hp_ocs.as_deref(),
                        ocs_id_opt,
                        &files,
                    );
                }
                _ => {
                    // Fetch failed - populate with basic button (no OCS file data)
                    populate_install_slot(
                        &install_slot_ocs,
                        &app_name_ocs,
                        &dl_url_ocs,
                        gh_dl_url.as_deref(),
                        gh_assets.as_deref(),
                        &toast_ocs,
                        &db_ocs,
                        hp_ocs.as_deref(),
                        ocs_id_opt,
                        &[],
                    );
                }
            }
        });
    }

    // --- Screenshots section (paged carousel with arrows, click for lightbox) ---
    // Only use real screenshots (not the OCS preview URL which is typically just an icon)
    let screenshots_source = app.screenshots.as_deref()
        .filter(|s| !s.is_empty());
    if let Some(screenshots_str) = screenshots_source {
        let paths: Vec<String> = screenshots_str.split(';')
            .filter(|s| !s.is_empty())
            .map(|s| s.to_string())
            .collect();
        if !paths.is_empty() {
            let screenshots_label = gtk::Label::builder()
                .label(&i18n("Screenshots"))
                .css_classes(["title-2"])
                .xalign(0.0)
                .halign(gtk::Align::Start)
                .margin_top(12)
                .build();
            content.append(&screenshots_label);

            const SCREENSHOTS_PER_PAGE: usize = 2;
            let total_screenshots = paths.len();
            let max_page = total_screenshots.saturating_sub(1) / SCREENSHOTS_PER_PAGE;

            // Store all textures for lightbox (across all pages)
            let textures: Rc<RefCell<Vec<Option<gtk::gdk::Texture>>>> =
                Rc::new(RefCell::new(vec![None; total_screenshots]));

            // Store all frame refs so we can show/hide pages
            let all_paths: Rc<Vec<String>> = Rc::new(paths);
            let screenshot_page: Rc<std::cell::Cell<usize>> = Rc::new(std::cell::Cell::new(0));

            // Stack for crossfade page transitions
            let ss_stack = gtk::Stack::builder()
                .transition_type(gtk::StackTransitionType::Crossfade)
                .transition_duration(200)
                .hexpand(true)
                .height_request(340)
                .build();

            // Navigation arrows
            let ss_left = gtk::Button::builder()
                .icon_name("go-previous-symbolic")
                .css_classes(["circular", "osd"])
                .valign(gtk::Align::Center)
                .sensitive(false)
                .tooltip_text(&i18n("Previous screenshot"))
                .build();
            ss_left.update_property(&[gtk::accessible::Property::Label("Previous screenshot")]);

            let ss_right = gtk::Button::builder()
                .icon_name("go-next-symbolic")
                .css_classes(["circular", "osd"])
                .valign(gtk::Align::Center)
                .sensitive(max_page > 0)
                .tooltip_text(&i18n("Next screenshot"))
                .build();
            ss_right.update_property(&[gtk::accessible::Property::Label("Next screenshot")]);

            ss_left.set_visible(max_page > 0);
            ss_right.set_visible(max_page > 0);

            // Carousel row: [<] [stack] [>]
            let ss_row = gtk::Box::builder()
                .orientation(gtk::Orientation::Horizontal)
                .spacing(8)
                .build();
            ss_row.append(&ss_left);
            ss_row.append(&ss_stack);
            ss_row.append(&ss_right);

            // Flip state for crossfade (alternates between "a" and "b" stack children)
            let ss_flip: Rc<std::cell::Cell<bool>> = Rc::new(std::cell::Cell::new(false));

            // Build and show a page of screenshots
            let build_ss_page = {
                let textures_ref = textures.clone();
                let paths_ref = all_paths.clone();
                let app_name = app.name.clone();
                Rc::new(move |page: usize, stack: &gtk::Stack, flip: &Rc<std::cell::Cell<bool>>,
                              left: &gtk::Button, right: &gtk::Button| {
                    let start = page * SCREENSHOTS_PER_PAGE;
                    let end = (start + SCREENSHOTS_PER_PAGE).min(paths_ref.len());

                    left.set_sensitive(page > 0);
                    right.set_sensitive(page < max_page);

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

                    for i in start..end {
                        let frame = gtk::Frame::new(None);
                        frame.add_css_class("card");
                        frame.set_height_request(320);

                        let spinner = gtk::Spinner::builder()
                            .halign(gtk::Align::Center)
                            .valign(gtk::Align::Center)
                            .spinning(true)
                            .width_request(32)
                            .height_request(32)
                            .build();
                        spinner.update_property(&[gtk::accessible::Property::Label("Loading screenshot")]);
                        frame.set_child(Some(&spinner));

                        widgets::set_pointer_cursor(&frame);

                        // Click handler for lightbox
                        let textures_click = textures_ref.clone();
                        let paths_click = paths_ref.clone();
                        let name_click = app_name.clone();
                        let click = gtk::GestureClick::new();
                        let idx = i;
                        click.connect_released(move |gesture, _, _, _| {
                            gesture.set_state(gtk::EventSequenceState::Claimed);
                            let t = textures_click.borrow();
                            if t.get(idx).is_some_and(|t| t.is_some()) {
                                if let Some(widget) = gesture.widget() {
                                    if let Some(root) = gtk::prelude::WidgetExt::root(&widget) {
                                        if let Ok(window) = root.downcast::<gtk::Window>() {
                                            detail_view::show_screenshot_lightbox(
                                                &window,
                                                &textures_click,
                                                idx,
                                                Some(&paths_click),
                                                Some(&name_click),
                                            );
                                        }
                                    }
                                }
                            }
                        });
                        frame.add_controller(click);

                        // Load screenshot asynchronously
                        let name = app_name.clone();
                        let spath = paths_ref[i].clone();
                        let frame_ref = frame.clone();
                        let tex_ref = textures_ref.clone();

                        glib::spawn_future_local(async move {
                            let n = name.clone();
                            let n_label = n.clone();
                            let sp = spath.clone();
                            let load_idx = i;

                            let result = gio::spawn_blocking(move || {
                                catalog::cache_screenshot(&n, &sp, load_idx)
                                    .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::Contain)
                                            .halign(gtk::Align::Center)
                                            .valign(gtk::Align::Center)
                                            .build();
                                        picture.update_property(&[gtk::accessible::Property::Label(
                                            &format!("Screenshot of {}", n_label),
                                        )]);
                                        frame_ref.set_child(Some(&picture));
                                        tex_ref.borrow_mut()[load_idx] = Some(texture);
                                    }
                                }
                                _ => {
                                    let fallback = gtk::Label::builder()
                                        .label("Screenshot unavailable")
                                        .css_classes(["dim-label"])
                                        .halign(gtk::Align::Center)
                                        .valign(gtk::Align::Center)
                                        .build();
                                    frame_ref.set_child(Some(&fallback));
                                }
                            }
                        });

                        page_box.append(&frame);
                    }

                    // Crossfade: alternate between "a" and "b" children
                    let current_flip = flip.get();
                    let child_name = if current_flip { "ss_b" } else { "ss_a" };
                    stack.add_named(&page_box, Some(child_name));
                    stack.set_visible_child_name(child_name);
                    // Remove the old child
                    let old_name = if current_flip { "ss_a" } else { "ss_b" };
                    if let Some(old_child) = stack.child_by_name(old_name) {
                        stack.remove(&old_child);
                    }
                    flip.set(!current_flip);
                })
            };

            // Show initial page
            build_ss_page(0, &ss_stack, &ss_flip, &ss_left, &ss_right);

            // Wire arrow clicks
            {
                let page_ref = screenshot_page.clone();
                let stack_ref = ss_stack.clone();
                let flip_ref = ss_flip.clone();
                let left_ref = ss_left.clone();
                let right_ref = ss_right.clone();
                let build_ref = build_ss_page.clone();
                ss_left.connect_clicked(move |_| {
                    let page = page_ref.get();
                    if page > 0 {
                        page_ref.set(page - 1);
                        build_ref(page - 1, &stack_ref, &flip_ref, &left_ref, &right_ref);
                    }
                });
            }
            {
                let page_ref = screenshot_page.clone();
                let stack_ref = ss_stack.clone();
                let flip_ref = ss_flip.clone();
                let left_ref = ss_left.clone();
                let right_ref = ss_right.clone();
                let build_ref = build_ss_page.clone();
                ss_right.connect_clicked(move |_| {
                    let page = page_ref.get();
                    if page < max_page {
                        page_ref.set(page + 1);
                        build_ref(page + 1, &stack_ref, &flip_ref, &left_ref, &right_ref);
                    }
                });
            }

            content.append(&ss_row);
        }
    }

    content.append(&about_container);
    content.append(&readme_container);

    // --- Details section ---
    let details_group = adw::PreferencesGroup::builder()
        .title(&i18n("Details"))
        .build();

    // Type/Category from OCS
    if let Some(ref typename) = app.ocs_typename {
        if !typename.is_empty() {
            let escaped = typename.replace('&', "&amp;");
            let row = adw::ActionRow::builder()
                .title(&i18n("Type"))
                .subtitle(&escaped)
                .build();
            details_group.add(&row);
        }
    }

    if let Some(ref cats) = app.categories {
        if !cats.is_empty() {
            let escaped_cats = cats.replace('&', "&amp;");
            let row = adw::ActionRow::builder()
                .title(&i18n("Categories"))
                .subtitle(&escaped_cats)
                .build();
            details_group.add(&row);
        }
    }

    if let Some(ref license) = app.license {
        if !license.is_empty() {
            let row = adw::ActionRow::builder()
                .title(&i18n("License"))
                .subtitle(license)
                .subtitle_selectable(true)
                .build();
            details_group.add(&row);
        }
    }

    // Architecture
    if let Some(ref arch) = app.ocs_arch {
        if !arch.is_empty() {
            let row = adw::ActionRow::builder()
                .title(&i18n("Architecture"))
                .subtitle(arch)
                .build();
            details_group.add(&row);
        }
    }

    // File name
    if let Some(ref dlname) = app.ocs_downloadname {
        if !dlname.is_empty() {
            let row = adw::ActionRow::builder()
                .title(&i18n("Filename"))
                .subtitle(dlname)
                .subtitle_selectable(true)
                .build();
            details_group.add(&row);
        }
    }

    // File size
    if let Some(size_kb) = app.ocs_downloadsize {
        if size_kb > 0 {
            let row = adw::ActionRow::builder()
                .title(&i18n("File size"))
                .subtitle(&widgets::format_size(size_kb * 1024))
                .build();
            details_group.add(&row);
        }
    }

    // MD5 checksum
    if let Some(ref md5) = app.ocs_md5sum {
        if !md5.is_empty() {
            let row = adw::ActionRow::builder()
                .title(&i18n("MD5"))
                .subtitle(md5)
                .subtitle_selectable(true)
                .build();
            details_group.add(&row);
        }
    }

    if let Some(ref tags) = app.ocs_tags {
        if !tags.is_empty() {
            // Format tags nicely with commas
            let formatted = tags.split(',')
                .map(|t| t.trim())
                .filter(|t| !t.is_empty())
                .collect::<Vec<_>>()
                .join(", ")
                .replace('&', "&amp;");
            let row = adw::ActionRow::builder()
                .title(&i18n("Tags"))
                .subtitle(&formatted)
                .subtitle_selectable(true)
                .build();
            details_group.add(&row);
        }
    }

    // Created date
    if let Some(ref created) = app.ocs_created {
        if !created.is_empty() {
            let row = adw::ActionRow::builder()
                .title(&i18n("Published"))
                .subtitle(&widgets::relative_time(created))
                .build();
            details_group.add(&row);
        }
    }

    // Comments count
    if let Some(comments) = app.ocs_comments {
        if comments > 0 {
            let row = adw::ActionRow::builder()
                .title(&i18n("Comments"))
                .subtitle(&format!("{}", comments))
                .build();
            details_group.add(&row);
        }
    }

    // Source catalog
    let source_type = if app.ocs_id.is_some() {
        catalog::CatalogType::OcsAppImageHub
    } else {
        catalog::CatalogType::AppImageHub
    };
    let source_row = adw::ActionRow::builder()
        .title(&i18n("Source"))
        .subtitle(source_type.label())
        .build();
    details_group.add(&source_row);

    content.append(&details_group);

    // --- Links section ---
    let links_group = adw::PreferencesGroup::builder()
        .title(&i18n("Links"))
        .build();

    // GitHub link (extracted from description or already known)
    if let Some(ref owner) = app.github_owner {
        if let Some(ref repo) = app.github_repo {
            let gh_url = format!("https://github.com/{}/{}", owner, repo);
            let row = adw::ActionRow::builder()
                .title(&i18n("GitHub"))
                .subtitle(&gh_url)
                .subtitle_selectable(true)
                .activatable(true)
                .build();
            let arrow = gtk::Image::from_icon_name("external-link-symbolic");
            arrow.set_valign(gtk::Align::Center);
            arrow.set_accessible_role(gtk::AccessibleRole::Presentation);
            row.add_suffix(&arrow);

            let url = gh_url;
            row.connect_activated(move |row| {
                let launcher = gtk::UriLauncher::new(&url);
                let root = row.root().and_downcast::<gtk::Window>();
                launcher.launch(root.as_ref(), gio::Cancellable::NONE, |_| {});
            });
            links_group.add(&row);
        }
    }

    // AppImageHub.com detail page
    if let Some(ref detailpage) = app.ocs_detailpage {
        if !detailpage.is_empty() {
            let row = adw::ActionRow::builder()
                .title(&i18n("AppImageHub"))
                .subtitle(detailpage)
                .subtitle_selectable(true)
                .activatable(true)
                .build();
            let arrow = gtk::Image::from_icon_name("external-link-symbolic");
            arrow.set_valign(gtk::Align::Center);
            arrow.set_accessible_role(gtk::AccessibleRole::Presentation);
            row.add_suffix(&arrow);

            let dp = detailpage.clone();
            row.connect_activated(move |row| {
                let launcher = gtk::UriLauncher::new(&dp);
                let root = row.root().and_downcast::<gtk::Window>();
                launcher.launch(root.as_ref(), gio::Cancellable::NONE, |_| {});
            });
            links_group.add(&row);
        }
    }

    // Homepage (non-OCS apps or if we have a real homepage)
    if let Some(ref homepage) = app.homepage {
        // Don't duplicate if it's the same as the detailpage
        let is_detailpage = app.ocs_detailpage.as_deref() == Some(homepage.as_str());
        if !is_detailpage {
            let row = adw::ActionRow::builder()
                .title(&i18n("Homepage"))
                .subtitle(homepage)
                .subtitle_selectable(true)
                .activatable(true)
                .build();
            let arrow = gtk::Image::from_icon_name("external-link-symbolic");
            arrow.set_valign(gtk::Align::Center);
            arrow.set_accessible_role(gtk::AccessibleRole::Presentation);
            row.add_suffix(&arrow);

            let hp = homepage.clone();
            row.connect_activated(move |row| {
                let launcher = gtk::UriLauncher::new(&hp);
                let root = row.root().and_downcast::<gtk::Window>();
                launcher.launch(root.as_ref(), gio::Cancellable::NONE, |_| {});
            });
            links_group.add(&row);
        }
    }



    content.append(&links_group);

    // --- Status section ---
    let status_group = adw::PreferencesGroup::builder()
        .title(&i18n("Status"))
        .build();

    let status_row = adw::ActionRow::builder()
        .title(&i18n("Installed"))
        .build();
    let status_badge = if is_installed {
        widgets::status_badge(&i18n("Yes"), "success")
    } else {
        widgets::status_badge(&i18n("No"), "neutral")
    };
    status_badge.set_valign(gtk::Align::Center);
    status_row.add_suffix(&status_badge);
    status_group.add(&status_row);

    content.append(&status_group);

    clamp.set_child(Some(&content));
    scrolled.set_child(Some(&clamp));
    toolbar.set_content(Some(&scrolled));
    widgets::apply_pointer_cursors(&toolbar);

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

/// Trigger an install from a URL. Handles async download, DB registration, and UI feedback.
/// `ocs_slot` is the 1-based OCS download file slot (default 1).
fn do_install(
    url: String,
    app_name: String,
    homepage: Option<String>,
    toast_overlay: adw::ToastOverlay,
    db: Rc<Database>,
    widget: gtk::Widget,
    ocs_id: Option<i64>,
    ocs_slot: u32,
) {
    glib::spawn_future_local(async move {
        let name = app_name.clone();
        let hp = homepage.clone();
        let dl_url = url.clone();

        let result = gio::spawn_blocking(move || {
            let install_dir = dirs::home_dir()
                .unwrap_or_else(|| std::path::PathBuf::from("/tmp"))
                .join("Applications");
            std::fs::create_dir_all(&install_dir).ok();

            let cat_app = catalog::CatalogApp {
                name,
                description: None,
                categories: Vec::new(),
                latest_version: None,
                download_url: dl_url,
                icon_url: None,
                homepage: hp,
                file_size: None,
                architecture: None,
                screenshots: Vec::new(),
                license: None,
                github_link: None,
            };

            catalog::install_from_catalog_with_ocs(&cat_app, &install_dir, ocs_id, ocs_slot)
                .map_err(|e| e.to_string())
        }).await;

        match result {
            Ok(Ok(path)) => {
                let filename = path.file_name()
                    .map(|n| n.to_string_lossy().to_string())
                    .unwrap_or_default();
                let size = std::fs::metadata(&path)
                    .map(|m| m.len() as i64)
                    .unwrap_or(0);
                db.upsert_appimage(
                    &path.to_string_lossy(),
                    &filename,
                    Some(2),
                    size,
                    true,
                    None,
                ).ok();
                toast_overlay.add_toast(widgets::info_toast(&i18n("Installed successfully")));
                if let Some(btn) = widget.downcast_ref::<gtk::Button>() {
                    btn.set_label("Installed");
                } else if let Some(split) = widget.downcast_ref::<adw::SplitButton>() {
                    split.set_label("Installed");
                }
            }
            Ok(Err(e)) => {
                log::error!("Install failed: {}", e);
                toast_overlay.add_toast(widgets::error_toast(&i18n("Install failed")));
                widget.set_sensitive(true);
                if let Some(btn) = widget.downcast_ref::<gtk::Button>() {
                    btn.set_label("Install");
                } else if let Some(split) = widget.downcast_ref::<adw::SplitButton>() {
                    split.set_label("Install");
                }
            }
            Err(_) => {
                log::error!("Install thread panicked");
                toast_overlay.add_toast(widgets::error_toast(&i18n("Install failed")));
                widget.set_sensitive(true);
                if let Some(btn) = widget.downcast_ref::<gtk::Button>() {
                    btn.set_label("Install");
                } else if let Some(split) = widget.downcast_ref::<adw::SplitButton>() {
                    split.set_label("Install");
                }
            }
        }
    });
}

/// Populate the install button slot with the appropriate button (plain or split).
/// Clears any existing children first, so it can be called to rebuild after enrichment.
/// `ocs_files` - if provided, OCS download files to show in the dropdown.
fn populate_install_slot(
    slot: &gtk::Box,
    app_name: &str,
    download_url: &str,
    github_download_url: Option<&str>,
    github_release_assets: Option<&str>,
    toast_overlay: &adw::ToastOverlay,
    db: &Rc<Database>,
    homepage: Option<&str>,
    ocs_id: Option<i64>,
    ocs_files: &[catalog::OcsDownloadFile],
) {
    // Clear existing children (e.g. the "Loading..." placeholder)
    while let Some(child) = slot.first_child() {
        slot.remove(&child);
    }

    let assets: Vec<AppImageAsset> = github_release_assets
        .and_then(|json| serde_json::from_str(json).ok())
        .unwrap_or_default();

    let default_url = github_download_url
        .unwrap_or(download_url)
        .to_string();

    // Determine the default OCS slot (first file = newest)
    let default_ocs_slot = ocs_files.first().map(|f| f.slot).unwrap_or(1);

    // Combine GitHub assets and OCS files into dropdown options
    let has_gh_assets = assets.len() > 1;
    let has_ocs_files = ocs_files.len() > 1;

    if has_gh_assets || has_ocs_files {
        let menu = gio::Menu::new();

        // OCS version files (each needs slot-based install)
        for file in ocs_files {
            let label = format_ocs_file_label(file);
            // Encode ocs_id and slot as action param: "ocs:{ocs_id}:{slot}"
            menu.append(Some(&label), Some(&format!("install.ocs::{}:{}", file.ocs_id, file.slot)));
        }

        // GitHub release assets
        for asset in &assets {
            let label = format_asset_label(&asset.name, asset.size);
            menu.append(Some(&label), Some(&format!("install.gh::{}", asset.url)));
        }

        // Fallback to original catalog URL
        if has_gh_assets || has_ocs_files {
            menu.append(
                Some(&i18n("Direct download (original)")),
                Some(&format!("install.gh::{}", download_url)),
            );
        }

        let split_btn = adw::SplitButton::builder()
            .label(&i18n("Install"))
            .menu_model(&menu)
            .css_classes(["suggested-action", "pill"])
            .build();
        split_btn.update_property(&[gtk::accessible::Property::Description(
            &format!("Install {}", app_name),
        )]);

        // Default click: install the default version
        let url_for_click = default_url;
        let name_for_click = app_name.to_string();
        let hp_for_click = homepage.map(|s| s.to_string());
        let toast_for_click = toast_overlay.clone();
        let db_for_click = db.clone();
        let ocs_id_click = ocs_id;
        let default_slot = default_ocs_slot;

        split_btn.connect_clicked(move |btn| {
            btn.set_sensitive(false);
            btn.set_label("Installing...");
            do_install(
                url_for_click.clone(),
                name_for_click.clone(),
                hp_for_click.clone(),
                toast_for_click.clone(),
                db_for_click.clone(),
                btn.upcast_ref::<gtk::Widget>().clone(),
                ocs_id_click,
                default_slot,
            );
        });

        let action_group = gio::SimpleActionGroup::new();

        // OCS slot-based install action
        let ocs_action = gio::SimpleAction::new("ocs", Some(glib::VariantTy::STRING));
        let name_ocs = app_name.to_string();
        let hp_ocs = homepage.map(|s| s.to_string());
        let toast_ocs = toast_overlay.clone();
        let db_ocs = db.clone();
        let split_ocs = split_btn.clone();
        let dl_url_ocs = download_url.to_string();
        ocs_action.connect_activate(move |_, param| {
            if let Some(param_str) = param.and_then(|p| p.str()) {
                // Parse "ocs_id:slot"
                let parts: Vec<&str> = param_str.splitn(2, ':').collect();
                if let (Some(id_str), Some(slot_str)) = (parts.first(), parts.get(1)) {
                    let ocs_id = id_str.parse::<i64>().ok();
                    let slot = slot_str.parse::<u32>().unwrap_or(1);
                    split_ocs.set_sensitive(false);
                    split_ocs.set_label("Installing...");
                    do_install(
                        dl_url_ocs.clone(),
                        name_ocs.clone(),
                        hp_ocs.clone(),
                        toast_ocs.clone(),
                        db_ocs.clone(),
                        split_ocs.upcast_ref::<gtk::Widget>().clone(),
                        ocs_id,
                        slot,
                    );
                }
            }
        });
        action_group.add_action(&ocs_action);

        // GitHub URL-based install action
        let gh_action = gio::SimpleAction::new("gh", Some(glib::VariantTy::STRING));
        let name_gh = app_name.to_string();
        let hp_gh = homepage.map(|s| s.to_string());
        let toast_gh = toast_overlay.clone();
        let db_gh = db.clone();
        let split_gh = split_btn.clone();
        gh_action.connect_activate(move |_, param| {
            if let Some(url) = param.and_then(|p| p.str()) {
                split_gh.set_sensitive(false);
                split_gh.set_label("Installing...");
                do_install(
                    url.to_string(),
                    name_gh.clone(),
                    hp_gh.clone(),
                    toast_gh.clone(),
                    db_gh.clone(),
                    split_gh.upcast_ref::<gtk::Widget>().clone(),
                    None,
                    1,
                );
            }
        });
        action_group.add_action(&gh_action);

        split_btn.insert_action_group("install", Some(&action_group));

        widgets::set_pointer_cursor(&split_btn);
        slot.append(&split_btn);
    } else {
        // Single asset - plain button
        let install_btn = gtk::Button::builder()
            .label(&i18n("Install"))
            .css_classes(["suggested-action", "pill"])
            .build();
        widgets::set_pointer_cursor(&install_btn);

        let url_clone = default_url;
        let name_clone = app_name.to_string();
        let hp_clone = homepage.map(|s| s.to_string());
        let toast_clone = toast_overlay.clone();
        let db_clone = db.clone();
        let ocs_id_plain = ocs_id;
        let slot_plain = default_ocs_slot;

        install_btn.connect_clicked(move |btn| {
            btn.set_sensitive(false);
            btn.set_label("Installing...");
            do_install(
                url_clone.clone(),
                name_clone.clone(),
                hp_clone.clone(),
                toast_clone.clone(),
                db_clone.clone(),
                btn.upcast_ref::<gtk::Widget>().clone(),
                ocs_id_plain,
                slot_plain,
            );
        });

        slot.append(&install_btn);
    }
}

/// Format an OCS download file label for the dropdown menu.
fn format_ocs_file_label(file: &catalog::OcsDownloadFile) -> String {
    let mut parts = Vec::new();
    if !file.version.is_empty() {
        parts.push(format!("v{}", file.version));
    }
    if let Some(ref arch) = file.arch {
        parts.push(arch.clone());
    }
    if !file.filename.is_empty() {
        parts.push(file.filename.clone());
    }
    if let Some(ref pkg_type) = file.pkg_type {
        if pkg_type != "appimage" {
            parts.push(format!("[{}]", pkg_type));
        }
    }
    if let Some(size_kb) = file.size_kb {
        if size_kb > 0 {
            parts.push(format!("({})", widgets::format_size(size_kb * 1024)));
        }
    }
    if parts.is_empty() {
        format!("File {}", file.slot)
    } else {
        parts.join(" - ")
    }
}

/// Format an asset filename with size for the dropdown menu.
fn format_asset_label(name: &str, size: i64) -> String {
    if size > 0 {
        format!("{} ({})", name, widgets::format_size(size))
    } else {
        name.to_string()
    }
}

/// Build a single stat card widget with icon, value label, and description label.
fn build_stat_card(icon_name: &str, value_label: &gtk::Label, label_text: &str) -> gtk::Box {
    let card = gtk::Box::builder()
        .orientation(gtk::Orientation::Vertical)
        .spacing(4)
        .hexpand(true)
        .build();
    card.add_css_class("stat-card");

    let icon_row = gtk::Box::builder()
        .orientation(gtk::Orientation::Horizontal)
        .spacing(6)
        .build();

    let icon = gtk::Image::from_icon_name(icon_name);
    icon.set_pixel_size(14);
    icon.update_property(&[gtk::accessible::Property::Label(label_text)]);
    icon_row.append(&icon);
    icon_row.append(value_label);
    card.append(&icon_row);

    let label = gtk::Label::builder()
        .label(label_text)
        .css_classes(["stat-label"])
        .xalign(0.0)
        .build();
    card.append(&label);

    // Accessible label combining value and type
    let value_text = value_label.text();
    card.update_property(&[gtk::accessible::Property::Label(&format!("{}: {}", label_text, value_text))]);

    card
}

/// Check if enrichment data is stale (>24 hours old).
fn is_enrichment_stale(enriched_at: Option<&str>) -> bool {
    let Some(ts) = enriched_at else { return true };
    let Ok(parsed) = chrono::NaiveDateTime::parse_from_str(ts, "%Y-%m-%d %H:%M:%S") else {
        return true;
    };
    let now = chrono::Utc::now().naive_utc();
    let elapsed = now.signed_duration_since(parsed);
    elapsed.num_hours() >= 24
}

/// Re-export format_count from widgets for use in this module.
fn format_count(n: i64) -> String {
    widgets::format_count(n)
}

/// Extract author/org from a URL for display.
/// For GitHub/GitLab URLs, extracts the username/org from the path.
/// For other URLs, returns the domain.
fn extract_author(url: &str) -> String {
    let stripped = url.trim_start_matches("https://")
        .trim_start_matches("http://");
    let parts: Vec<&str> = stripped.splitn(3, '/').collect();
    let domain = parts.first().copied().unwrap_or("");

    // For GitHub/GitLab, extract the org/user from the first path segment
    if domain == "github.com" || domain == "gitlab.com" {
        if let Some(org) = parts.get(1) {
            if !org.is_empty() {
                return org.to_string();
            }
        }
    }

    domain.to_string()
}
