use adw::prelude::*;
use gtk::glib;
use gtk::subclass::prelude::ObjectSubclassIsExt;
use std::cell::RefCell;
use std::collections::HashSet;
use std::path::PathBuf;
use std::rc::Rc;

use crate::app::AppState;
use crate::utils::format_size;

const THUMB_SIZE: i32 = 120;

pub fn build_images_page(state: &AppState) -> adw::NavigationPage {
    let stack = gtk::Stack::builder()
        .transition_type(gtk::StackTransitionType::Crossfade)
        .build();

    // Empty state - drop zone
    let empty_state = build_empty_state();
    stack.add_named(&empty_state, Some("empty"));

    // Loaded state - thumbnail grid
    let loaded_state = build_loaded_state(state);
    stack.add_named(&loaded_state, Some("loaded"));

    stack.set_visible_child_name("empty");

    // Session-level remembered subfolder choice (None = not yet asked)
    let subfolder_choice: Rc<RefCell<Option<bool>>> = Rc::new(RefCell::new(None));

    // Set up drag-and-drop on the entire page
    let drop_target = gtk::DropTarget::new(gtk::gio::File::static_type(), gtk::gdk::DragAction::COPY);
    drop_target.set_types(&[gtk::gio::File::static_type()]);

    {
        let loaded_files = state.loaded_files.clone();
        let excluded = state.excluded_files.clone();
        let sizes = state.file_sizes.clone();
        let stack_ref = stack.clone();
        let subfolder_choice = subfolder_choice.clone();
        drop_target.connect_drop(move |target, value, _x, _y| {
            if let Ok(file) = value.get::<gtk::gio::File>()
                && let Some(path) = file.path()
            {
                if path.is_dir() {
                    let has_subdirs = has_subfolders(&path);
                    if !has_subdirs {
                        let mut files = loaded_files.borrow_mut();
                        add_images_flat(&path, &mut files);
                        let count = files.len();
                        drop(files);
                        refresh_grid(&stack_ref, &loaded_files, &excluded, &sizes, count);
                    } else {
                        let choice = *subfolder_choice.borrow();
                        match choice {
                            Some(true) => {
                                let mut files = loaded_files.borrow_mut();
                                add_images_from_dir(&path, &mut files);
                                let count = files.len();
                                drop(files);
                                refresh_grid(&stack_ref, &loaded_files, &excluded, &sizes, count);
                            }
                            Some(false) => {
                                let mut files = loaded_files.borrow_mut();
                                add_images_flat(&path, &mut files);
                                let count = files.len();
                                drop(files);
                                refresh_grid(&stack_ref, &loaded_files, &excluded, &sizes, count);
                            }
                            None => {
                                let mut files = loaded_files.borrow_mut();
                                add_images_flat(&path, &mut files);
                                let count = files.len();
                                drop(files);
                                refresh_grid(&stack_ref, &loaded_files, &excluded, &sizes, count);

                                let loaded_files = loaded_files.clone();
                                let excluded = excluded.clone();
                                let sz = sizes.clone();
                                let stack_ref = stack_ref.clone();
                                let subfolder_choice = subfolder_choice.clone();
                                let dir_path = path.clone();
                                let window = target.widget()
                                    .and_then(|w| w.root())
                                    .and_then(|r| r.downcast::<gtk::Window>().ok());
                                glib::idle_add_local_once(move || {
                                    show_subfolder_prompt(
                                        window.as_ref(),
                                        &dir_path,
                                        &loaded_files,
                                        &excluded,
                                        &sz,
                                        &stack_ref,
                                        &subfolder_choice,
                                    );
                                });
                            }
                        }
                    }
                    return true;
                } else if is_image_file(&path) {
                    let mut files = loaded_files.borrow_mut();
                    if !files.contains(&path) {
                        files.push(path);
                    }
                    let count = files.len();
                    drop(files);
                    refresh_grid(&stack_ref, &loaded_files, &excluded, &sizes, count);
                    return true;
                }
            }
            false
        });
    }

    stack.add_controller(drop_target);

    // Also accept URI text drops (from web browsers)
    let uri_drop = gtk::DropTarget::new(glib::GString::static_type(), gtk::gdk::DragAction::COPY);
    {
        let loaded_files = state.loaded_files.clone();
        let excluded = state.excluded_files.clone();
        let sizes = state.file_sizes.clone();
        let stack_ref = stack.clone();
        uri_drop.connect_drop(move |_target, value, _x, _y| {
            if let Ok(text) = value.get::<glib::GString>() {
                let text = text.trim().to_string();
                // Check if it looks like an image URL
                let lower = text.to_lowercase();
                let is_image_url = (lower.starts_with("http://") || lower.starts_with("https://"))
                    && (lower.ends_with(".jpg")
                        || lower.ends_with(".jpeg")
                        || lower.ends_with(".png")
                        || lower.ends_with(".webp")
                        || lower.ends_with(".gif")
                        || lower.ends_with(".avif")
                        || lower.ends_with(".tiff")
                        || lower.ends_with(".bmp")
                        || lower.contains(".jpg?")
                        || lower.contains(".jpeg?")
                        || lower.contains(".png?")
                        || lower.contains(".webp?"));

                if is_image_url {
                    let loaded = loaded_files.clone();
                    let excl = excluded.clone();
                    let sz = sizes.clone();
                    let sr = stack_ref.clone();
                    // Download in background thread
                    let (tx, rx) = std::sync::mpsc::channel::<Option<std::path::PathBuf>>();
                    let url = text.clone();
                    std::thread::spawn(move || {
                        let result = download_image_url(&url);
                        let _ = tx.send(result);
                    });

                    glib::timeout_add_local(std::time::Duration::from_millis(100), move || {
                        match rx.try_recv() {
                            Ok(Some(path)) => {
                                let mut files = loaded.borrow_mut();
                                if !files.contains(&path) {
                                    files.push(path);
                                }
                                let count = files.len();
                                drop(files);
                                refresh_grid(&sr, &loaded, &excl, &sz, count);
                                glib::ControlFlow::Break
                            }
                            Ok(None) => glib::ControlFlow::Break,
                            Err(std::sync::mpsc::TryRecvError::Empty) => glib::ControlFlow::Continue,
                            Err(_) => glib::ControlFlow::Break,
                        }
                    });
                    return true;
                }
            }
            false
        });
    }
    stack.add_controller(uri_drop);

    adw::NavigationPage::builder()
        .title("Add Images")
        .tag("step-images")
        .child(&stack)
        .build()
}

fn is_image_file(path: &std::path::Path) -> bool {
    match path.extension().and_then(|e| e.to_str()).map(|e| e.to_lowercase()) {
        Some(ext) => matches!(ext.as_str(),
            "jpg" | "jpeg" | "png" | "webp" | "avif" | "gif" | "tiff" | "tif" | "bmp"
            | "ico" | "hdr" | "exr" | "pnm" | "ppm" | "pgm" | "pbm" | "pam"
            | "tga" | "dds" | "ff" | "farbfeld" | "qoi"
            | "heic" | "heif" | "jxl"
            | "svg" | "svgz"
            | "raw" | "cr2" | "cr3" | "nef" | "nrw" | "arw" | "srf" | "sr2"
            | "orf" | "rw2" | "raf" | "dng" | "pef" | "srw" | "x3f"
            | "pcx" | "xpm" | "xbm" | "wbmp" | "jp2" | "j2k" | "jpf" | "jpx"
        ),
        None => false,
    }
}

fn add_images_from_dir(dir: &std::path::Path, files: &mut Vec<PathBuf>) {
    let existing: std::collections::HashSet<PathBuf> = files.iter().cloned().collect();
    add_images_from_dir_inner(dir, files, &existing);
}

fn add_images_from_dir_inner(
    dir: &std::path::Path,
    files: &mut Vec<PathBuf>,
    existing: &std::collections::HashSet<PathBuf>,
) {
    if let Ok(entries) = std::fs::read_dir(dir) {
        for entry in entries.flatten() {
            let path = entry.path();
            if path.is_dir() {
                add_images_from_dir_inner(&path, files, existing);
            } else if is_image_file(&path) && !existing.contains(&path) && !files.contains(&path) {
                files.push(path);
            }
        }
    }
}

fn add_images_flat(dir: &std::path::Path, files: &mut Vec<PathBuf>) {
    let existing: std::collections::HashSet<PathBuf> = files.iter().cloned().collect();
    if let Ok(entries) = std::fs::read_dir(dir) {
        for entry in entries.flatten() {
            let path = entry.path();
            if path.is_file() && is_image_file(&path) && !existing.contains(&path) {
                files.push(path);
            }
        }
    }
}

fn has_subfolders(dir: &std::path::Path) -> bool {
    if let Ok(entries) = std::fs::read_dir(dir) {
        for entry in entries.flatten() {
            if entry.path().is_dir() {
                return true;
            }
        }
    }
    false
}

fn add_images_from_subdirs(dir: &std::path::Path, files: &mut Vec<PathBuf>) {
    if let Ok(entries) = std::fs::read_dir(dir) {
        for entry in entries.flatten() {
            let path = entry.path();
            if path.is_dir() {
                add_images_from_dir(&path, files);
            }
        }
    }
}

fn show_subfolder_prompt(
    window: Option<&gtk::Window>,
    dir: &std::path::Path,
    loaded_files: &Rc<RefCell<Vec<PathBuf>>>,
    excluded: &Rc<RefCell<HashSet<PathBuf>>>,
    file_sizes: &Rc<RefCell<std::collections::HashMap<PathBuf, u64>>>,
    stack: &gtk::Stack,
    subfolder_choice: &Rc<RefCell<Option<bool>>>,
) {
    let dialog = adw::AlertDialog::builder()
        .heading("Include subfolders?")
        .body("This folder contains subfolders. Would you like to include images from subfolders too?")
        .build();
    dialog.add_response("no", "Top-level Only");
    dialog.add_response("yes", "Include Subfolders");
    dialog.set_default_response(Some("yes"));
    dialog.set_response_appearance("yes", adw::ResponseAppearance::Suggested);

    let loaded_files = loaded_files.clone();
    let excluded = excluded.clone();
    let file_sizes = file_sizes.clone();
    let stack = stack.clone();
    let subfolder_choice = subfolder_choice.clone();
    let dir = dir.to_path_buf();
    dialog.connect_response(None, move |_dialog, response| {
        let include_subdirs = response == "yes";
        *subfolder_choice.borrow_mut() = Some(include_subdirs);
        if include_subdirs {
            let mut files = loaded_files.borrow_mut();
            add_images_from_subdirs(&dir, &mut files);
            let count = files.len();
            drop(files);
            refresh_grid(&stack, &loaded_files, &excluded, &file_sizes, count);
        }
    });

    if let Some(win) = window {
        dialog.present(Some(win));
    }
}

// ------------------------------------------------------------------
// Thumbnail grid
// ------------------------------------------------------------------

/// Refresh the grid view and count label from current loaded_files state
fn refresh_grid(
    stack: &gtk::Stack,
    loaded_files: &Rc<RefCell<Vec<PathBuf>>>,
    excluded: &Rc<RefCell<HashSet<PathBuf>>>,
    file_sizes: &Rc<RefCell<std::collections::HashMap<PathBuf, u64>>>,
    count: usize,
) {
    if count > 0 {
        stack.set_visible_child_name("loaded");
    } else {
        stack.set_visible_child_name("empty");
    }
    if let Some(loaded_widget) = stack.child_by_name("loaded") {
        rebuild_grid_model(&loaded_widget, loaded_files, excluded, file_sizes);
    }
}

/// Walk the widget tree to find our ListStore and count label, then rebuild
pub fn rebuild_grid_model(
    widget: &gtk::Widget,
    loaded_files: &Rc<RefCell<Vec<PathBuf>>>,
    excluded: &Rc<RefCell<HashSet<PathBuf>>>,
    file_sizes: &Rc<RefCell<std::collections::HashMap<PathBuf, u64>>>,
) {
    let files = loaded_files.borrow();
    let excluded_set = excluded.borrow();
    let count = files.len();
    let included_count = files.iter().filter(|p| !excluded_set.contains(*p)).count();
    // Populate size cache for any new files
    {
        let mut sizes = file_sizes.borrow_mut();
        for p in files.iter() {
            sizes.entry(p.clone()).or_insert_with(|| {
                std::fs::metadata(p).map(|m| m.len()).unwrap_or(0)
            });
        }
    }
    let sizes = file_sizes.borrow();
    let total_size: u64 = files.iter()
        .filter(|p| !excluded_set.contains(*p))
        .map(|p| sizes.get(p).copied().unwrap_or(0))
        .sum();
    let size_str = format_size(total_size);

    // Update the count label
    update_heading_label(widget, count, included_count, &size_str);

    // Find the GridView and rebuild its model
    if let Some(grid_view) = find_grid_view(widget) {
        if let Some(model) = grid_view.model()
            && let Some(sel) = model.downcast_ref::<gtk::NoSelection>()
            && let Some(store) = sel.model()
            && let Some(store) = store.downcast_ref::<gtk::gio::ListStore>()
        {
            store.remove_all();
            for path in files.iter() {
                let item = ImageItem::new(path);
                store.append(&item);
            }
        }
    }
}

fn find_grid_view(widget: &gtk::Widget) -> Option<gtk::GridView> {
    if let Some(gv) = widget.downcast_ref::<gtk::GridView>() {
        return Some(gv.clone());
    }
    let mut child = widget.first_child();
    while let Some(c) = child {
        if let Some(gv) = find_grid_view(&c) {
            return Some(gv);
        }
        child = c.next_sibling();
    }
    None
}

fn update_heading_label(widget: &gtk::Widget, count: usize, included_count: usize, size_str: &str) {
    if let Some(label) = widget.downcast_ref::<gtk::Label>()
        && label.css_classes().iter().any(|c| c == "heading")
    {
        if included_count == count {
            label.set_label(&format!("{} images ({})", count, size_str));
        } else {
            label.set_label(&format!("{}/{} images selected ({})", included_count, count, size_str));
        }
    }
    let mut child = widget.first_child();
    while let Some(c) = child {
        update_heading_label(&c, count, included_count, size_str);
        child = c.next_sibling();
    }
}

/// Update only the count label without rebuilding the grid
fn update_count_label(
    widget: &gtk::Widget,
    loaded_files: &Rc<RefCell<Vec<PathBuf>>>,
    excluded: &Rc<RefCell<HashSet<PathBuf>>>,
    file_sizes: &Rc<RefCell<std::collections::HashMap<PathBuf, u64>>>,
) {
    let files = loaded_files.borrow();
    let excluded_set = excluded.borrow();
    let sizes = file_sizes.borrow();
    let count = files.len();
    let included_count = files.iter().filter(|p| !excluded_set.contains(*p)).count();
    let total_size: u64 = files.iter()
        .filter(|p| !excluded_set.contains(*p))
        .map(|p| sizes.get(p).copied().unwrap_or(0))
        .sum();
    let size_str = format_size(total_size);
    update_heading_label(widget, count, included_count, &size_str);
}

// ------------------------------------------------------------------
// GObject wrapper for list store items
// ------------------------------------------------------------------

mod imp {
    use super::*;
    use std::cell::OnceCell;

    #[derive(Default)]
    pub struct ImageItem {
        pub path: OnceCell<PathBuf>,
    }

    #[glib::object_subclass]
    impl glib::subclass::types::ObjectSubclass for ImageItem {
        const NAME: &'static str = "PixstripImageItem";
        type Type = super::ImageItem;
        type ParentType = glib::Object;
    }

    impl glib::subclass::object::ObjectImpl for ImageItem {}
}

glib::wrapper! {
    pub struct ImageItem(ObjectSubclass<imp::ImageItem>);
}

impl ImageItem {
    fn new(path: &std::path::Path) -> Self {
        let obj: Self = glib::Object::builder().build();
        let _ = obj.imp().path.set(path.to_path_buf());
        obj
    }

    pub fn path(&self) -> &std::path::Path {
        self.imp().path.get().map(|p: &PathBuf| p.as_path()).unwrap_or(std::path::Path::new(""))
    }
}

// ------------------------------------------------------------------
// Empty state
// ------------------------------------------------------------------

fn build_empty_state() -> gtk::Box {
    let container = gtk::Box::builder()
        .orientation(gtk::Orientation::Vertical)
        .vexpand(true)
        .hexpand(true)
        .build();

    let drop_zone = gtk::Box::builder()
        .orientation(gtk::Orientation::Vertical)
        .spacing(12)
        .halign(gtk::Align::Center)
        .valign(gtk::Align::Center)
        .vexpand(true)
        .margin_top(48)
        .margin_bottom(48)
        .margin_start(48)
        .margin_end(48)
        .build();
    drop_zone.add_css_class("card");
    drop_zone.update_property(&[
        gtk::accessible::Property::Label("Image drop zone. Drop images here or use the Browse Files button."),
    ]);

    let inner = gtk::Box::builder()
        .orientation(gtk::Orientation::Vertical)
        .spacing(12)
        .margin_top(48)
        .margin_bottom(48)
        .margin_start(64)
        .margin_end(64)
        .halign(gtk::Align::Center)
        .build();

    let icon = gtk::Image::builder()
        .icon_name("folder-pictures-symbolic")
        .pixel_size(64)
        .css_classes(["dim-label"])
        .build();
    icon.set_accessible_role(gtk::AccessibleRole::Presentation);

    let title = gtk::Label::builder()
        .label("Drop images here")
        .css_classes(["title-2"])
        .build();

    let subtitle = gtk::Label::builder()
        .label("or click Browse to select files.\nYou can also drop folders.")
        .css_classes(["dim-label"])
        .halign(gtk::Align::Center)
        .justify(gtk::Justification::Center)
        .build();

    let formats_label = gtk::Label::builder()
        .label("Supports all common image formats including RAW")
        .css_classes(["dim-label", "caption"])
        .halign(gtk::Align::Center)
        .margin_top(8)
        .build();

    let browse_button = gtk::Button::builder()
        .label("Browse Files")
        .tooltip_text("Add image files (Ctrl+O)")
        .halign(gtk::Align::Center)
        .action_name("win.add-files")
        .build();
    browse_button.add_css_class("suggested-action");
    browse_button.add_css_class("pill");

    let hint = gtk::Label::builder()
        .label("Start by adding your images below, then use the Next button to configure each processing step.")
        .css_classes(["dim-label", "caption"])
        .halign(gtk::Align::Center)
        .justify(gtk::Justification::Center)
        .wrap(true)
        .margin_bottom(8)
        .build();

    inner.append(&hint);
    inner.append(&icon);
    inner.append(&title);
    inner.append(&subtitle);
    inner.append(&formats_label);
    inner.append(&browse_button);
    drop_zone.append(&inner);

    container.append(&drop_zone);
    container
}

// ------------------------------------------------------------------
// Loaded state with thumbnail GridView
// ------------------------------------------------------------------

fn build_loaded_state(state: &AppState) -> gtk::Box {
    let container = gtk::Box::builder()
        .orientation(gtk::Orientation::Vertical)
        .spacing(0)
        .build();

    // Use the shared batch_updating flag from AppState
    let batch_updating = state.batch_updating.clone();

    // Toolbar
    let toolbar = gtk::Box::builder()
        .orientation(gtk::Orientation::Horizontal)
        .spacing(8)
        .margin_start(12)
        .margin_end(12)
        .margin_top(8)
        .margin_bottom(8)
        .build();

    let count_label = gtk::Label::builder()
        .label("0 images")
        .hexpand(true)
        .halign(gtk::Align::Start)
        .css_classes(["heading"])
        .build();

    let add_button = gtk::Button::builder()
        .icon_name("list-add-symbolic")
        .tooltip_text("Add more images (Ctrl+O)")
        .action_name("win.add-files")
        .build();
    add_button.add_css_class("flat");

    let select_all_button = gtk::Button::builder()
        .icon_name("edit-select-all-symbolic")
        .tooltip_text("Select all images (Ctrl+A)")
        .sensitive(false)
        .build();
    select_all_button.add_css_class("flat");
    select_all_button.update_property(&[
        gtk::accessible::Property::Label("Select all images for processing"),
    ]);

    let deselect_all_button = gtk::Button::builder()
        .icon_name("edit-clear-symbolic")
        .tooltip_text("Deselect all images (Ctrl+Shift+A)")
        .sensitive(false)
        .build();
    deselect_all_button.add_css_class("flat");
    deselect_all_button.update_property(&[
        gtk::accessible::Property::Label("Deselect all images from processing"),
    ]);

    let clear_button = gtk::Button::builder()
        .icon_name("edit-clear-all-symbolic")
        .tooltip_text("Remove all images")
        .sensitive(false)
        .build();
    clear_button.add_css_class("flat");
    clear_button.update_property(&[
        gtk::accessible::Property::Label("Remove all images from list"),
    ]);

    // Build the grid view model
    let store = gtk::gio::ListStore::new::<ImageItem>();
    let selection = gtk::NoSelection::new(Some(store.clone()));
    let factory = gtk::SignalListItemFactory::new();

    // Factory: setup
    {
        factory.connect_setup(move |_factory, list_item| {
            let Some(list_item) = list_item.downcast_ref::<gtk::ListItem>() else { return };

            let overlay = gtk::Overlay::builder()
                .width_request(THUMB_SIZE)
                .height_request(THUMB_SIZE)
                .build();

            let picture = gtk::Picture::builder()
                .content_fit(gtk::ContentFit::Cover)
                .width_request(THUMB_SIZE)
                .height_request(THUMB_SIZE)
                .build();
            picture.add_css_class("thumbnail-image");

            // Placeholder icon shown while loading
            let placeholder = gtk::Image::builder()
                .icon_name("image-x-generic-symbolic")
                .pixel_size(48)
                .css_classes(["dim-label"])
                .halign(gtk::Align::Center)
                .valign(gtk::Align::Center)
                .build();

            let thumb_stack = gtk::Stack::builder()
                .transition_type(gtk::StackTransitionType::Crossfade)
                .transition_duration(150)
                .width_request(THUMB_SIZE)
                .height_request(THUMB_SIZE)
                .build();
            thumb_stack.add_named(&placeholder, Some("placeholder"));
            thumb_stack.add_named(&picture, Some("picture"));
            thumb_stack.set_visible_child_name("placeholder");

            // Frame to clip rounded corners
            let frame = gtk::Frame::builder()
                .child(&thumb_stack)
                .overflow(gtk::Overflow::Hidden)
                .build();
            frame.add_css_class("thumbnail-frame");

            overlay.set_child(Some(&frame));

            // Checkbox overlay in top-left corner
            let check = gtk::CheckButton::builder()
                .active(true)
                .tooltip_text("Include in processing")
                .halign(gtk::Align::Start)
                .valign(gtk::Align::Start)
                .margin_start(4)
                .margin_top(4)
                .build();
            check.add_css_class("thumbnail-check");
            overlay.add_overlay(&check);

            // File name label at the bottom
            let name_label = gtk::Label::builder()
                .css_classes(["caption"])
                .halign(gtk::Align::Center)
                .ellipsize(gtk::pango::EllipsizeMode::Middle)
                .max_width_chars(14)
                .build();

            let vbox = gtk::Box::builder()
                .orientation(gtk::Orientation::Vertical)
                .spacing(4)
                .halign(gtk::Align::Center)
                .margin_top(4)
                .margin_bottom(4)
                .margin_start(4)
                .margin_end(4)
                .build();
            vbox.append(&overlay);
            vbox.append(&name_label);

            list_item.set_child(Some(&vbox));
        });
    }

    // Factory: bind
    {
        let excluded = state.excluded_files.clone();
        let loaded = state.loaded_files.clone();
        let cached_sizes = state.file_sizes.clone();
        let batch_flag = batch_updating.clone();
        factory.connect_bind(move |_factory, list_item| {
            let Some(list_item) = list_item.downcast_ref::<gtk::ListItem>() else { return };
            let Some(item) = list_item.item().and_downcast::<ImageItem>() else { return };
            let path = item.path().to_path_buf();

            let Some(vbox) = list_item.child().and_downcast::<gtk::Box>() else { return };
            let Some(overlay) = vbox.first_child().and_downcast::<gtk::Overlay>() else { return };
            let Some(name_label) = overlay.next_sibling().and_downcast::<gtk::Label>() else { return };

            // Set filename
            let file_name = path.file_name()
                .and_then(|n| n.to_str())
                .unwrap_or("unknown");
            name_label.set_label(file_name);

            // Get the frame -> stack -> picture
            let Some(frame) = overlay.child().and_downcast::<gtk::Frame>() else { return };
            let Some(thumb_stack) = frame.child().and_downcast::<gtk::Stack>() else { return };
            let Some(picture) = thumb_stack.child_by_name("picture")
                .and_downcast::<gtk::Picture>() else { return };

            // Reset to placeholder
            thumb_stack.set_visible_child_name("placeholder");

            // Bump bind generation so stale idle callbacks are ignored
            let bind_gen: u32 = unsafe {
                thumb_stack.data::<u32>("bind-gen")
                    .map(|p| *p.as_ref())
                    .unwrap_or(0)
                    .wrapping_add(1)
            };
            unsafe { thumb_stack.set_data("bind-gen", bind_gen); }

            // Load thumbnail in background thread to avoid blocking the UI
            let thumb_stack_c = thumb_stack.clone();
            let picture_c = picture.clone();
            let path_c = path.clone();
            let (tx, rx) = std::sync::mpsc::channel::<Option<Vec<u8>>>();
            std::thread::spawn(move || {
                let result = (|| -> Option<Vec<u8>> {
                    let img = image::open(&path_c).ok()?;
                    let thumb = img.resize(
                        (THUMB_SIZE * 2) as u32,
                        (THUMB_SIZE * 2) as u32,
                        image::imageops::FilterType::Triangle,
                    );
                    let mut buf = Vec::new();
                    thumb.write_to(
                        &mut std::io::Cursor::new(&mut buf),
                        image::ImageFormat::Png,
                    ).ok()?;
                    Some(buf)
                })();
                let _ = tx.send(result);
            });
            glib::timeout_add_local(std::time::Duration::from_millis(50), move || {
                let current: u32 = unsafe {
                    thumb_stack_c.data::<u32>("bind-gen")
                        .map(|p| *p.as_ref())
                        .unwrap_or(0)
                };
                if current != bind_gen {
                    return glib::ControlFlow::Break;
                }
                match rx.try_recv() {
                    Ok(Some(bytes)) => {
                        let gbytes = glib::Bytes::from(&bytes);
                        if let Ok(texture) = gtk::gdk::Texture::from_bytes(&gbytes) {
                            picture_c.set_paintable(Some(&texture));
                            thumb_stack_c.set_visible_child_name("picture");
                        }
                        glib::ControlFlow::Break
                    }
                    Ok(None) => glib::ControlFlow::Break, // decode failed, leave placeholder
                    Err(std::sync::mpsc::TryRecvError::Empty) => glib::ControlFlow::Continue,
                    Err(_) => glib::ControlFlow::Break,
                }
            });

            // Set accessible label on thumbnail picture
            picture.update_property(&[
                gtk::accessible::Property::Label(&format!("Thumbnail of {}", file_name)),
            ]);

            // Set checkbox state
            let check = find_check_button(overlay.upcast_ref::<gtk::Widget>());
            if let Some(ref check) = check {
                let is_excluded = excluded.borrow().contains(&path);
                check.set_active(!is_excluded);
                check.update_property(&[
                    gtk::accessible::Property::Label(&format!("Include {} in processing", file_name)),
                ]);

                // Wire checkbox toggle
                let excl = excluded.clone();
                let loaded_ref = loaded.clone();
                let sizes_ref = cached_sizes.clone();
                let batch_guard = batch_flag.clone();
                let file_path = path.clone();
                let handler_id = check.connect_toggled(move |btn| {
                    {
                        let mut excl = excl.borrow_mut();
                        if btn.is_active() {
                            excl.remove(&file_path);
                        } else {
                            excl.insert(file_path.clone());
                        }
                    }
                    // Skip per-toggle label update during batch select/deselect
                    if batch_guard.get() {
                        return;
                    }
                    // Update count label
                    if let Some(parent) = btn.ancestor(gtk::Stack::static_type())
                        && let Some(stack) = parent.downcast_ref::<gtk::Stack>()
                        && let Some(loaded_widget) = stack.child_by_name("loaded")
                    {
                        update_count_label(&loaded_widget, &loaded_ref, &excl, &sizes_ref);
                    }
                });
                // Store handler id so we can disconnect in unbind
                unsafe {
                    check.set_data("handler-id", handler_id);
                }
            }
        });
    }

    // Factory: unbind - disconnect signal to avoid stale closures
    {
        factory.connect_unbind(move |_factory, list_item| {
            let Some(list_item) = list_item.downcast_ref::<gtk::ListItem>() else { return };
            let Some(vbox) = list_item.child().and_downcast::<gtk::Box>() else { return };
            let Some(overlay) = vbox.first_child().and_downcast::<gtk::Overlay>() else { return };

            if let Some(check) = find_check_button(overlay.upcast_ref::<gtk::Widget>()) {
                let handler: Option<glib::SignalHandlerId> = unsafe {
                    check.steal_data("handler-id")
                };
                if let Some(id) = handler {
                    check.disconnect(id);
                }
            }
        });
    }

    let grid_view = gtk::GridView::builder()
        .model(&selection)
        .factory(&factory)
        .min_columns(2)
        .max_columns(10)
        .build();
    grid_view.add_css_class("thumbnail-grid");
    grid_view.update_property(&[
        gtk::accessible::Property::Label("Image thumbnail grid. Use arrow keys to navigate."),
    ]);

    let scrolled = gtk::ScrolledWindow::builder()
        .hscrollbar_policy(gtk::PolicyType::Never)
        .vexpand(true)
        .child(&grid_view)
        .build();

    // Wire clear button
    {
        let files = state.loaded_files.clone();
        let excl = state.excluded_files.clone();
        let count_label_c = count_label.clone();
        let store_c = store.clone();
        clear_button.connect_clicked(move |btn| {
            files.borrow_mut().clear();
            excl.borrow_mut().clear();
            store_c.remove_all();
            count_label_c.set_label("0 images");
            if let Some(parent) = btn.ancestor(gtk::Stack::static_type())
                && let Some(stack) = parent.downcast_ref::<gtk::Stack>()
            {
                stack.set_visible_child_name("empty");
            }
        });
    }

    // Wire Select All
    {
        let excl = state.excluded_files.clone();
        let loaded = state.loaded_files.clone();
        let sizes = state.file_sizes.clone();
        let batch_flag = batch_updating.clone();
        select_all_button.connect_clicked(move |btn| {
            excl.borrow_mut().clear();
            if let Some(parent) = btn.ancestor(gtk::Stack::static_type())
                && let Some(stack) = parent.downcast_ref::<gtk::Stack>()
                && let Some(loaded_widget) = stack.child_by_name("loaded")
            {
                batch_flag.set(true);
                set_all_checkboxes_in(&loaded_widget, true);
                batch_flag.set(false);
                update_count_label(&loaded_widget, &loaded, &excl, &sizes);
            }
        });
    }

    // Wire Deselect All
    {
        let excl = state.excluded_files.clone();
        let loaded = state.loaded_files.clone();
        let sizes = state.file_sizes.clone();
        let batch_flag = batch_updating.clone();
        deselect_all_button.connect_clicked(move |btn| {
            {
                let files = loaded.borrow();
                let mut excl_set = excl.borrow_mut();
                for f in files.iter() {
                    excl_set.insert(f.clone());
                }
            }
            if let Some(parent) = btn.ancestor(gtk::Stack::static_type())
                && let Some(stack) = parent.downcast_ref::<gtk::Stack>()
                && let Some(loaded_widget) = stack.child_by_name("loaded")
            {
                batch_flag.set(true);
                set_all_checkboxes_in(&loaded_widget, false);
                batch_flag.set(false);
                update_count_label(&loaded_widget, &loaded, &excl, &sizes);
            }
        });
    }

    toolbar.append(&count_label);
    toolbar.append(&select_all_button);
    toolbar.append(&deselect_all_button);
    toolbar.append(&add_button);
    toolbar.append(&clear_button);

    // Enable/disable toolbar buttons based on whether the store has items
    {
        let sa = select_all_button.clone();
        let da = deselect_all_button.clone();
        let cl = clear_button.clone();
        store.connect_items_changed(move |store, _, _, _| {
            let has_items = store.n_items() > 0;
            sa.set_sensitive(has_items);
            da.set_sensitive(has_items);
            cl.set_sensitive(has_items);
        });
    }

    toolbar.update_property(&[
        gtk::accessible::Property::Label("Image toolbar with count, selection, and add controls"),
    ]);

    let separator = gtk::Separator::new(gtk::Orientation::Horizontal);

    container.append(&toolbar);
    container.append(&separator);
    container.append(&scrolled);

    container
}

fn find_check_button(widget: &gtk::Widget) -> Option<gtk::CheckButton> {
    if let Some(cb) = widget.downcast_ref::<gtk::CheckButton>() {
        return Some(cb.clone());
    }
    let mut child = widget.first_child();
    while let Some(c) = child {
        if let Some(cb) = find_check_button(&c) {
            return Some(cb);
        }
        child = c.next_sibling();
    }
    None
}

/// Download an image from a URL to a temporary file
fn download_image_url(url: &str) -> Option<std::path::PathBuf> {
    let temp_dir = std::env::temp_dir().join("pixstrip-downloads");
    std::fs::create_dir_all(&temp_dir).ok()?;

    // Extract and sanitize filename from URL to prevent path traversal
    let url_path = url.split('?').next().unwrap_or(url);
    let raw_name = url_path
        .rsplit('/')
        .next()
        .unwrap_or("downloaded.jpg");
    let sanitized = std::path::Path::new(raw_name)
        .file_name()
        .and_then(|f| f.to_str())
        .unwrap_or("downloaded.jpg");
    let filename = if sanitized.is_empty() { "downloaded.jpg" } else { sanitized };

    // Avoid overwriting previous downloads with the same filename
    let dest = {
        let base = temp_dir.join(filename);
        if base.exists() {
            let stem = std::path::Path::new(filename).file_stem()
                .and_then(|s| s.to_str()).unwrap_or("downloaded");
            let ext = std::path::Path::new(filename).extension()
                .and_then(|e| e.to_str()).unwrap_or("jpg");
            let ts = std::time::SystemTime::now()
                .duration_since(std::time::UNIX_EPOCH)
                .map(|d| d.as_millis()).unwrap_or(0);
            temp_dir.join(format!("{}_{}.{}", stem, ts, ext))
        } else {
            base
        }
    };

    // Use GIO for the download (synchronous, runs in background thread)
    let gfile = gtk::gio::File::for_uri(url);
    let stream = gfile.read(gtk::gio::Cancellable::NONE).ok()?;

    const MAX_DOWNLOAD_BYTES: usize = 100 * 1024 * 1024; // 100 MB
    let mut buf = Vec::new();
    loop {
        let bytes = stream.read_bytes(8192, gtk::gio::Cancellable::NONE).ok()?;
        if bytes.is_empty() {
            break;
        }
        buf.extend_from_slice(&bytes);
        if buf.len() > MAX_DOWNLOAD_BYTES {
            return None;
        }
    }

    if buf.is_empty() {
        return None;
    }

    std::fs::write(&dest, &buf).ok()?;

    // Verify it's actually an image
    if is_image_file(&dest) {
        Some(dest)
    } else {
        let _ = std::fs::remove_file(&dest);
        None
    }
}

/// Set all CheckButton widgets within a container to a given state
pub fn set_all_checkboxes_in(widget: &gtk::Widget, active: bool) {
    if let Some(check) = widget.downcast_ref::<gtk::CheckButton>() {
        check.set_active(active);
        return;
    }
    let mut child = widget.first_child();
    while let Some(c) = child {
        set_all_checkboxes_in(&c, active);
        child = c.next_sibling();
    }
}
