use adw::prelude::*;
use gtk::glib;
use std::cell::RefCell;
use std::collections::HashMap;
use std::rc::Rc;
use std::sync::atomic::{AtomicBool, Ordering};
use std::sync::Arc;

use crate::step_indicator::StepIndicator;
use crate::steps;
use crate::utils::format_size;
use crate::wizard::WizardState;

pub const APP_ID: &str = "live.lashman.Pixstrip";

/// User's choices from the wizard steps, used to build the ProcessingJob
#[derive(Clone, Debug)]
pub struct JobConfig {
    // When true, skip all intermediate steps (2-8) - go straight from Images to Output
    pub preset_mode: bool,
    // Resize
    pub resize_enabled: bool,
    pub resize_width: u32,
    pub resize_height: u32,
    pub resize_mode: u32, // 0=exact, 1=fit within box
    pub allow_upscale: bool,
    pub resize_algorithm: u32,
    pub output_dpi: u32,
    // Adjustments
    pub adjustments_enabled: bool,
    pub rotation: u32,
    pub flip: u32,
    pub brightness: i32,
    pub contrast: i32,
    pub saturation: i32,
    pub sharpen: bool,
    pub grayscale: bool,
    pub sepia: bool,
    pub crop_aspect_ratio: u32,
    pub trim_whitespace: bool,
    pub canvas_padding: u32,
    // Convert
    pub convert_enabled: bool,
    pub convert_format: Option<pixstrip_core::types::ImageFormat>,
    pub progressive_jpeg: bool,
    pub format_mappings: HashMap<String, u32>,
    // Compress
    pub compress_enabled: bool,
    pub quality_preset: pixstrip_core::types::QualityPreset,
    pub jpeg_quality: u8,
    pub png_level: u8,
    pub webp_quality: u8,
    pub avif_quality: u8,
    pub webp_effort: u8,
    pub avif_speed: u8,
    // Metadata
    pub metadata_enabled: bool,
    pub metadata_mode: MetadataMode,
    pub strip_gps: bool,
    pub strip_camera: bool,
    pub strip_software: bool,
    pub strip_timestamps: bool,
    pub strip_copyright: bool,
    // Watermark
    pub watermark_enabled: bool,
    pub watermark_text: String,
    pub watermark_image_path: Option<std::path::PathBuf>,
    pub watermark_position: u32,
    pub watermark_opacity: f32,
    pub watermark_font_size: f32,
    pub watermark_color: [u8; 4],
    pub watermark_font_family: String,
    pub watermark_use_image: bool,
    pub watermark_tiled: bool,
    pub watermark_margin: u32,
    pub watermark_scale: f32,
    pub watermark_rotation: i32,
    // Rename
    pub rename_enabled: bool,
    pub rename_prefix: String,
    pub rename_suffix: String,
    pub rename_counter_enabled: bool,
    pub rename_counter_start: u32,
    pub rename_counter_padding: u32,
    pub rename_counter_position: u32, // 0=before prefix, 1=before name, 2=after name, 3=after suffix, 4=replace name
    pub rename_replace_spaces: u32,   // 0=none, 1=underscore, 2=hyphen, 3=dot, 4=camelcase, 5=remove
    pub rename_special_chars: u32,    // 0=keep all, 1=filesystem-safe, 2=web-safe, 3=hyphens+underscores, 4=hyphens only, 5=alphanumeric only
    pub rename_case: u32,             // 0=none, 1=lower, 2=upper, 3=title
    pub rename_template: String,
    pub rename_find: String,
    pub rename_replace: String,
    // Output
    pub preserve_dir_structure: bool,
    pub overwrite_behavior: u8,
}

#[derive(Clone, Debug, Default, PartialEq)]
pub enum MetadataMode {
    #[default]
    StripAll,
    Privacy,
    KeepAll,
    Custom,
}

/// Shared app state accessible from all UI callbacks
#[derive(Clone)]
pub struct AppState {
    pub wizard: Rc<RefCell<WizardState>>,
    pub loaded_files: Rc<RefCell<Vec<std::path::PathBuf>>>,
    pub excluded_files: Rc<RefCell<std::collections::HashSet<std::path::PathBuf>>>,
    pub file_sizes: Rc<RefCell<std::collections::HashMap<std::path::PathBuf, u64>>>,
    pub output_dir: Rc<RefCell<Option<std::path::PathBuf>>>,
    pub job_config: Rc<RefCell<JobConfig>>,
    pub detailed_mode: bool,
    pub batch_queue: Rc<RefCell<BatchQueue>>,
    pub expanded_sections: Rc<RefCell<std::collections::HashMap<String, bool>>>,
    /// Guard flag to suppress per-checkbox callbacks during bulk select/deselect
    pub batch_updating: Rc<std::cell::Cell<bool>>,
}

impl AppState {
    /// Get the expanded state for a named section, falling back to the skill level default
    pub fn is_section_expanded(&self, section: &str) -> bool {
        self.expanded_sections.borrow().get(section).copied().unwrap_or(self.detailed_mode)
    }

    /// Set and persist an expander's state
    pub fn set_section_expanded(&self, section: &str, expanded: bool) {
        self.expanded_sections.borrow_mut().insert(section.to_string(), expanded);
    }
}

/// A queued batch of images with their processing settings
#[derive(Clone, Debug)]
pub struct QueuedBatch {
    pub name: String,
    pub files: Vec<std::path::PathBuf>,
    pub output_dir: std::path::PathBuf,
    pub job_config: JobConfig,
    pub status: BatchStatus,
}

#[derive(Clone, Debug, PartialEq)]
pub enum BatchStatus {
    Pending,
    Processing,
    Completed,
    Failed(String),
}

/// Batch queue holding pending, active, and completed batches
#[derive(Clone, Debug, Default)]
pub struct BatchQueue {
    pub batches: Vec<QueuedBatch>,
}

#[derive(Clone)]
struct WizardUi {
    nav_view: adw::NavigationView,
    step_indicator: StepIndicator,
    back_button: gtk::Button,
    next_button: gtk::Button,
    title: adw::WindowTitle,
    pages: Vec<adw::NavigationPage>,
    toast_overlay: adw::ToastOverlay,
    queue_button: gtk::ToggleButton,
    queue_list_box: gtk::ListBox,
    state: AppState,
}

pub fn build_app() -> adw::Application {
    let app = adw::Application::builder()
        .application_id(APP_ID)
        .flags(gtk::gio::ApplicationFlags::HANDLES_OPEN)
        .build();

    app.connect_activate(build_ui);

    // Handle files opened via file manager integration or command line
    app.connect_open(|app, files, _hint| {
        // Ensure the window exists (activate first if needed)
        if app.active_window().is_none() {
            app.activate();
        }

        // Collect valid image file paths
        let image_paths: Vec<std::path::PathBuf> = files.iter()
            .filter_map(|f| f.path())
            .filter(|p| {
                p.extension()
                    .and_then(|e| e.to_str())
                    .is_some_and(|ext| {
                        matches!(
                            ext.to_lowercase().as_str(),
                            "jpg" | "jpeg" | "png" | "webp" | "avif" | "gif" | "tiff" | "tif" | "bmp"
                        )
                    })
            })
            .collect();

        if image_paths.is_empty() {
            return;
        }

        // Find the WizardUi through the window's action group
        if let Some(window) = app.active_window() {
            // Store files in a global channel for the UI to pick up
            if let Some(action) = window.downcast_ref::<adw::ApplicationWindow>()
                .and_then(|w| w.lookup_action("load-external-files"))
            {
                // Serialize paths as string variant
                let paths_str: String = image_paths.iter()
                    .map(|p| p.display().to_string())
                    .collect::<Vec<_>>()
                    .join("\n");
                if let Some(simple) = action.downcast_ref::<gtk::gio::SimpleAction>() {
                    simple.activate(Some(&paths_str.to_variant()));
                }
            }
        }
    });

    setup_shortcuts(&app);

    // App-level quit action
    let quit_action = gtk::gio::SimpleAction::new("quit", None);
    let app_clone = app.clone();
    quit_action.connect_activate(move |_, _| {
        app_clone.quit();
    });
    app.add_action(&quit_action);

    app
}

fn setup_shortcuts(app: &adw::Application) {
    app.set_accels_for_action("win.next-step", &["<Alt>Right"]);
    app.set_accels_for_action("win.prev-step", &["<Alt>Left", "Escape"]);
    app.set_accels_for_action("win.process", &["<Control>Return"]);
    for i in 1..=9 {
        app.set_accels_for_action(
            &format!("win.goto-step({})", i),
            &[&format!("<Alt>{}", i)],
        );
    }
    app.set_accels_for_action("win.add-files", &["<Control>o"]);
    app.set_accels_for_action("win.select-all-images", &["<Control>a"]);
    app.set_accels_for_action("win.deselect-all-images", &["<Control><Shift>a"]);
    app.set_accels_for_action("win.undo-last-batch", &["<Control>z"]);
    app.set_accels_for_action("win.paste-images", &["<Control>v"]);
    app.set_accels_for_action("app.quit", &["<Control>q"]);
    app.set_accels_for_action("win.show-settings", &["<Control>comma"]);
    app.set_accels_for_action("win.show-shortcuts", &["<Control>question", "F1"]);
}

fn load_css() {
    let provider = gtk::CssProvider::new();
    provider.load_from_string(
        r#"
        .thumbnail-frame {
            border-radius: 8px;
            background: @card_bg_color;
        }
        .thumbnail-grid {
            padding: 8px;
        }
        .thumbnail-check {
            opacity: 0.9;
        }
        "#,
    );
    gtk::style_context_add_provider_for_display(
        &gtk::gdk::Display::default().expect("Could not get default display"),
        &provider,
        gtk::STYLE_PROVIDER_PRIORITY_APPLICATION,
    );
}

fn parse_image_format(s: &str) -> Option<pixstrip_core::types::ImageFormat> {
    match s {
        "Jpeg" => Some(pixstrip_core::types::ImageFormat::Jpeg),
        "Png" => Some(pixstrip_core::types::ImageFormat::Png),
        "WebP" => Some(pixstrip_core::types::ImageFormat::WebP),
        "Avif" => Some(pixstrip_core::types::ImageFormat::Avif),
        "Gif" => Some(pixstrip_core::types::ImageFormat::Gif),
        "Tiff" => Some(pixstrip_core::types::ImageFormat::Tiff),
        _ => None,
    }
}

fn parse_quality_preset(s: &str) -> Option<pixstrip_core::types::QualityPreset> {
    match s {
        "Maximum" => Some(pixstrip_core::types::QualityPreset::Maximum),
        "High" => Some(pixstrip_core::types::QualityPreset::High),
        "Medium" => Some(pixstrip_core::types::QualityPreset::Medium),
        "Low" => Some(pixstrip_core::types::QualityPreset::Low),
        "WebOptimized" => Some(pixstrip_core::types::QualityPreset::WebOptimized),
        _ => None,
    }
}

fn parse_metadata_mode(s: &str) -> Option<MetadataMode> {
    match s {
        "StripAll" => Some(MetadataMode::StripAll),
        "Privacy" => Some(MetadataMode::Privacy),
        "KeepAll" => Some(MetadataMode::KeepAll),
        "Custom" => Some(MetadataMode::Custom),
        _ => None,
    }
}

fn build_ui(app: &adw::Application) {
    load_css();

    // Restore last-used wizard settings from session
    let sess = pixstrip_core::storage::SessionStore::new();
    let sess_state = sess.load().unwrap_or_default();
    let cfg_store = pixstrip_core::storage::ConfigStore::new();
    let app_cfg = cfg_store.load().unwrap_or_default();
    let remember = app_cfg.remember_settings;

    let app_state = AppState {
        wizard: Rc::new(RefCell::new(WizardState::new())),
        loaded_files: Rc::new(RefCell::new(Vec::new())),
        excluded_files: Rc::new(RefCell::new(std::collections::HashSet::new())),
        file_sizes: Rc::new(RefCell::new(std::collections::HashMap::new())),
        output_dir: Rc::new(RefCell::new(None)),
        detailed_mode: app_cfg.skill_level.is_advanced(),
        batch_queue: Rc::new(RefCell::new(BatchQueue::default())),
        expanded_sections: Rc::new(RefCell::new(sess_state.expanded_sections.clone())),
        batch_updating: Rc::new(std::cell::Cell::new(false)),
        job_config: Rc::new(RefCell::new(JobConfig {
            preset_mode: false,
            resize_enabled: if remember { sess_state.resize_enabled.unwrap_or(true) } else { true },
            resize_width: if remember { sess_state.resize_width.unwrap_or(1200) } else { 1200 },
            resize_height: if remember { sess_state.resize_height.unwrap_or(0) } else { 0 },
            resize_mode: 0,
            allow_upscale: false,
            resize_algorithm: 0,
            output_dpi: 72,
            adjustments_enabled: if remember { sess_state.adjustments_enabled.unwrap_or(false) } else { false },
            rotation: 0,
            flip: 0,
            brightness: 0,
            contrast: 0,
            saturation: 0,
            sharpen: false,
            grayscale: false,
            sepia: false,
            crop_aspect_ratio: 0,
            trim_whitespace: false,
            canvas_padding: 0,
            convert_enabled: if remember { sess_state.convert_enabled.unwrap_or(false) } else { false },
            convert_format: if remember {
                sess_state.convert_format.as_deref().and_then(parse_image_format)
            } else {
                None
            },
            progressive_jpeg: false,
            format_mappings: HashMap::new(),
            compress_enabled: if remember { sess_state.compress_enabled.unwrap_or(true) } else { true },
            quality_preset: if remember {
                sess_state.quality_preset.as_deref()
                    .and_then(parse_quality_preset)
                    .unwrap_or(pixstrip_core::types::QualityPreset::Medium)
            } else {
                pixstrip_core::types::QualityPreset::Medium
            },
            jpeg_quality: 85,
            png_level: 3,
            webp_quality: 80,
            avif_quality: 50,
            webp_effort: 4,
            avif_speed: 6,
            metadata_enabled: if remember { sess_state.metadata_enabled.unwrap_or(true) } else { true },
            metadata_mode: if remember {
                sess_state.metadata_mode.as_deref()
                    .and_then(parse_metadata_mode)
                    .unwrap_or(MetadataMode::StripAll)
            } else {
                MetadataMode::StripAll
            },
            strip_gps: true,
            strip_camera: true,
            strip_software: true,
            strip_timestamps: true,
            strip_copyright: true,
            watermark_enabled: if remember { sess_state.watermark_enabled.unwrap_or(false) } else { false },
            watermark_text: String::new(),
            watermark_image_path: None,
            watermark_position: 8, // BottomRight
            watermark_opacity: 0.5,
            watermark_font_size: 24.0,
            watermark_color: [255, 255, 255, 255],
            watermark_font_family: String::new(),
            watermark_use_image: false,
            watermark_tiled: false,
            watermark_margin: 10,
            watermark_scale: 20.0,
            watermark_rotation: 0,
            rename_enabled: if remember { sess_state.rename_enabled.unwrap_or(false) } else { false },
            rename_prefix: String::new(),
            rename_suffix: String::new(),
            rename_counter_enabled: false,
            rename_counter_start: 1,
            rename_counter_padding: 3,
            rename_counter_position: 3, // after suffix
            rename_replace_spaces: 0,
            rename_special_chars: 0,
            rename_case: 0,
            rename_template: String::new(),
            rename_find: String::new(),
            rename_replace: String::new(),
            preserve_dir_structure: false,
            overwrite_behavior: match app_cfg.overwrite_behavior {
                pixstrip_core::config::OverwriteBehavior::Ask => 0,
                pixstrip_core::config::OverwriteBehavior::AutoRename => 1,
                pixstrip_core::config::OverwriteBehavior::Overwrite => 2,
                pixstrip_core::config::OverwriteBehavior::Skip => 3,
            },
        })),
    };

    // Header bar
    let header = adw::HeaderBar::new();
    let title = adw::WindowTitle::new("Pixstrip", "Batch Image Processor");
    header.set_title_widget(Some(&title));

    // Queue toggle button
    let queue_button = gtk::ToggleButton::builder()
        .icon_name("view-list-symbolic")
        .tooltip_text("Batch Queue")
        .visible(false)
        .build();
    queue_button.add_css_class("flat");
    queue_button.update_property(&[
        gtk::accessible::Property::Label("Toggle batch queue panel"),
    ]);
    header.pack_start(&queue_button);

    // Help button for per-step contextual help
    let help_button = gtk::Button::builder()
        .icon_name("help-about-symbolic")
        .tooltip_text("Help for this step")
        .build();
    help_button.add_css_class("flat");
    help_button.update_property(&[
        gtk::accessible::Property::Label("Help for this step"),
    ]);
    help_button.set_widget_name("tour-help-button");
    header.pack_end(&help_button);

    // Hamburger menu
    let menu = build_menu();
    let menu_button = gtk::MenuButton::builder()
        .icon_name("open-menu-symbolic")
        .menu_model(&menu)
        .primary(true)
        .tooltip_text("Main Menu")
        .build();
    menu_button.set_widget_name("tour-menu-button");
    header.pack_end(&menu_button);

    // Step indicator
    let step_indicator = StepIndicator::new(&app_state.wizard.borrow().step_names());

    // Navigation view for wizard content
    let nav_view = adw::NavigationView::new();
    nav_view.set_widget_name("tour-content");
    nav_view.set_vexpand(true);
    nav_view.update_property(&[
        gtk::accessible::Property::Label("Wizard steps. Use Alt+Left/Right to navigate."),
    ]);

    // Build wizard pages
    let pages = crate::wizard::build_wizard_pages(&app_state);
    for page in &pages {
        nav_view.add(page);
    }

    // Bottom action bar
    let back_button = gtk::Button::builder()
        .label("Back")
        .tooltip_text("Go to previous step (Alt+Left)")
        .build();
    back_button.add_css_class("flat");

    let next_button = gtk::Button::builder()
        .label("Next")
        .tooltip_text("Go to next step (Alt+Right)")
        .build();
    next_button.add_css_class("suggested-action");
    next_button.set_widget_name("tour-next-button");

    let bottom_box = gtk::CenterBox::new();
    bottom_box.set_start_widget(Some(&back_button));
    bottom_box.set_end_widget(Some(&next_button));
    bottom_box.set_margin_start(12);
    bottom_box.set_margin_end(12);
    bottom_box.set_margin_top(6);
    bottom_box.set_margin_bottom(6);

    let bottom_bar = gtk::Box::new(gtk::Orientation::Vertical, 0);
    let separator = gtk::Separator::new(gtk::Orientation::Horizontal);
    bottom_bar.append(&separator);
    bottom_bar.append(&bottom_box);

    // Watch folders collapsible panel
    let watch_revealer = gtk::Revealer::builder()
        .transition_type(gtk::RevealerTransitionType::SlideUp)
        .reveal_child(false)
        .build();

    let watch_panel = build_watch_folder_panel();
    watch_revealer.set_child(Some(&watch_panel));

    // Watch folder toggle button in header
    let watch_button = gtk::ToggleButton::builder()
        .icon_name("folder-visiting-symbolic")
        .tooltip_text("Watch Folders")
        .build();
    watch_button.add_css_class("flat");
    watch_button.update_property(&[
        gtk::accessible::Property::Label("Toggle watch folders panel"),
    ]);
    header.pack_start(&watch_button);

    {
        let revealer = watch_revealer.clone();
        watch_button.connect_toggled(move |btn| {
            revealer.set_reveal_child(btn.is_active());
        });
    }

    // Main content layout
    let content_box = gtk::Box::new(gtk::Orientation::Vertical, 0);
    let indicator_scroll = gtk::ScrolledWindow::builder()
        .hscrollbar_policy(gtk::PolicyType::Automatic)
        .vscrollbar_policy(gtk::PolicyType::Never)
        .child(step_indicator.widget())
        .build();
    indicator_scroll.set_size_request(-1, 52);
    indicator_scroll.set_widget_name("tour-step-indicator");
    content_box.append(&indicator_scroll);
    content_box.append(&nav_view);
    content_box.append(&watch_revealer);

    // Queue side panel
    let (queue_panel, queue_list_box) = build_queue_panel(&app_state);

    // Overlay split view: queue sidebar + main content
    let split_view = adw::OverlaySplitView::builder()
        .sidebar(&queue_panel)
        .sidebar_position(gtk::PackType::End)
        .show_sidebar(false)
        .max_sidebar_width(300.0)
        .min_sidebar_width(250.0)
        .build();

    // Wire queue toggle button to show/hide sidebar
    {
        let sv = split_view.clone();
        queue_button.connect_toggled(move |btn| {
            sv.set_show_sidebar(btn.is_active());
        });
    }

    // Toolbar view with header and bottom bar
    let toolbar_view = adw::ToolbarView::new();
    toolbar_view.add_top_bar(&header);
    split_view.set_content(Some(&content_box));
    toolbar_view.set_content(Some(&split_view));
    toolbar_view.add_bottom_bar(&bottom_bar);

    // Toast overlay wraps everything for in-app notifications
    let toast_overlay = adw::ToastOverlay::new();
    toast_overlay.set_child(Some(&toolbar_view));

    // Restore window size from session
    let session = pixstrip_core::storage::SessionStore::new();
    let session_state = session.load().unwrap_or_default();
    let win_width = session_state.window_width.unwrap_or(900);
    let win_height = session_state.window_height.unwrap_or(700);

    let window = adw::ApplicationWindow::builder()
        .application(app)
        .default_width(win_width)
        .default_height(win_height)
        .content(&toast_overlay)
        .title("Pixstrip")
        .icon_name(APP_ID)
        .build();

    if session_state.window_maximized {
        window.maximize();
    }

    // Save window size and wizard settings on close
    {
        let app_state_for_close = app_state.clone();
        window.connect_close_request(move |win| {
            let session = pixstrip_core::storage::SessionStore::new();
            let mut state = session.load().unwrap_or_default();
            state.window_maximized = win.is_maximized();
            if !win.is_maximized() {
                let (w, h) = (win.default_size().0, win.default_size().1);
                if w > 0 && h > 0 {
                    state.window_width = Some(w);
                    state.window_height = Some(h);
                }
            }

            // Save last-used wizard settings if remember_settings is enabled
            let config_store = pixstrip_core::storage::ConfigStore::new();
            let config = config_store.load().unwrap_or_default();
            if config.remember_settings {
                let cfg = app_state_for_close.job_config.borrow();
                state.resize_enabled = Some(cfg.resize_enabled);
                state.resize_width = Some(cfg.resize_width);
                state.resize_height = Some(cfg.resize_height);
                state.adjustments_enabled = Some(cfg.adjustments_enabled);
                state.convert_enabled = Some(cfg.convert_enabled);
                state.convert_format = cfg.convert_format.map(|f| format!("{:?}", f));
                state.compress_enabled = Some(cfg.compress_enabled);
                state.quality_preset = Some(format!("{:?}", cfg.quality_preset));
                state.metadata_enabled = Some(cfg.metadata_enabled);
                state.metadata_mode = Some(format!("{:?}", cfg.metadata_mode));
                state.watermark_enabled = Some(cfg.watermark_enabled);
                state.rename_enabled = Some(cfg.rename_enabled);
            }

            // Always save expanded section states
            state.expanded_sections = app_state_for_close.expanded_sections.borrow().clone();

            let _ = session.save(&state);

            // Clean up temporary download directory
            let temp_downloads = std::env::temp_dir().join("pixstrip-downloads");
            if temp_downloads.exists() {
                let _ = std::fs::remove_dir_all(&temp_downloads);
            }

            // Clean up clipboard temp files on exit
            let temp_dir = std::env::temp_dir().join("pixstrip-clipboard");
            if temp_dir.is_dir() {
                let _ = std::fs::remove_dir_all(&temp_dir);
            }

            glib::Propagation::Proceed
        });
    }

    let ui = WizardUi {
        nav_view,
        step_indicator,
        back_button,
        next_button,
        title,
        pages,
        toast_overlay,
        queue_button,
        queue_list_box,
        state: app_state,
    };

    // Wire help button to show contextual help for current step
    {
        let wizard = ui.state.wizard.clone();
        let window_ref = window.clone();
        help_button.connect_clicked(move |_| {
            let step = wizard.borrow().current_step;
            show_step_help(&window_ref, step);
        });
    }

    setup_window_actions(&window, &ui);
    update_nav_buttons(
        &ui.state.wizard.borrow(),
        &ui.back_button,
        &ui.next_button,
    );
    ui.step_indicator.set_current(0);

    // Apply saved accessibility settings
    apply_accessibility_settings();

    window.present();

    crate::welcome::show_welcome_if_first_launch(&window);

    // Auto-show What's New dialog on first launch after update
    {
        let current_version = env!("CARGO_PKG_VERSION").to_string();
        let session = pixstrip_core::storage::SessionStore::new();
        let sess = session.load().unwrap_or_default();
        let last_seen = sess.last_seen_version.clone().unwrap_or_default();

        if last_seen != current_version {
            // Update stored version immediately
            let mut updated_sess = sess;
            updated_sess.last_seen_version = Some(current_version.clone());
            let _ = session.save(&updated_sess);

            // Only show dialog if this is not the very first run
            // (welcome wizard handles first run)
            let config_store = pixstrip_core::storage::ConfigStore::new();
            let config = config_store.load().unwrap_or_default();
            if config.first_run_complete && !last_seen.is_empty() {
                let w = window.clone();
                glib::idle_add_local_once(move || {
                    show_whats_new_dialog(&w);
                });
            }
        }
    }

    // Start watch folder monitoring for active folders
    start_watch_folder_monitoring(&ui);
}

fn start_watch_folder_monitoring(ui: &WizardUi) {
    let config_store = pixstrip_core::storage::ConfigStore::new();
    let config = config_store.load().unwrap_or_default();

    let active_folders: Vec<_> = config.watch_folders.iter()
        .filter(|f| f.active && f.path.exists())
        .cloned()
        .collect();

    if active_folders.is_empty() {
        return;
    }

    let (tx, rx) = std::sync::mpsc::channel::<pixstrip_core::watcher::WatchEvent>();

    // Start a watcher for each active folder, keeping them alive
    let mut watchers = Vec::new();
    for folder in &active_folders {
        let watcher = pixstrip_core::watcher::FolderWatcher::new();
        let folder_tx = tx.clone();
        if let Err(e) = watcher.start(folder, folder_tx) {
            eprintln!("Failed to start watching {}: {}", folder.path.display(), e);
            continue;
        }
        watchers.push(watcher);
    }

    // Build a lookup from folder path to preset name
    let folder_presets: std::collections::HashMap<std::path::PathBuf, String> = active_folders
        .iter()
        .map(|f| (f.path.clone(), f.preset_name.clone()))
        .collect();

    let toast_overlay = ui.toast_overlay.clone();

    // Poll the channel from the main loop
    // Move watchers into closure to keep them alive for the app lifetime
    glib::timeout_add_local(std::time::Duration::from_millis(500), move || {
        let _watchers = &watchers; // prevent drop
        let mut batch: Vec<(std::path::PathBuf, String)> = Vec::new();

        // Drain all pending events
        while let Ok(event) = rx.try_recv() {
            match event {
                pixstrip_core::watcher::WatchEvent::NewImage(path) => {
                    // Find which watch folder this belongs to
                    for (folder_path, preset_name) in &folder_presets {
                        if path.starts_with(folder_path) {
                            batch.push((path.clone(), preset_name.clone()));
                            break;
                        }
                    }
                }
                pixstrip_core::watcher::WatchEvent::Error(e) => {
                    eprintln!("Watch folder error: {}", e);
                }
            }
        }

        if !batch.is_empty() {
            // Group by preset name and process
            let preset_store = pixstrip_core::storage::PresetStore::new();
            let mut by_preset: std::collections::HashMap<String, Vec<std::path::PathBuf>> =
                std::collections::HashMap::new();
            for (path, preset) in batch {
                by_preset.entry(preset).or_default().push(path);
            }

            for (preset_name, files) in by_preset {
                if let Ok(presets) = preset_store.list() {
                    if let Some(preset) = presets.iter().find(|p| p.name == preset_name) {
                        let count = files.len();
                        let preset = preset.clone();
                        std::thread::spawn(move || {
                            // Build output dir next to the first file
                            let output_dir = files.first()
                                .and_then(|f| f.parent())
                                .map(|p| p.join("processed"))
                                .unwrap_or_else(|| std::path::PathBuf::from("processed"));
                            let input_dir = files.first()
                                .and_then(|f| f.parent())
                                .unwrap_or_else(|| std::path::Path::new("."))
                                .to_path_buf();
                            let mut job = preset.to_job(&input_dir, &output_dir);
                            for file in &files {
                                job.add_source(file);
                            }
                            let executor = pixstrip_core::executor::PipelineExecutor::new();
                            if let Err(e) = executor.execute(&job, |_| {}) {
                                eprintln!("Watch folder processing error: {}", e);
                            }
                        });

                        let toast = adw::Toast::new(&format!(
                            "Watch: processing {} new image{}",
                            count,
                            if count == 1 { "" } else { "s" }
                        ));
                        toast.set_timeout(3);
                        toast_overlay.add_toast(toast);
                    }
                }
            }
        }

        glib::ControlFlow::Continue
    });
}

fn build_menu() -> gtk::gio::Menu {
    let menu = gtk::gio::Menu::new();
    menu.append(Some("Settings"), Some("win.show-settings"));
    menu.append(Some("History"), Some("win.show-history"));
    menu.append(Some("Keyboard Shortcuts"), Some("win.show-shortcuts"));
    menu.append(Some("What's New"), Some("win.show-whats-new"));
    menu
}

fn setup_window_actions(window: &adw::ApplicationWindow, ui: &WizardUi) {
    let action_group = gtk::gio::SimpleActionGroup::new();

    // Next step action (skips disabled steps)
    {
        let ui = ui.clone();
        let action = gtk::gio::SimpleAction::new("next-step", None);
        action.connect_activate(move |_, _| {
            let mut s = ui.state.wizard.borrow_mut();
            if !s.can_go_next() {
                return;
            }
            let total = s.total_steps;
            let cfg = ui.state.job_config.borrow();
            let mut next = s.current_step + 1;
            while next < total && should_skip_step(next, &cfg) {
                next += 1;
            }
            drop(cfg);
            if next < total {
                s.current_step = next;
                s.visited[next] = true;
                let idx = s.current_step;
                drop(s);
                navigate_to_step(&ui, idx);
            }
        });
        action_group.add_action(&action);
    }

    // Previous step action (skips disabled steps)
    {
        let ui = ui.clone();
        let action = gtk::gio::SimpleAction::new("prev-step", None);
        action.connect_activate(move |_, _| {
            let mut s = ui.state.wizard.borrow_mut();
            if !s.can_go_back() {
                return;
            }
            let cfg = ui.state.job_config.borrow();
            let mut prev = s.current_step.saturating_sub(1);
            while prev > 0 && should_skip_step(prev, &cfg) {
                prev = prev.saturating_sub(1);
            }
            drop(cfg);
            s.current_step = prev;
            s.visited[prev] = true;
            let idx = s.current_step;
            drop(s);
            navigate_to_step(&ui, idx);
        });
        action_group.add_action(&action);
    }

    // Go to specific step action
    {
        let ui = ui.clone();
        let action = gtk::gio::SimpleAction::new(
            "goto-step",
            Some(&i32::static_variant_type()),
        );
        action.connect_activate(move |_, param| {
            if let Some(step) = param.and_then(|p| p.get::<i32>())
                && step >= 1
            {
                let target = (step - 1) as usize;
                let s = ui.state.wizard.borrow();
                let cfg = ui.state.job_config.borrow();
                if target < s.total_steps && s.visited[target] && !should_skip_step(target, &cfg) {
                    drop(cfg);
                    drop(s);
                    ui.state.wizard.borrow_mut().current_step = target;
                    navigate_to_step(&ui, target);
                }
            }
        });
        action_group.add_action(&action);
    }

    // Process action - runs the pipeline
    {
        let ui = ui.clone();
        let window = window.clone();
        let action = gtk::gio::SimpleAction::new("process", None);
        action.connect_activate(move |_, _| {
            let excluded = ui.state.excluded_files.borrow();
            let has_included = ui.state.loaded_files.borrow().iter().any(|p| !excluded.contains(p));
            drop(excluded);
            if !has_included {
                let toast = adw::Toast::new("No images selected - go to Step 2 to add images");
                ui.toast_overlay.add_toast(toast);
                return;
            }
            run_processing(&window, &ui);
        });
        action_group.add_action(&action);
    }

    // Add files action - opens file chooser
    {
        let window = window.clone();
        let ui = ui.clone();
        let action = gtk::gio::SimpleAction::new("add-files", None);
        action.connect_activate(move |_, _| {
            open_file_chooser(&window, &ui);
        });
        action_group.add_action(&action);
    }

    // Select all images action
    {
        let ui = ui.clone();
        let action = gtk::gio::SimpleAction::new("select-all-images", None);
        action.connect_activate(move |_, _| {
            ui.state.excluded_files.borrow_mut().clear();
            // Update the images step UI
            if let Some(page) = ui.pages.get(1)
                && let Some(stack) = page.child().and_downcast::<gtk::Stack>()
                && let Some(loaded) = stack.child_by_name("loaded")
            {
                ui.state.batch_updating.set(true);
                crate::steps::step_images::set_all_checkboxes_in(&loaded, true);
                ui.state.batch_updating.set(false);
                let files = ui.state.loaded_files.borrow();
                let count = files.len();
                drop(files);
                update_images_count_label(&ui, count);
            }
        });
        action_group.add_action(&action);
    }

    // Deselect all images action
    {
        let ui = ui.clone();
        let action = gtk::gio::SimpleAction::new("deselect-all-images", None);
        action.connect_activate(move |_, _| {
            {
                let files = ui.state.loaded_files.borrow();
                let mut excl = ui.state.excluded_files.borrow_mut();
                for f in files.iter() {
                    excl.insert(f.clone());
                }
            }
            // Update the images step UI
            if let Some(page) = ui.pages.get(1)
                && let Some(stack) = page.child().and_downcast::<gtk::Stack>()
                && let Some(loaded) = stack.child_by_name("loaded")
            {
                ui.state.batch_updating.set(true);
                crate::steps::step_images::set_all_checkboxes_in(&loaded, false);
                ui.state.batch_updating.set(false);
                let files = ui.state.loaded_files.borrow();
                let count = files.len();
                drop(files);
                update_images_count_label(&ui, count);
            }
        });
        action_group.add_action(&action);
    }

    // Settings action - opens settings dialog
    {
        let window = window.clone();
        let action = gtk::gio::SimpleAction::new("show-settings", None);
        action.connect_activate(move |_, _| {
            let dialog = crate::settings::build_settings_dialog();
            dialog.present(Some(&window));
        });
        action_group.add_action(&action);
    }

    // History action - shows history dialog
    {
        let window = window.clone();
        let action = gtk::gio::SimpleAction::new("show-history", None);
        action.connect_activate(move |_, _| {
            show_history_dialog(&window);
        });
        action_group.add_action(&action);
    }

    // Choose output directory action
    {
        let window = window.clone();
        let ui = ui.clone();
        let action = gtk::gio::SimpleAction::new("choose-output", None);
        action.connect_activate(move |_, _| {
            open_output_chooser(&window, &ui);
        });
        action_group.add_action(&action);
    }

    // Import preset action
    {
        let window = window.clone();
        let ui = ui.clone();
        let action = gtk::gio::SimpleAction::new("import-preset", None);
        action.connect_activate(move |_, _| {
            import_preset(&window, &ui);
        });
        action_group.add_action(&action);
    }

    // Save as preset action
    {
        let window = window.clone();
        let ui = ui.clone();
        let action = gtk::gio::SimpleAction::new("save-preset", None);
        action.connect_activate(move |_, _| {
            save_preset_dialog(&window, &ui);
        });
        action_group.add_action(&action);
    }

    // Undo last batch action
    {
        let ui = ui.clone();
        let action = gtk::gio::SimpleAction::new("undo-last-batch", None);
        action.connect_activate(move |_, _| {
            undo_last_batch(&ui);
        });
        action_group.add_action(&action);
    }

    // Keyboard shortcuts window
    {
        let window = window.clone();
        let action = gtk::gio::SimpleAction::new("show-shortcuts", None);
        action.connect_activate(move |_, _| {
            show_shortcuts_window(&window);
        });
        action_group.add_action(&action);
    }

    // What's New action
    {
        let window = window.clone();
        let action = gtk::gio::SimpleAction::new("show-whats-new", None);
        action.connect_activate(move |_, _| {
            show_whats_new_dialog(&window);
        });
        action_group.add_action(&action);
    }

    // Paste images from clipboard (Ctrl+V)
    {
        let window = window.clone();
        let ui = ui.clone();
        let action = gtk::gio::SimpleAction::new("paste-images", None);
        action.connect_activate(move |_, _| {
            paste_images_from_clipboard(&window, &ui);
        });
        action_group.add_action(&action);
    }

    // Connect button clicks
    ui.back_button.connect_clicked({
        let action_group = action_group.clone();
        move |_| {
            ActionGroupExt::activate_action(&action_group, "prev-step", None);
        }
    });

    ui.next_button.connect_clicked({
        let action_group = action_group.clone();
        let ui = ui.clone();
        move |btn| {
            // If the button says "Process More", reset the wizard
            if btn.label().as_deref() == Some("Process More") {
                reset_wizard(&ui);
                return;
            }
            let s = ui.state.wizard.borrow();
            if s.is_last_step() {
                drop(s);
                ActionGroupExt::activate_action(&action_group, "process", None);
            } else {
                drop(s);
                ActionGroupExt::activate_action(&action_group, "next-step", None);
            }
        }
    });

    // Load external files action - used by single-instance file open
    {
        let ui = ui.clone();
        let action = gtk::gio::SimpleAction::new("load-external-files", Some(&String::static_variant_type()));
        action.connect_activate(move |_, param| {
            if let Some(paths_str) = param.and_then(|v| v.get::<String>()) {
                let new_files: Vec<std::path::PathBuf> = paths_str
                    .lines()
                    .map(std::path::PathBuf::from)
                    .filter(|p| p.exists())
                    .collect();

                if !new_files.is_empty() {
                    let mut loaded = ui.state.loaded_files.borrow_mut();
                    let new_files: Vec<_> = new_files
                        .into_iter()
                        .filter(|p| !loaded.contains(p))
                        .collect();
                    let count = new_files.len();
                    loaded.extend(new_files);
                    drop(loaded);
                    ui.toast_overlay.add_toast(adw::Toast::new(
                        &format!("{} images added from file manager", count)
                    ));

                    // Navigate to step 2 (images) if we're on step 1 or earlier
                    let current = ui.state.wizard.borrow().current_step;
                    if current <= 1 {
                        {
                            let mut w = ui.state.wizard.borrow_mut();
                            w.current_step = 1;
                            if w.visited.len() > 1 {
                                w.visited[1] = true;
                            }
                        }
                        ui.step_indicator.set_current(1);
                        if let Some(page) = ui.pages.get(1) {
                            ui.nav_view.push(page);
                        }
                    }
                }
            }
        });
        action_group.add_action(&action);
    }

    window.insert_action_group("win", Some(&action_group));
}

fn should_skip_step(step: usize, cfg: &JobConfig) -> bool {
    match step {
        0 | 1 | 9 => false, // Workflow, Images, Output - always shown
        2..=8 if cfg.preset_mode => true, // Preset mode: skip all intermediate steps
        2 => !cfg.resize_enabled,
        3 => !cfg.adjustments_enabled,
        4 => !cfg.convert_enabled,
        5 => !cfg.compress_enabled,
        6 => !cfg.metadata_enabled,
        7 => !cfg.watermark_enabled,
        8 => !cfg.rename_enabled,
        _ => false,
    }
}

fn rebuild_step_indicator(ui: &WizardUi) {
    let cfg = ui.state.job_config.borrow();
    let all_names = [
        "Workflow", "Images", "Resize", "Adjustments", "Convert",
        "Compress", "Metadata", "Watermark", "Rename", "Output",
    ];
    let visible: Vec<(usize, String)> = all_names.iter().enumerate()
        .filter(|&(i, _)| !should_skip_step(i, &cfg))
        .map(|(i, name)| (i, name.to_string()))
        .collect();
    drop(cfg);
    ui.step_indicator.rebuild(&visible);
}

fn navigate_to_step(ui: &WizardUi, target: usize) {
    // Rebuild indicator: show all steps on Workflow, only relevant steps elsewhere
    if target == 0 {
        let all_names = [
            "Workflow", "Images", "Resize", "Adjustments", "Convert",
            "Compress", "Metadata", "Watermark", "Rename", "Output",
        ];
        let all: Vec<(usize, String)> = all_names.iter().enumerate()
            .map(|(i, name)| (i, name.to_string()))
            .collect();
        ui.step_indicator.rebuild(&all);
    } else {
        rebuild_step_indicator(ui);
    }

    let s = ui.state.wizard.borrow();

    // Update step indicator
    ui.step_indicator.set_current(target);
    for (i, visited) in s.visited.iter().enumerate() {
        if *visited && i != target {
            ui.step_indicator.set_completed(i);
        }
    }

    // Navigate - replace entire stack up to target
    if target < ui.pages.len() {
        ui.nav_view.replace(&ui.pages[..=target]);
        ui.title.set_subtitle(&ui.pages[target].title());

        // Announce step change for screen readers
        let step_name = ui.pages[target].title().to_string();
        let total_steps = ui.pages.len();
        ui.nav_view.update_property(&[
            gtk::accessible::Property::Label(
                &format!("Step {} of {}: {}", target + 1, total_steps, step_name)
            ),
        ]);

        // Focus management - move focus to first interactive element on new step
        // Use idle callback to let the page fully render first
        let page = ui.pages[target].clone();
        glib::idle_add_local_once(move || {
            page.child_focus(gtk::DirectionType::TabForward);
        });
    }

    // Update dynamic content on certain steps
    if target == 9 {
        // Output step - update image count, total size, and operation summary
        let files = ui.state.loaded_files.borrow();
        let excluded = ui.state.excluded_files.borrow();
        let included_count = files.iter().filter(|p| !excluded.contains(*p)).count();
        let total_size: u64 = files.iter()
            .filter(|p| !excluded.contains(*p))
            .filter_map(|p| std::fs::metadata(p).ok())
            .map(|m| m.len())
            .sum();
        drop(excluded);
        drop(files);
        if let Some(page) = ui.pages.get(9) {
            walk_widgets(&page.child(), &|widget| {
                if let Some(row) = widget.downcast_ref::<adw::ActionRow>()
                    && row.title().as_str() == "Images to process"
                {
                    row.set_subtitle(&format!("{} images ({})", included_count, format_size(total_size)));
                }
            });
        }
        update_output_summary(ui);
    }

    update_nav_buttons(&s, &ui.back_button, &ui.next_button);
}

fn update_nav_buttons(state: &WizardState, back_button: &gtk::Button, next_button: &gtk::Button) {
    back_button.set_sensitive(state.can_go_back());
    back_button.set_visible(state.current_step > 0);

    if state.is_last_step() {
        next_button.set_label("Process");
        next_button.remove_css_class("suggested-action");
        next_button.add_css_class("suggested-action");
        next_button.set_tooltip_text(Some("Start processing (Ctrl+Enter)"));
    } else {
        next_button.set_label("Next");
        next_button.add_css_class("suggested-action");
        next_button.set_tooltip_text(Some("Go to next step (Alt+Right)"));
    }
}

fn open_file_chooser(window: &adw::ApplicationWindow, ui: &WizardUi) {
    let dialog = gtk::FileDialog::builder()
        .title("Select Images")
        .modal(true)
        .build();

    let filter = gtk::FileFilter::new();
    filter.set_name(Some("Image files"));
    // Common raster formats
    filter.add_mime_type("image/jpeg");
    filter.add_mime_type("image/png");
    filter.add_mime_type("image/webp");
    filter.add_mime_type("image/avif");
    filter.add_mime_type("image/gif");
    filter.add_mime_type("image/tiff");
    filter.add_mime_type("image/bmp");
    // Note: ico/heic/svg excluded - not supported by processing pipeline
    filter.add_mime_type("image/x-tga");
    filter.add_mime_type("image/x-portable-anymap");
    filter.add_mime_type("image/x-portable-bitmap");
    filter.add_mime_type("image/x-portable-graymap");
    filter.add_mime_type("image/x-portable-pixmap");
    filter.add_mime_type("image/x-pcx");
    filter.add_mime_type("image/x-xpixmap");
    filter.add_mime_type("image/x-xbitmap");
    filter.add_mime_type("image/vnd.wap.wbmp");
    filter.add_mime_type("image/vnd.ms-dds");
    // HDR / EXR
    filter.add_mime_type("image/vnd.radiance");
    filter.add_mime_type("image/x-exr");
    // Modern formats
    filter.add_mime_type("image/jxl");
    filter.add_mime_type("image/heic");
    filter.add_mime_type("image/heif");
    filter.add_mime_type("image/jp2");
    filter.add_mime_type("image/jpx");
    filter.add_mime_type("image/x-qoi");
    // Vector
    filter.add_mime_type("image/svg+xml");
    // RAW camera formats
    filter.add_mime_type("image/x-canon-cr2");
    filter.add_mime_type("image/x-canon-cr3");
    filter.add_mime_type("image/x-nikon-nef");
    filter.add_mime_type("image/x-sony-arw");
    filter.add_mime_type("image/x-sony-srf");
    filter.add_mime_type("image/x-sony-sr2");
    filter.add_mime_type("image/x-olympus-orf");
    filter.add_mime_type("image/x-panasonic-rw2");
    filter.add_mime_type("image/x-fuji-raf");
    filter.add_mime_type("image/x-adobe-dng");
    filter.add_mime_type("image/x-pentax-pef");
    filter.add_mime_type("image/x-samsung-srw");
    filter.add_mime_type("image/x-sigma-x3f");
    // Catch-all pattern for any image type the system recognizes
    filter.add_mime_type("image/*");

    let filters = gtk::gio::ListStore::new::<gtk::FileFilter>();
    filters.append(&filter);
    dialog.set_filters(Some(&filters));
    dialog.set_default_filter(Some(&filter));

    let ui = ui.clone();
    dialog.open_multiple(Some(window), gtk::gio::Cancellable::NONE, move |result| {
        if let Ok(files) = result {
            let mut paths = ui.state.loaded_files.borrow_mut();
            for i in 0..files.n_items() {
                if let Some(file) = files.item(i)
                    && let Some(gfile) = file.downcast_ref::<gtk::gio::File>()
                    && let Some(path) = gfile.path()
                    && !paths.contains(&path)
                {
                    paths.push(path);
                }
            }
            let count = paths.len();
            drop(paths);

            // Update the images step UI if we can find the label
            update_images_count_label(&ui, count);
        }
    });
}

fn open_output_chooser(window: &adw::ApplicationWindow, ui: &WizardUi) {
    let dialog = gtk::FileDialog::builder()
        .title("Choose Output Folder")
        .modal(true)
        .build();

    let ui = ui.clone();
    dialog.select_folder(Some(window), gtk::gio::Cancellable::NONE, move |result| {
        if let Ok(folder) = result
            && let Some(path) = folder.path()
        {
            *ui.state.output_dir.borrow_mut() = Some(path.clone());
            update_output_label(&ui, &path);
        }
    });
}

fn update_output_label(ui: &WizardUi, path: &std::path::Path) {
    // Find the output step page (index 9) and update the output location subtitle
    if let Some(page) = ui.pages.get(9) {
        walk_widgets(&page.child(), &|widget| {
            if let Some(row) = widget.downcast_ref::<adw::ActionRow>()
                && row.title().as_str() == "Output Location"
            {
                row.set_subtitle(&path.display().to_string());
            }
        });
    }
}

fn update_images_count_label(ui: &WizardUi, count: usize) {
    // Find the step-images page and switch its stack to "loaded" if we have files
    if let Some(page) = ui.pages.get(1)
        && let Some(stack) = page.child().and_downcast::<gtk::Stack>()
    {
        if count > 0 {
            stack.set_visible_child_name("loaded");
        } else {
            stack.set_visible_child_name("empty");
        }
        if let Some(loaded_box) = stack.child_by_name("loaded") {
            steps::step_images::rebuild_grid_model(
                &loaded_box,
                &ui.state.loaded_files,
                &ui.state.excluded_files,
                &ui.state.file_sizes,
            );
        }
    }
}

fn show_history_dialog(window: &adw::ApplicationWindow) {
    let dialog = adw::Dialog::builder()
        .title("Processing History")
        .content_width(500)
        .content_height(400)
        .build();

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

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

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

    let history = pixstrip_core::storage::HistoryStore::new();
    match history.list() {
        Ok(entries) if entries.is_empty() => {
            let empty = adw::StatusPage::builder()
                .title("No History Yet")
                .description("Processed batches will appear here")
                .icon_name("document-open-recent-symbolic")
                .vexpand(true)
                .build();
            content.append(&empty);
        }
        Ok(entries) => {
            let group = adw::PreferencesGroup::builder()
                .title("Recent Batches")
                .margin_start(12)
                .margin_end(12)
                .margin_top(12)
                .build();

            for entry in entries.iter().rev() {
                let savings = if entry.total_input_bytes > 0 {
                    let pct = (1.0
                        - entry.total_output_bytes as f64 / entry.total_input_bytes as f64)
                        * 100.0;
                    format!("{:.0}% saved", pct)
                } else {
                    "N/A".into()
                };

                let subtitle = format!(
                    "{}/{} succeeded - {} - {}",
                    entry.succeeded,
                    entry.total,
                    savings,
                    format_duration(entry.elapsed_ms)
                );

                let preset_label = entry
                    .preset_name
                    .as_deref()
                    .unwrap_or("Custom workflow");

                // Format timestamp
                let time_label = entry.timestamp.parse::<u64>().ok().and_then(|ts| {
                    let dt = glib::DateTime::from_unix_local(ts as i64).ok()?;
                    let formatted = dt.format("%Y-%m-%d %H:%M").ok()?;
                    Some(formatted.to_string())
                }).unwrap_or_else(|| "Unknown date".to_string());

                let row = adw::ExpanderRow::builder()
                    .title(preset_label)
                    .subtitle(&format!("{} - {}", time_label, subtitle))
                    .show_enable_switch(false)
                    .build();
                let history_icon = gtk::Image::from_icon_name("image-x-generic-symbolic");
                history_icon.set_accessible_role(gtk::AccessibleRole::Presentation);
                row.add_prefix(&history_icon);

                // Detail rows inside expander
                let input_row = adw::ActionRow::builder()
                    .title("Input")
                    .subtitle(&entry.input_dir)
                    .build();
                let input_icon = gtk::Image::from_icon_name("folder-symbolic");
                input_icon.set_accessible_role(gtk::AccessibleRole::Presentation);
                input_row.add_prefix(&input_icon);
                row.add_row(&input_row);

                let output_row = adw::ActionRow::builder()
                    .title("Output")
                    .subtitle(&entry.output_dir)
                    .activatable(true)
                    .build();
                let output_icon = gtk::Image::from_icon_name("folder-open-symbolic");
                output_icon.set_accessible_role(gtk::AccessibleRole::Presentation);
                output_row.add_prefix(&output_icon);
                let out_dir = entry.output_dir.clone();
                output_row.connect_activated(move |_| {
                    let uri = gtk::gio::File::for_path(&out_dir).uri();
                    let _ = gtk::gio::AppInfo::launch_default_for_uri(
                        &uri,
                        gtk::gio::AppLaunchContext::NONE,
                    );
                });
                row.add_row(&output_row);

                let size_row = adw::ActionRow::builder()
                    .title("Size")
                    .subtitle(&format!(
                        "{} -> {} ({})",
                        format_size(entry.total_input_bytes),
                        format_size(entry.total_output_bytes),
                        savings
                    ))
                    .build();
                let size_icon = gtk::Image::from_icon_name("drive-harddisk-symbolic");
                size_icon.set_accessible_role(gtk::AccessibleRole::Presentation);
                size_row.add_prefix(&size_icon);
                row.add_row(&size_row);

                if entry.failed > 0 {
                    let err_row = adw::ActionRow::builder()
                        .title("Errors")
                        .subtitle(&format!("{} files failed", entry.failed))
                        .build();
                    let err_icon = gtk::Image::from_icon_name("dialog-warning-symbolic");
                    err_icon.set_accessible_role(gtk::AccessibleRole::Presentation);
                    err_row.add_prefix(&err_icon);
                    row.add_row(&err_row);
                }

                // Action buttons row
                let actions_row = adw::ActionRow::builder()
                    .title("Actions")
                    .build();

                // Undo button - moves output files to trash
                if !entry.output_files.is_empty() {
                    let undo_btn = gtk::Button::builder()
                        .icon_name("edit-undo-symbolic")
                        .tooltip_text("Undo - move outputs to trash")
                        .valign(gtk::Align::Center)
                        .build();
                    undo_btn.add_css_class("flat");
                    let files = entry.output_files.clone();
                    undo_btn.connect_clicked(move |btn| {
                        let mut trashed = 0;
                        for file_path in &files {
                            let gfile = gtk::gio::File::for_path(file_path);
                            if gfile.trash(gtk::gio::Cancellable::NONE).is_ok() {
                                trashed += 1;
                            }
                        }
                        btn.set_sensitive(false);
                        btn.set_tooltip_text(Some(&format!("{} files moved to trash", trashed)));
                    });
                    actions_row.add_suffix(&undo_btn);
                }

                // Open output folder button
                let open_btn = gtk::Button::builder()
                    .icon_name("folder-open-symbolic")
                    .tooltip_text("Open output folder")
                    .valign(gtk::Align::Center)
                    .build();
                open_btn.add_css_class("flat");
                let out_dir2 = entry.output_dir.clone();
                open_btn.connect_clicked(move |_| {
                    let uri = gtk::gio::File::for_path(&out_dir2).uri();
                    let _ = gtk::gio::AppInfo::launch_default_for_uri(
                        &uri,
                        gtk::gio::AppLaunchContext::NONE,
                    );
                });
                actions_row.add_suffix(&open_btn);

                row.add_row(&actions_row);
                group.add(&row);
            }
            content.append(&group);

            // Clear history button
            let clear_btn = gtk::Button::builder()
                .label("Clear History")
                .halign(gtk::Align::Center)
                .margin_top(12)
                .margin_bottom(12)
                .build();
            clear_btn.add_css_class("destructive-action");
            clear_btn.connect_clicked(move |btn| {
                let history = pixstrip_core::storage::HistoryStore::new();
                let _ = history.clear();
                btn.set_label("History Cleared");
                btn.set_sensitive(false);
            });
            content.append(&clear_btn);
        }
        Err(e) => {
            let error = adw::StatusPage::builder()
                .title("Could not load history")
                .description(e.to_string())
                .icon_name("dialog-error-symbolic")
                .vexpand(true)
                .build();
            content.append(&error);
        }
    }

    scrolled.set_child(Some(&content));
    toolbar_view.set_content(Some(&scrolled));
    dialog.set_child(Some(&toolbar_view));
    dialog.present(Some(window));
}

fn show_whats_new_dialog(window: &adw::ApplicationWindow) {
    let dialog = adw::Dialog::builder()
        .title("What's New")
        .content_width(450)
        .content_height(350)
        .build();

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

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

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

    let version_label = gtk::Label::builder()
        .label("Pixstrip 0.1.0")
        .css_classes(["title-1"])
        .halign(gtk::Align::Start)
        .build();

    let subtitle_label = gtk::Label::builder()
        .label("First release")
        .css_classes(["dim-label"])
        .halign(gtk::Align::Start)
        .build();

    content.append(&version_label);
    content.append(&subtitle_label);

    let changes_group = adw::PreferencesGroup::builder()
        .title("Changes in this version")
        .build();

    let features = [
        ("Wizard workflow", "Step-by-step batch image processing"),
        ("Resize", "Width/height, social media presets, fit-in-box"),
        ("Convert", "JPEG, PNG, WebP, AVIF, GIF, TIFF conversion"),
        ("Compress", "Quality presets with per-format controls"),
        ("Metadata", "Strip all, privacy mode, photographer mode"),
        ("Watermark", "Text and image watermarks with positioning"),
        ("Rename", "Prefix, suffix, counter, template engine"),
        ("Adjustments", "Brightness, contrast, crop, trim, effects"),
    ];

    for (title, subtitle) in &features {
        let row = adw::ActionRow::builder()
            .title(*title)
            .subtitle(*subtitle)
            .build();
        row.add_prefix(&gtk::Image::from_icon_name("emblem-ok-symbolic"));
        changes_group.add(&row);
    }

    content.append(&changes_group);

    scrolled.set_child(Some(&content));
    toolbar_view.set_content(Some(&scrolled));
    dialog.set_child(Some(&toolbar_view));
    dialog.present(Some(window));
}

fn run_processing(_window: &adw::ApplicationWindow, ui: &WizardUi) {
    let excluded = ui.state.excluded_files.borrow().clone();
    let files: Vec<std::path::PathBuf> = ui.state.loaded_files.borrow()
        .iter()
        .filter(|p| !excluded.contains(*p))
        .cloned()
        .collect();
    if files.is_empty() {
        return;
    }

    let input_dir = {
        let first_parent = files[0]
            .parent()
            .unwrap_or_else(|| std::path::Path::new("."))
            .to_path_buf();
        if files.len() == 1 {
            first_parent
        } else {
            // Find common ancestor of all files
            let mut common = first_parent.clone();
            for f in &files[1..] {
                let p = f.parent().unwrap_or_else(|| std::path::Path::new("."));
                while !p.starts_with(&common) {
                    if !common.pop() {
                        break;
                    }
                }
            }
            if common.as_os_str().is_empty() {
                first_parent
            } else {
                common
            }
        }
    };

    let output_dir = ui
        .state
        .output_dir
        .borrow()
        .clone()
        .unwrap_or_else(|| input_dir.join("processed"));

    // Build job from wizard settings
    let mut job = pixstrip_core::pipeline::ProcessingJob::new(&input_dir, &output_dir);

    let cfg = ui.state.job_config.borrow();

    if cfg.resize_enabled && cfg.resize_width > 0 {
        let target_w = cfg.resize_width;
        let target_h = cfg.resize_height;
        if target_h == 0 {
            job.resize = Some(pixstrip_core::operations::ResizeConfig::ByWidth(target_w));
        } else if cfg.resize_mode == 1 {
            job.resize = Some(pixstrip_core::operations::ResizeConfig::FitInBox {
                max: pixstrip_core::types::Dimensions { width: target_w, height: target_h },
                allow_upscale: cfg.allow_upscale,
            });
        } else {
            job.resize = Some(pixstrip_core::operations::ResizeConfig::Exact(
                pixstrip_core::types::Dimensions { width: target_w, height: target_h },
            ));
        }
        job.resize_algorithm = match cfg.resize_algorithm {
            1 => pixstrip_core::operations::ResizeAlgorithm::CatmullRom,
            2 => pixstrip_core::operations::ResizeAlgorithm::Bilinear,
            3 => pixstrip_core::operations::ResizeAlgorithm::Nearest,
            _ => pixstrip_core::operations::ResizeAlgorithm::Lanczos3,
        };
    }

    if cfg.convert_enabled {
        // Check if any per-format mappings are set (non-zero = overridden)
        let has_mapping = cfg.format_mappings.values().any(|&v| v > 0);

        if has_mapping {
            // Dropdown order: 0=Same as above, 1=Keep Original,
            // 2=JPEG, 3=PNG, 4=WebP, 5=AVIF, 6=GIF, 7=TIFF
            let mapping_to_format = |idx: u32, default: Option<pixstrip_core::types::ImageFormat>| -> Option<pixstrip_core::types::ImageFormat> {
                match idx {
                    1 => None, // Keep Original
                    2 => Some(pixstrip_core::types::ImageFormat::Jpeg),
                    3 => Some(pixstrip_core::types::ImageFormat::Png),
                    4 => Some(pixstrip_core::types::ImageFormat::WebP),
                    5 => Some(pixstrip_core::types::ImageFormat::Avif),
                    6 => Some(pixstrip_core::types::ImageFormat::Gif),
                    7 => Some(pixstrip_core::types::ImageFormat::Tiff),
                    _ => default, // 0 = "Same as above" - use global format
                }
            };

            let global = cfg.convert_format;
            let mut map = Vec::new();

            for (ext, &mapping_idx) in &cfg.format_mappings {
                if let Some(input_fmt) = pixstrip_core::types::ImageFormat::from_extension(ext) {
                    if let Some(output_fmt) = mapping_to_format(mapping_idx, global) {
                        if output_fmt != input_fmt {
                            map.push((input_fmt, output_fmt));
                        }
                    }
                }
            }

            if !map.is_empty() {
                job.convert = Some(pixstrip_core::operations::ConvertConfig::FormatMapping(map));
            }
        } else if let Some(fmt) = cfg.convert_format {
            job.convert = Some(pixstrip_core::operations::ConvertConfig::SingleFormat(fmt));
        }
    }

    if cfg.compress_enabled {
        // Check if user has customized per-format quality values beyond the preset defaults
        let preset_jpeg = cfg.quality_preset.jpeg_quality();
        let preset_webp = cfg.quality_preset.webp_quality();
        let preset_avif = cfg.quality_preset.avif_quality();
        let preset_avif_speed = cfg.quality_preset.avif_speed();
        let preset_webp_effort = cfg.quality_preset.webp_effort();
        let has_custom = cfg.jpeg_quality != preset_jpeg
            || cfg.webp_quality != preset_webp as u8
            || cfg.avif_quality != preset_avif
            || cfg.avif_speed != preset_avif_speed
            || cfg.webp_effort != preset_webp_effort;

        if has_custom {
            job.compress = Some(pixstrip_core::operations::CompressConfig::Custom {
                jpeg_quality: Some(cfg.jpeg_quality),
                png_level: Some(cfg.png_level),
                webp_quality: Some(cfg.webp_quality as f32),
                avif_quality: Some(cfg.avif_quality as f32),
            });
        } else {
            job.compress = Some(pixstrip_core::operations::CompressConfig::Preset(cfg.quality_preset));
        }
    }

    // Pass encoder options to the job
    job.progressive_jpeg = cfg.progressive_jpeg;
    job.avif_speed = cfg.avif_speed;

    if cfg.metadata_enabled {
        job.metadata = Some(match cfg.metadata_mode {
            MetadataMode::StripAll => pixstrip_core::operations::MetadataConfig::StripAll,
            MetadataMode::Privacy => pixstrip_core::operations::MetadataConfig::Privacy,
            MetadataMode::KeepAll => pixstrip_core::operations::MetadataConfig::KeepAll,
            MetadataMode::Custom => pixstrip_core::operations::MetadataConfig::Custom {
                strip_gps: cfg.strip_gps,
                strip_camera: cfg.strip_camera,
                strip_software: cfg.strip_software,
                strip_timestamps: cfg.strip_timestamps,
                strip_copyright: cfg.strip_copyright,
            },
        });
    }

    // Rotation and Flip apply from the resize step, so enable when either resize or adjustments is active
    if cfg.resize_enabled || cfg.adjustments_enabled {
        job.rotation = Some(match cfg.rotation {
            1 => pixstrip_core::operations::Rotation::Cw90,
            2 => pixstrip_core::operations::Rotation::Cw180,
            3 => pixstrip_core::operations::Rotation::Cw270,
            4 => pixstrip_core::operations::Rotation::AutoOrient,
            _ => pixstrip_core::operations::Rotation::None,
        });

        job.flip = Some(match cfg.flip {
            1 => pixstrip_core::operations::Flip::Horizontal,
            2 => pixstrip_core::operations::Flip::Vertical,
            _ => pixstrip_core::operations::Flip::None,
        });
    }

    // Adjustments (brightness, contrast, etc.)
    if cfg.adjustments_enabled {
        let crop = match cfg.crop_aspect_ratio {
            1 => Some((1.0, 1.0)),
            2 => Some((4.0, 3.0)),
            3 => Some((3.0, 2.0)),
            4 => Some((16.0, 9.0)),
            5 => Some((9.0, 16.0)),
            6 => Some((3.0, 4.0)),
            7 => Some((2.0, 3.0)),
            _ => None,
        };
        let adj = pixstrip_core::operations::AdjustmentsConfig {
            brightness: cfg.brightness,
            contrast: cfg.contrast,
            saturation: cfg.saturation,
            sharpen: cfg.sharpen,
            grayscale: cfg.grayscale,
            sepia: cfg.sepia,
            crop_aspect_ratio: crop,
            trim_whitespace: cfg.trim_whitespace,
            canvas_padding: cfg.canvas_padding,
        };
        if !adj.is_noop() {
            job.adjustments = Some(adj);
        }
    }

    // Watermark
    if cfg.watermark_enabled {
        let position = match cfg.watermark_position {
            0 => pixstrip_core::operations::WatermarkPosition::TopLeft,
            1 => pixstrip_core::operations::WatermarkPosition::TopCenter,
            2 => pixstrip_core::operations::WatermarkPosition::TopRight,
            3 => pixstrip_core::operations::WatermarkPosition::MiddleLeft,
            4 => pixstrip_core::operations::WatermarkPosition::Center,
            5 => pixstrip_core::operations::WatermarkPosition::MiddleRight,
            6 => pixstrip_core::operations::WatermarkPosition::BottomLeft,
            7 => pixstrip_core::operations::WatermarkPosition::BottomCenter,
            _ => pixstrip_core::operations::WatermarkPosition::BottomRight,
        };

        let wm_rotation = if cfg.watermark_rotation != 0 {
            Some(pixstrip_core::operations::WatermarkRotation::Custom(cfg.watermark_rotation as f32))
        } else {
            None
        };

        if cfg.watermark_use_image {
            if let Some(ref path) = cfg.watermark_image_path {
                job.watermark = Some(pixstrip_core::operations::WatermarkConfig::Image {
                    path: path.clone(),
                    position,
                    opacity: cfg.watermark_opacity,
                    scale: cfg.watermark_scale / 100.0,
                    rotation: wm_rotation,
                    tiled: cfg.watermark_tiled,
                    margin: cfg.watermark_margin,
                });
            }
        } else if !cfg.watermark_text.is_empty() {
            job.watermark = Some(pixstrip_core::operations::WatermarkConfig::Text {
                text: cfg.watermark_text.clone(),
                position,
                font_size: cfg.watermark_font_size,
                opacity: cfg.watermark_opacity,
                color: cfg.watermark_color,
                font_family: if cfg.watermark_font_family.is_empty() { None } else { Some(cfg.watermark_font_family.clone()) },
                rotation: wm_rotation,
                tiled: cfg.watermark_tiled,
                margin: cfg.watermark_margin,
            });
        }
    }

    // Rename
    if cfg.rename_enabled {
        job.rename = Some(pixstrip_core::operations::RenameConfig {
            prefix: cfg.rename_prefix.clone(),
            suffix: cfg.rename_suffix.clone(),
            counter_start: cfg.rename_counter_start,
            counter_padding: cfg.rename_counter_padding,
            counter_enabled: cfg.rename_counter_enabled,
            counter_position: cfg.rename_counter_position,
            template: if cfg.rename_template.is_empty() {
                None
            } else {
                Some(cfg.rename_template.clone())
            },
            case_mode: cfg.rename_case,
            replace_spaces: cfg.rename_replace_spaces,
            special_chars: cfg.rename_special_chars,
            regex_find: cfg.rename_find.clone(),
            regex_replace: cfg.rename_replace.clone(),
        });
    }

    job.preserve_directory_structure = cfg.preserve_dir_structure;
    job.output_dpi = cfg.output_dpi;
    let ask_overwrite = cfg.overwrite_behavior == 0;
    job.overwrite_behavior = match cfg.overwrite_behavior {
        1 => pixstrip_core::operations::OverwriteAction::AutoRename,
        2 => pixstrip_core::operations::OverwriteAction::Overwrite,
        3 => pixstrip_core::operations::OverwriteAction::Skip,
        _ => pixstrip_core::operations::OverwriteAction::AutoRename,
    };

    drop(cfg);

    for file in &files {
        job.add_source(file);
    }

    // Check for existing output files when "Ask" overwrite behavior is set.
    // Skip check if rename or format conversion will actually change output names.
    let has_rename_or_convert = job.rename.as_ref().is_some_and(|r| r.changes_filename())
        || job.convert.as_ref().is_some_and(|c| c.changes_extension());
    if ask_overwrite && !has_rename_or_convert {
        let output_dir = ui.state.output_dir.borrow().clone()
            .unwrap_or_else(|| {
                files[0].parent()
                    .unwrap_or_else(|| std::path::Path::new("."))
                    .join("processed")
            });
        if output_dir.exists() {
            let conflicts: Vec<String> = files.iter()
                .filter_map(|f| {
                    let name = f.file_name()?;
                    let out_path = if job.preserve_directory_structure {
                        if let Ok(rel) = f.strip_prefix(&job.input_dir) {
                            if let Some(parent) = rel.parent() {
                                output_dir.join(parent).join(name)
                            } else {
                                output_dir.join(name)
                            }
                        } else {
                            output_dir.join(name)
                        }
                    } else {
                        output_dir.join(name)
                    };
                    if out_path.exists() { Some(name.to_string_lossy().into()) } else { None }
                })
                .take(10)
                .collect();

            if !conflicts.is_empty() {
                let msg = if conflicts.len() == 10 {
                    format!("{} files already exist in the output folder (showing first 10):\n\n{}", conflicts.len(), conflicts.join("\n"))
                } else {
                    format!("{} files already exist in the output folder:\n\n{}", conflicts.len(), conflicts.join("\n"))
                };

                let confirm = adw::AlertDialog::builder()
                    .heading("Files Already Exist")
                    .body(&msg)
                    .build();
                confirm.add_response("rename", "Auto-rename");
                confirm.add_response("skip", "Skip Existing");
                confirm.add_response("overwrite", "Overwrite All");
                confirm.set_response_appearance("overwrite", adw::ResponseAppearance::Destructive);
                confirm.set_default_response(Some("rename"));
                confirm.set_close_response("rename");

                let ui_c = ui.clone();
                let window_c = _window.clone();
                let mut job_c = job;
                confirm.choose(Some(_window), gtk::gio::Cancellable::NONE, move |response| {
                    job_c.overwrite_behavior = match response.as_str() {
                        "overwrite" => pixstrip_core::operations::OverwriteAction::Overwrite,
                        "skip" => pixstrip_core::operations::OverwriteAction::Skip,
                        _ => pixstrip_core::operations::OverwriteAction::AutoRename,
                    };
                    continue_processing(&window_c, &ui_c, job_c);
                });
                return;
            }
        }
    }

    continue_processing(_window, ui, job);
}

fn continue_processing(
    _window: &adw::ApplicationWindow,
    ui: &WizardUi,
    job: pixstrip_core::pipeline::ProcessingJob,
) {
    // Build processing UI inline in the nav_view
    let processing_page = crate::processing::build_processing_page();
    ui.nav_view.push(&processing_page);

    // Hide bottom nav buttons and step indicator during processing
    ui.back_button.set_visible(false);
    ui.next_button.set_visible(false);
    ui.step_indicator.widget().set_visible(false);
    ui.title.set_subtitle("Processing...");

    // Disable navigation actions so Escape/shortcuts can't navigate away during processing
    set_nav_actions_enabled(&ui.nav_view, false);

    // Get references to progress widgets inside the page
    let progress_bar = find_widget_by_type::<gtk::ProgressBar>(&processing_page);
    let cancel_flag = Arc::new(AtomicBool::new(false));

    let pause_flag = Arc::new(AtomicBool::new(false));

    // Find cancel button and wire it; also wire pause button
    wire_cancel_button(&processing_page, cancel_flag.clone());
    wire_pause_button(&processing_page, pause_flag.clone());

    // Run processing in a background thread
    let (tx, rx) = std::sync::mpsc::channel::<ProcessingMessage>();

    let cancel = cancel_flag.clone();
    let pause = pause_flag.clone();
    // Load processing settings from config
    let cfg_store = pixstrip_core::storage::ConfigStore::new();
    let app_cfg = cfg_store.load().unwrap_or_default();
    std::thread::spawn(move || {
        let mut executor = pixstrip_core::executor::PipelineExecutor::with_cancel_and_pause(cancel, pause);
        match app_cfg.thread_count {
            pixstrip_core::config::ThreadCount::Auto => executor.set_thread_count(0),
            pixstrip_core::config::ThreadCount::Manual(n) => executor.set_thread_count(n),
        }
        executor.set_pause_on_error(app_cfg.error_behavior == pixstrip_core::config::ErrorBehavior::PauseOnError);
        let result = executor.execute(&job, |update| {
            let _ = tx.send(ProcessingMessage::Progress {
                current: update.current,
                total: update.total,
                file: update.current_file,
            });
        });
        match result {
            Ok(r) => {
                let _ = tx.send(ProcessingMessage::Done(r));
            }
            Err(e) => {
                let _ = tx.send(ProcessingMessage::Error(e.to_string()));
            }
        }
    });

    // Poll for messages from the processing thread
    let ui_for_rx = ui.clone();
    let start_time = std::time::Instant::now();
    glib::timeout_add_local(std::time::Duration::from_millis(50), move || {
        while let Ok(msg) = rx.try_recv() {
            match msg {
                ProcessingMessage::Progress {
                    current,
                    total,
                    file,
                } => {
                    if let Some(ref bar) = progress_bar {
                        let frac = if total > 0 { (current as f64 / total as f64).clamp(0.0, 1.0) } else { 0.0 };
                        bar.set_fraction(frac);
                        bar.set_text(Some(&format!("{}/{} - {}", current, total, file)));
                        bar.update_property(&[
                            gtk::accessible::Property::ValueNow(frac * 100.0),
                            gtk::accessible::Property::ValueText(
                                &format!("Processing {} of {}: {}", current, total, file)
                            ),
                        ]);
                    }
                    let eta = calculate_eta(&start_time, current, total);
                    update_progress_labels(&ui_for_rx.nav_view, current, total, &file, &eta);
                    add_log_entry(&ui_for_rx.nav_view, current, total, &file);
                }
                ProcessingMessage::Done(result) => {
                    show_results(&ui_for_rx, &result);
                    return glib::ControlFlow::Break;
                }
                ProcessingMessage::Error(err) => {
                    mark_current_queue_batch(&ui_for_rx, false, Some(&err));
                    let toast = adw::Toast::new(&format!("Processing failed: {}. Try with fewer images or check that the output folder exists.", err));
                    ui_for_rx.toast_overlay.add_toast(toast);
                    ui_for_rx.back_button.set_visible(true);
                    ui_for_rx.next_button.set_visible(true);
                    // Re-enable navigation actions
                    set_nav_actions_enabled(&ui_for_rx.nav_view, true);
                    if let Some(visible) = ui_for_rx.nav_view.visible_page()
                        && visible.tag().as_deref() == Some("processing")
                    {
                        ui_for_rx.nav_view.pop();
                    }
                    // Try to process next batch even if this one failed
                    let ui_q = ui_for_rx.clone();
                    glib::timeout_add_local_once(std::time::Duration::from_millis(500), move || {
                        process_next_queued_batch(&ui_q);
                    });
                    return glib::ControlFlow::Break;
                }
            }
        }
        glib::ControlFlow::Continue
    });
}

fn show_results(
    ui: &WizardUi,
    result: &pixstrip_core::executor::BatchResult,
) {
    let results_page = crate::processing::build_results_page();

    // Update result stats by walking the widget tree
    update_results_stats(&results_page, result);

    // Show errors if any
    if !result.errors.is_empty() {
        update_results_errors(&results_page, &result.errors);
    }

    // Wire the action buttons
    wire_results_actions(ui, &results_page);

    ui.nav_view.push(&results_page);

    ui.title.set_subtitle("Processing Complete");
    ui.step_indicator.widget().set_visible(false);
    ui.back_button.set_visible(false);
    ui.next_button.set_label("Process More");
    ui.next_button.set_visible(true);

    // Re-enable navigation actions
    set_nav_actions_enabled(&ui.nav_view, true);

    // Save history with output paths for undo support
    let history = pixstrip_core::storage::HistoryStore::new();
    let output_dir_str = ui.state.output_dir.borrow()
        .as_ref()
        .map(|p| p.display().to_string())
        .unwrap_or_default();
    let input_dir_str = ui.state.loaded_files.borrow()
        .first()
        .and_then(|p| p.parent())
        .map(|p| p.display().to_string())
        .unwrap_or_default();

    // Use actual output file paths from the executor (only successfully written files)
    let output_files: Vec<String> = result.output_files.clone();

    let _ = history.add(pixstrip_core::storage::HistoryEntry {
        timestamp: format!(
            "{}",
            std::time::SystemTime::now()
                .duration_since(std::time::UNIX_EPOCH)
                .unwrap_or_default()
                .as_secs()
        ),
        input_dir: input_dir_str,
        output_dir: output_dir_str,
        preset_name: None,
        total: result.total,
        succeeded: result.succeeded,
        failed: result.failed,
        total_input_bytes: result.total_input_bytes,
        total_output_bytes: result.total_output_bytes,
        elapsed_ms: result.elapsed_ms,
        output_files: output_files.clone(),
    }, 50, 30);

    // Prune old history entries
    let config_store = pixstrip_core::storage::ConfigStore::new();
    let app_config = config_store.load().unwrap_or_default();
    let _ = history.prune(app_config.history_max_entries, app_config.history_max_days);

    // Show toast
    let savings = if result.total_input_bytes > 0 {
        let pct =
            (1.0 - result.total_output_bytes as f64 / result.total_input_bytes as f64) * 100.0;
        format!(
            "{} images processed, {:.0}% space saved",
            result.succeeded, pct
        )
    } else {
        format!("{} images processed", result.succeeded)
    };
    // Undo toast with savings info
    let undo_toast = adw::Toast::new(&savings);
    undo_toast.set_button_label(Some("Undo"));
    undo_toast.set_timeout(10);
    {
        let undo_files = output_files;
        undo_toast.connect_button_clicked(move |t| {
            let mut trashed = 0;
            for path_str in &undo_files {
                let path = std::path::Path::new(path_str);
                if path.exists() {
                    let gfile = gtk::gio::File::for_path(path);
                    if gfile.trash(gtk::gio::Cancellable::NONE).is_ok() {
                        trashed += 1;
                    }
                }
            }
            t.dismiss();
            let _ = trashed;
        });
    }
    ui.toast_overlay.add_toast(undo_toast);

    // Desktop notification (if enabled in settings)
    let config_store = pixstrip_core::storage::ConfigStore::new();
    let config = config_store.load().unwrap_or_default();
    if config.notify_on_completion {
        let notification = gtk::gio::Notification::new("Pixstrip - Processing Complete");
        notification.set_body(Some(&savings));
        notification.set_priority(gtk::gio::NotificationPriority::Normal);
        if let Some(app) = gtk::gio::Application::default() {
            app.send_notification(Some("batch-complete"), &notification);
        }
    }

    // Play completion sound (if enabled in settings)
    if config.play_completion_sound {
        std::thread::spawn(|| {
            // Use canberra-gtk-play for system sound (standard on GNOME)
            let _ = std::process::Command::new("canberra-gtk-play")
                .arg("--id=complete")
                .arg("--description=Processing complete")
                .output();
        });
    }

    // Auto-open output folder if enabled
    if config.auto_open_output {
        let output = ui.state.output_dir.borrow().clone();
        if let Some(dir) = output {
            let uri = gtk::gio::File::for_path(&dir).uri();
            let _ = gtk::gio::AppInfo::launch_default_for_uri(
                &uri,
                gtk::gio::AppLaunchContext::NONE,
            );
        }
    }

    // Mark current queue batch as completed and process next one
    mark_current_queue_batch(ui, true, None);

    // Auto-start next queued batch after a short delay
    let ui_for_queue = ui.clone();
    glib::timeout_add_local_once(std::time::Duration::from_millis(500), move || {
        process_next_queued_batch(&ui_for_queue);
    });
}

fn update_results_stats(
    page: &adw::NavigationPage,
    result: &pixstrip_core::executor::BatchResult,
) {
    // Walk the widget tree looking for ActionRows to update
    walk_widgets(&page.child(), &|widget| {
        if let Some(row) = widget.downcast_ref::<adw::ActionRow>() {
            let title = row.title();
            match title.as_str() {
                "Images processed" => {
                    row.set_subtitle(&format!("{} images", result.succeeded));
                }
                "Original size" => {
                    row.set_subtitle(&format_size(result.total_input_bytes));
                }
                "Output size" => {
                    row.set_subtitle(&format_size(result.total_output_bytes));
                }
                "Space saved" => {
                    if result.total_input_bytes > 0 {
                        let pct = (1.0
                            - result.total_output_bytes as f64
                                / result.total_input_bytes as f64)
                            * 100.0;
                        row.set_subtitle(&format!("{:.1}%", pct));
                    }
                }
                "Processing time" => {
                    row.set_subtitle(&format_duration(result.elapsed_ms));
                }
                _ => {}
            }
        }
    });
}

fn update_results_errors(page: &adw::NavigationPage, errors: &[(String, String)]) {
    walk_widgets(&page.child(), &|widget| {
        if let Some(group) = widget.downcast_ref::<adw::PreferencesGroup>()
            && group.title().as_str() == "Errors"
        {
            group.set_visible(true);
        }
        if let Some(expander) = widget.downcast_ref::<adw::ExpanderRow>()
            && expander.title().contains("errors")
        {
            expander.set_title(&format!("{} errors occurred", errors.len()));
            for (file, err) in errors {
                let row = adw::ActionRow::builder()
                    .title(file)
                    .subtitle(err)
                    .build();
                row.add_prefix(&gtk::Image::from_icon_name("dialog-error-symbolic"));
                expander.add_row(&row);
            }
        }
    });
}

fn wire_results_actions(
    ui: &WizardUi,
    page: &adw::NavigationPage,
) {
    walk_widgets(&page.child(), &|widget| {
        if let Some(row) = widget.downcast_ref::<adw::ActionRow>() {
            match row.title().as_str() {
                "Open Output Folder" => {
                    let ui = ui.clone();
                    row.connect_activated(move |_| {
                        let output = ui.state.output_dir.borrow().clone();
                        if let Some(dir) = output {
                            let uri = gtk::gio::File::for_path(&dir).uri();
                            let _ = gtk::gio::AppInfo::launch_default_for_uri(
                                &uri,
                                gtk::gio::AppLaunchContext::NONE,
                            );
                        }
                    });
                }
                "Process Another Batch" => {
                    let ui = ui.clone();
                    row.connect_activated(move |_| {
                        reset_wizard(&ui);
                    });
                }
                "Add to Queue" => {
                    let ui = ui.clone();
                    row.connect_activated(move |_| {
                        add_current_batch_to_queue(&ui);
                        reset_wizard(&ui);
                    });
                }
                "Save as Preset" => {
                    row.set_action_name(Some("win.save-preset"));
                }
                _ => {}
            }
        }
    });
}

fn undo_last_batch(ui: &WizardUi) {
    let history = pixstrip_core::storage::HistoryStore::new();
    let entries = match history.list() {
        Ok(e) => e,
        Err(_) => {
            ui.toast_overlay.add_toast(adw::Toast::new("No processing history available. Process a batch first before undoing."));
            return;
        }
    };

    let Some(last) = entries.last() else {
        ui.toast_overlay.add_toast(adw::Toast::new("No batches to undo. Process some images first."));
        return;
    };

    if last.output_files.is_empty() {
        ui.toast_overlay.add_toast(adw::Toast::new("No output files recorded for last batch. The batch may have been already undone."));
        return;
    }

    // Move output files to trash using GIO
    let mut trashed = 0usize;
    for path_str in &last.output_files {
        let file = gtk::gio::File::for_path(path_str);
        if file.trash(gtk::gio::Cancellable::NONE).is_ok() {
            trashed += 1;
        }
    }

    let toast = adw::Toast::new(&format!(
        "Undo: moved {} files to trash",
        trashed
    ));
    toast.set_timeout(5);
    ui.toast_overlay.add_toast(toast);
}

fn paste_images_from_clipboard(window: &adw::ApplicationWindow, ui: &WizardUi) {
    let clipboard = window.clipboard();
    let ui = ui.clone();

    // Try to read a texture (image) from clipboard
    clipboard.read_texture_async(gtk::gio::Cancellable::NONE, move |result| {
        if let Ok(Some(texture)) = result {
            // Save the texture to a temp file
            let temp_dir = std::env::temp_dir().join("pixstrip-clipboard");
            if std::fs::create_dir_all(&temp_dir).is_err() {
                ui.toast_overlay.add_toast(adw::Toast::new("Failed to create temporary directory. Check disk space and permissions on /tmp."));
                return;
            }
            let timestamp = std::time::SystemTime::now()
                .duration_since(std::time::UNIX_EPOCH)
                .unwrap_or_default()
                .as_nanos();
            let temp_path = temp_dir.join(format!("clipboard-{}.png", timestamp));

            let bytes = texture.save_to_png_bytes();
            if std::fs::write(&temp_path, bytes.as_ref()).is_ok() {
                let mut files = ui.state.loaded_files.borrow_mut();
                if !files.contains(&temp_path) {
                    files.push(temp_path);
                }
                let count = files.len();
                drop(files);
                update_images_count_label(&ui, count);

                let toast = adw::Toast::new("Pasted image from clipboard");
                toast.set_timeout(2);
                ui.toast_overlay.add_toast(toast);
            } else {
                ui.toast_overlay.add_toast(adw::Toast::new("Failed to save clipboard image. The image format may be unsupported."));
            }
        } else {
            ui.toast_overlay.add_toast(adw::Toast::new("No image found in clipboard. Copy an image first, then try pasting again."));
        }
    });
}

fn reset_wizard(ui: &WizardUi) {
    // Clean up clipboard temp files
    let temp_dir = std::env::temp_dir().join("pixstrip-clipboard");
    if temp_dir.is_dir() {
        let _ = std::fs::remove_dir_all(&temp_dir);
    }

    // Reset state
    {
        let mut s = ui.state.wizard.borrow_mut();
        s.current_step = 0;
        s.visited = vec![false; s.total_steps];
        s.visited[0] = true;
    }
    ui.state.loaded_files.borrow_mut().clear();
    ui.state.excluded_files.borrow_mut().clear();
    // Reset job config to clear preset_mode so wizard steps show up again
    ui.state.job_config.borrow_mut().preset_mode = false;
    *ui.state.output_dir.borrow_mut() = None;

    // Reset nav
    ui.nav_view.replace(&ui.pages[..1]);
    // Rebuild indicator with all steps
    let all_names = [
        "Workflow", "Images", "Resize", "Adjustments", "Convert",
        "Compress", "Metadata", "Watermark", "Rename", "Output",
    ];
    let all: Vec<(usize, String)> = all_names.iter().enumerate()
        .map(|(i, name)| (i, name.to_string()))
        .collect();
    ui.step_indicator.rebuild(&all);
    ui.step_indicator.set_current(0);
    ui.step_indicator.widget().set_visible(true);
    ui.title.set_subtitle("Batch Image Processor");
    ui.back_button.set_visible(false);
    ui.next_button.set_label("Next");
    ui.next_button.set_visible(true);
    ui.next_button.add_css_class("suggested-action");
}

/// Enable or disable navigation actions (prev-step, next-step) to prevent
/// keyboard shortcuts from navigating away during processing.
fn set_nav_actions_enabled(nav_view: &adw::NavigationView, enabled: bool) {
    if let Some(root) = nav_view.root() {
        if let Ok(win) = root.downcast::<adw::ApplicationWindow>() {
            for name in ["prev-step", "next-step"] {
                if let Some(action) = win.lookup_action(name) {
                    if let Some(simple) = action.downcast_ref::<gtk::gio::SimpleAction>() {
                        simple.set_enabled(enabled);
                    }
                }
            }
        }
    }
}

fn wire_cancel_button(page: &adw::NavigationPage, cancel_flag: Arc<AtomicBool>) {
    walk_widgets(&page.child(), &|widget| {
        if let Some(button) = widget.downcast_ref::<gtk::Button>()
            && button.label().as_deref() == Some("Cancel")
        {
            let flag = cancel_flag.clone();
            button.connect_clicked(move |btn| {
                flag.store(true, Ordering::Relaxed);
                btn.set_sensitive(false);
                btn.set_label("Cancelling...");
            });
        }
    });
}

fn wire_pause_button(page: &adw::NavigationPage, pause_flag: Arc<AtomicBool>) {
    walk_widgets(&page.child(), &|widget| {
        if let Some(button) = widget.downcast_ref::<gtk::Button>()
            && button.label().as_deref() == Some("Pause")
        {
            let flag = pause_flag.clone();
            button.connect_clicked(move |btn| {
                if btn.label().as_deref() == Some("Pause") {
                    flag.store(true, Ordering::Relaxed);
                    btn.set_label("Resume");
                    btn.add_css_class("suggested-action");
                    btn.remove_css_class("flat");
                } else {
                    flag.store(false, Ordering::Relaxed);
                    btn.set_label("Pause");
                    btn.remove_css_class("suggested-action");
                }
            });
        }
    });
}

fn calculate_eta(start: &std::time::Instant, current: usize, total: usize) -> String {
    if current == 0 {
        return "Estimating time remaining...".into();
    }
    if current >= total {
        return "Almost done...".into();
    }
    let elapsed = start.elapsed().as_secs_f64();
    let per_image = elapsed / current as f64;
    let remaining = (total.saturating_sub(current)) as f64 * per_image;
    if remaining < 1.0 {
        "Almost done...".into()
    } else {
        format!("ETA: ~{}", format_duration(remaining as u64 * 1000))
    }
}

fn update_progress_labels(nav_view: &adw::NavigationView, current: usize, total: usize, file: &str, eta: &str) {
    if let Some(page) = nav_view.visible_page() {
        walk_widgets(&page.child(), &|widget| {
            if let Some(label) = widget.downcast_ref::<gtk::Label>() {
                if label.widget_name() == "processing-count-label" {
                    label.set_label(&format!("{} / {} images", current, total));
                }
                if label.widget_name() == "processing-eta-label" {
                    if current < total {
                        label.set_label(&format!("{} - {}", eta, file));
                    } else {
                        label.set_label("Finishing up...");
                    }
                }
            }
        });
    }
}

fn add_log_entry(nav_view: &adw::NavigationView, current: usize, total: usize, file: &str) {
    if let Some(page) = nav_view.visible_page() {
        walk_widgets(&page.child(), &|widget| {
            if let Some(bx) = widget.downcast_ref::<gtk::Box>()
                && bx.widget_name() == "processing-log-box"
            {
                let entry = gtk::Label::builder()
                    .label(format!("[{}/{}] {} - Done", current, total, file))
                    .halign(gtk::Align::Start)
                    .css_classes(["caption", "monospace"])
                    .build();
                bx.append(&entry);

                // Auto-scroll: the log box is inside a ScrolledWindow
                if let Some(parent) = bx.parent()
                    && let Some(sw) = parent.downcast_ref::<gtk::ScrolledWindow>()
                {
                    let adj = sw.vadjustment();
                    adj.set_value(adj.upper());
                }
            }
        });
    }
}

fn import_preset(window: &adw::ApplicationWindow, ui: &WizardUi) {
    let dialog = gtk::FileDialog::builder()
        .title("Import Preset")
        .modal(true)
        .build();

    let filter = gtk::FileFilter::new();
    filter.set_name(Some("Preset files (JSON)"));
    filter.add_mime_type("application/json");
    filter.add_suffix("json");

    let filters = gtk::gio::ListStore::new::<gtk::FileFilter>();
    filters.append(&filter);
    dialog.set_filters(Some(&filters));
    dialog.set_default_filter(Some(&filter));

    let ui = ui.clone();
    dialog.open(Some(window), gtk::gio::Cancellable::NONE, move |result| {
        if let Ok(file) = result
            && let Some(path) = file.path()
        {
            let store = pixstrip_core::storage::PresetStore::new();
            match store.import_from_file(&path) {
                Ok(preset) => {
                    let msg = format!("Imported preset: {}", preset.name);
                    let _ = store.save(&preset);
                    let toast = adw::Toast::new(&msg);
                    ui.toast_overlay.add_toast(toast);
                }
                Err(e) => {
                    let toast = adw::Toast::new(&format!("Failed to import preset: {}. Make sure the file is a valid .pixstrip-preset file.", e));
                    ui.toast_overlay.add_toast(toast);
                }
            }
        }
    });
}

fn save_preset_dialog(window: &adw::ApplicationWindow, ui: &WizardUi) {
    let dialog = adw::Dialog::builder()
        .title("Save as Preset")
        .content_width(400)
        .content_height(500)
        .build();

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

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

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

    // Show summary of current settings
    let cfg = ui.state.job_config.borrow();
    let summary = build_preset_description(&cfg);
    drop(cfg);

    let summary_group = adw::PreferencesGroup::builder()
        .title("Workflow Summary")
        .description(&summary)
        .build();
    content.append(&summary_group);

    // Name entry
    let name_group = adw::PreferencesGroup::builder()
        .title("Save as New Preset")
        .build();

    let name_entry = adw::EntryRow::builder()
        .title("Preset Name")
        .build();
    name_group.add(&name_entry);

    let desc_entry = adw::EntryRow::builder()
        .title("Description (optional)")
        .text(&summary)
        .build();
    name_group.add(&desc_entry);

    // Icon picker
    let icon_names = [
        ("user-bookmarks-symbolic", "Bookmark"),
        ("image-x-generic-symbolic", "Image"),
        ("camera-photo-symbolic", "Camera"),
        ("emblem-photos-symbolic", "Photos"),
        ("applications-graphics-symbolic", "Graphics"),
        ("starred-symbolic", "Star"),
        ("emblem-favorite-symbolic", "Heart"),
        ("folder-symbolic", "Folder"),
        ("preferences-color-symbolic", "Color"),
        ("emblem-system-symbolic", "Gear"),
    ];
    let icon_string_list = gtk::StringList::new(&icon_names.map(|(_, label)| label));
    let icon_combo = adw::ComboRow::builder()
        .title("Icon")
        .model(&icon_string_list)
        .build();
    // Show icon preview as prefix
    let icon_preview = gtk::Image::builder()
        .icon_name("user-bookmarks-symbolic")
        .pixel_size(24)
        .build();
    icon_preview.set_accessible_role(gtk::AccessibleRole::Presentation);
    icon_combo.add_prefix(&icon_preview);
    name_group.add(&icon_combo);

    // Color picker
    let color_labels = ["Default", "Blue", "Green", "Yellow", "Red"];
    let color_values = ["", "accent", "success", "warning", "error"];
    let color_string_list = gtk::StringList::new(&color_labels);
    let color_combo = adw::ComboRow::builder()
        .title("Icon Color")
        .model(&color_string_list)
        .build();
    let color_preview = gtk::Image::builder()
        .icon_name("user-bookmarks-symbolic")
        .pixel_size(24)
        .build();
    color_preview.set_accessible_role(gtk::AccessibleRole::Presentation);
    color_combo.add_prefix(&color_preview);
    name_group.add(&color_combo);

    // Update icon preview when icon selection changes
    {
        let ip = icon_preview.clone();
        let cp = color_preview.clone();
        let cc = color_combo.clone();
        icon_combo.connect_selected_notify(move |combo| {
            let idx = combo.selected() as usize;
            if idx < icon_names.len() {
                let icon_name = icon_names[idx].0;
                ip.set_icon_name(Some(icon_name));
                cp.set_icon_name(Some(icon_name));
                // Re-apply color class
                for cv in &color_values {
                    if !cv.is_empty() {
                        cp.remove_css_class(cv);
                    }
                }
                let cidx = cc.selected() as usize;
                if cidx < color_values.len() && !color_values[cidx].is_empty() {
                    cp.add_css_class(color_values[cidx]);
                }
            }
        });
    }

    // Update color preview when color selection changes
    {
        let cp = color_preview;
        color_combo.connect_selected_notify(move |combo| {
            let idx = combo.selected() as usize;
            for cv in &color_values {
                if !cv.is_empty() {
                    cp.remove_css_class(cv);
                }
            }
            if idx < color_values.len() && !color_values[idx].is_empty() {
                cp.add_css_class(color_values[idx]);
            }
        });
    }

    let save_new_button = gtk::Button::builder()
        .label("Save New Preset")
        .halign(gtk::Align::Center)
        .margin_top(8)
        .build();
    save_new_button.add_css_class("suggested-action");
    save_new_button.add_css_class("pill");

    content.append(&name_group);
    content.append(&save_new_button);

    // "Update existing" section - show user presets
    let store = pixstrip_core::storage::PresetStore::new();
    let user_presets: Vec<String> = store
        .list()
        .unwrap_or_default()
        .into_iter()
        .filter(|p| p.is_custom)
        .map(|p| p.name)
        .collect();

    if !user_presets.is_empty() {
        let update_group = adw::PreferencesGroup::builder()
            .title("Or Update Existing Preset")
            .description("Overwrite an existing user preset with current settings")
            .build();

        for preset_name in &user_presets {
            let row = adw::ActionRow::builder()
                .title(preset_name)
                .activatable(true)
                .build();
            row.add_prefix(&gtk::Image::from_icon_name("user-bookmarks-symbolic"));
            row.add_suffix(&gtk::Image::from_icon_name("go-next-symbolic"));

            let ui_c = ui.clone();
            let dlg_c = dialog.clone();
            let pname = preset_name.clone();
            let store_ref = pixstrip_core::storage::PresetStore::new();
            let existing_preset = store_ref.load(preset_name).ok();
            let existing_icon = existing_preset.as_ref().map(|p| p.icon.clone()).unwrap_or_default();
            let existing_color = existing_preset.as_ref().map(|p| p.icon_color.clone()).unwrap_or_default();
            row.connect_activated(move |_| {
                let cfg = ui_c.state.job_config.borrow();
                let ei = existing_icon.as_str();
                let ec = existing_color.as_str();
                let preset = build_preset_from_config(&cfg, &pname, None, Some(ei), Some(ec));
                drop(cfg);

                let store = pixstrip_core::storage::PresetStore::new();
                match store.save(&preset) {
                    Ok(()) => {
                        let toast = adw::Toast::new(&format!("Updated preset: {}", pname));
                        ui_c.toast_overlay.add_toast(toast);
                    }
                    Err(e) => {
                        let toast = adw::Toast::new(&format!("Failed to update preset: {}. The preset file may be read-only.", e));
                        ui_c.toast_overlay.add_toast(toast);
                    }
                }
                dlg_c.close();
            });

            update_group.add(&row);
        }

        content.append(&update_group);
    }

    // Wire save new button
    {
        let ui_c = ui.clone();
        let dlg_c = dialog.clone();
        let entry_c = name_entry.clone();
        let desc_c = desc_entry.clone();
        let icon_combo_c = icon_combo;
        let color_combo_c = color_combo;
        save_new_button.connect_clicked(move |_| {
            let name = entry_c.text().to_string();
            if name.trim().is_empty() {
                let toast = adw::Toast::new("Please enter a name for the preset");
                ui_c.toast_overlay.add_toast(toast);
                return;
            }

            let desc_text = desc_c.text().to_string();
            let icon_idx = icon_combo_c.selected() as usize;
            let selected_icon = icon_names.get(icon_idx).map(|(name, _)| *name);
            let color_idx = color_combo_c.selected() as usize;
            let selected_color = color_values.get(color_idx).copied();
            let cfg = ui_c.state.job_config.borrow();
            let preset = build_preset_from_config(&cfg, &name, Some(&desc_text), selected_icon, selected_color);
            drop(cfg);

            let store = pixstrip_core::storage::PresetStore::new();
            match store.save(&preset) {
                Ok(()) => {
                    let toast = adw::Toast::new(&format!("Saved preset: {}", name));
                    ui_c.toast_overlay.add_toast(toast);
                }
                Err(e) => {
                    let toast = adw::Toast::new(&format!("Failed to save preset: {}. Check that the presets folder is writable.", e));
                    ui_c.toast_overlay.add_toast(toast);
                }
            }
            dlg_c.close();
        });
    }

    scrolled.set_child(Some(&content));
    toolbar.set_content(Some(&scrolled));
    dialog.set_child(Some(&toolbar));

    dialog.present(Some(window));
}

fn build_preset_from_config(cfg: &JobConfig, name: &str, description: Option<&str>, icon: Option<&str>, icon_color: Option<&str>) -> pixstrip_core::preset::Preset {
    let resize = if cfg.resize_enabled && cfg.resize_width > 0 {
        if cfg.resize_height == 0 {
            Some(pixstrip_core::operations::ResizeConfig::ByWidth(cfg.resize_width))
        } else if cfg.resize_mode == 1 {
            Some(pixstrip_core::operations::ResizeConfig::FitInBox {
                max: pixstrip_core::types::Dimensions {
                    width: cfg.resize_width,
                    height: cfg.resize_height,
                },
                allow_upscale: cfg.allow_upscale,
            })
        } else {
            Some(pixstrip_core::operations::ResizeConfig::Exact(
                pixstrip_core::types::Dimensions {
                    width: cfg.resize_width,
                    height: cfg.resize_height,
                },
            ))
        }
    } else {
        None
    };

    let convert = if cfg.convert_enabled {
        cfg.convert_format.map(pixstrip_core::operations::ConvertConfig::SingleFormat)
    } else {
        None
    };

    let compress = if cfg.compress_enabled {
        Some(pixstrip_core::operations::CompressConfig::Preset(cfg.quality_preset))
    } else {
        None
    };

    let metadata = if cfg.metadata_enabled {
        Some(match cfg.metadata_mode {
            MetadataMode::StripAll => pixstrip_core::operations::MetadataConfig::StripAll,
            MetadataMode::Privacy => pixstrip_core::operations::MetadataConfig::Privacy,
            MetadataMode::KeepAll => pixstrip_core::operations::MetadataConfig::KeepAll,
            MetadataMode::Custom => pixstrip_core::operations::MetadataConfig::Custom {
                strip_gps: cfg.strip_gps,
                strip_camera: cfg.strip_camera,
                strip_software: cfg.strip_software,
                strip_timestamps: cfg.strip_timestamps,
                strip_copyright: cfg.strip_copyright,
            },
        })
    } else {
        None
    };

    let rotation = match cfg.rotation {
        1 => Some(pixstrip_core::operations::Rotation::Cw90),
        2 => Some(pixstrip_core::operations::Rotation::Cw180),
        3 => Some(pixstrip_core::operations::Rotation::Cw270),
        4 => Some(pixstrip_core::operations::Rotation::AutoOrient),
        _ => None,
    };

    let flip = match cfg.flip {
        1 => Some(pixstrip_core::operations::Flip::Horizontal),
        2 => Some(pixstrip_core::operations::Flip::Vertical),
        _ => None,
    };

    let watermark = if cfg.watermark_enabled {
        let position = match cfg.watermark_position {
            0 => pixstrip_core::operations::WatermarkPosition::TopLeft,
            1 => pixstrip_core::operations::WatermarkPosition::TopCenter,
            2 => pixstrip_core::operations::WatermarkPosition::TopRight,
            3 => pixstrip_core::operations::WatermarkPosition::MiddleLeft,
            4 => pixstrip_core::operations::WatermarkPosition::Center,
            5 => pixstrip_core::operations::WatermarkPosition::MiddleRight,
            6 => pixstrip_core::operations::WatermarkPosition::BottomLeft,
            7 => pixstrip_core::operations::WatermarkPosition::BottomCenter,
            _ => pixstrip_core::operations::WatermarkPosition::BottomRight,
        };
        let wm_rotation = if cfg.watermark_rotation != 0 {
            Some(pixstrip_core::operations::WatermarkRotation::Custom(cfg.watermark_rotation as f32))
        } else {
            None
        };
        if cfg.watermark_use_image {
            cfg.watermark_image_path.as_ref().map(|path| {
                pixstrip_core::operations::WatermarkConfig::Image {
                    path: path.clone(),
                    position,
                    opacity: cfg.watermark_opacity,
                    scale: cfg.watermark_scale / 100.0,
                    rotation: wm_rotation,
                    tiled: cfg.watermark_tiled,
                    margin: cfg.watermark_margin,
                }
            })
        } else if !cfg.watermark_text.is_empty() {
            Some(pixstrip_core::operations::WatermarkConfig::Text {
                text: cfg.watermark_text.clone(),
                position,
                font_size: cfg.watermark_font_size,
                opacity: cfg.watermark_opacity,
                color: cfg.watermark_color,
                font_family: if cfg.watermark_font_family.is_empty() { None } else { Some(cfg.watermark_font_family.clone()) },
                rotation: wm_rotation,
                tiled: cfg.watermark_tiled,
                margin: cfg.watermark_margin,
            })
        } else {
            None
        }
    } else {
        None
    };

    let rename = if cfg.rename_enabled {
        Some(pixstrip_core::operations::RenameConfig {
            prefix: cfg.rename_prefix.clone(),
            suffix: cfg.rename_suffix.clone(),
            counter_start: cfg.rename_counter_start,
            counter_padding: cfg.rename_counter_padding,
            counter_enabled: cfg.rename_counter_enabled,
            counter_position: cfg.rename_counter_position,
            template: if cfg.rename_template.is_empty() {
                None
            } else {
                Some(cfg.rename_template.clone())
            },
            case_mode: cfg.rename_case,
            replace_spaces: cfg.rename_replace_spaces,
            special_chars: cfg.rename_special_chars,
            regex_find: cfg.rename_find.clone(),
            regex_replace: cfg.rename_replace.clone(),
        })
    } else {
        None
    };

    pixstrip_core::preset::Preset {
        name: name.to_string(),
        description: description
            .filter(|d| !d.trim().is_empty())
            .map(|d| d.to_string())
            .unwrap_or_else(|| build_preset_description(cfg)),
        icon: icon.unwrap_or("user-bookmarks-symbolic").to_string(),
        icon_color: icon_color.unwrap_or("").to_string(),
        is_custom: true,
        resize,
        rotation,
        flip,
        convert,
        compress,
        metadata,
        watermark,
        rename,
    }
}

fn build_preset_description(cfg: &JobConfig) -> String {
    let mut parts = Vec::new();
    if cfg.resize_enabled && cfg.resize_width > 0 {
        if cfg.resize_height == 0 {
            parts.push(format!("Resize {}px wide", cfg.resize_width));
        } else {
            parts.push(format!("Resize {}x{}", cfg.resize_width, cfg.resize_height));
        }
    }
    if cfg.convert_enabled && let Some(fmt) = cfg.convert_format {
        parts.push(format!("Convert to {:?}", fmt));
    }
    if cfg.compress_enabled {
        parts.push(format!("Compress {:?}", cfg.quality_preset));
    }
    if cfg.metadata_enabled {
        parts.push(format!("Metadata: {:?}", cfg.metadata_mode));
    }
    if parts.is_empty() {
        "Custom workflow".into()
    } else {
        parts.join(", ")
    }
}

fn update_output_summary(ui: &WizardUi) {
    let cfg = ui.state.job_config.borrow();
    if let Some(page) = ui.pages.get(9) {
        // Build summary lines
        let mut ops = Vec::new();
        if cfg.resize_enabled && cfg.resize_width > 0 {
            if cfg.resize_height == 0 {
                ops.push(format!("Resize to {}px wide", cfg.resize_width));
            } else {
                ops.push(format!("Resize to {}x{}", cfg.resize_width, cfg.resize_height));
            }
        }
        if cfg.convert_enabled && let Some(fmt) = cfg.convert_format {
            ops.push(format!("Convert to {:?}", fmt));
        }
        if cfg.compress_enabled {
            ops.push(format!("Compress ({:?})", cfg.quality_preset));
        }
        if cfg.metadata_enabled {
            let mode = match cfg.metadata_mode {
                MetadataMode::StripAll => "Strip all metadata",
                MetadataMode::Privacy => "Privacy mode",
                MetadataMode::KeepAll => "Keep all metadata",
                MetadataMode::Custom => "Custom metadata stripping",
            };
            ops.push(mode.to_string());
        }
        if cfg.watermark_enabled {
            if cfg.watermark_use_image {
                ops.push("Image watermark".to_string());
            } else if !cfg.watermark_text.is_empty() {
                ops.push(format!("Watermark: \"{}\"", cfg.watermark_text));
            }
        }
        if cfg.rename_enabled {
            if !cfg.rename_template.is_empty() {
                ops.push(format!("Rename: {}", cfg.rename_template));
            } else if !cfg.rename_prefix.is_empty() || !cfg.rename_suffix.is_empty() {
                ops.push(format!(
                    "Rename: {}...{}",
                    cfg.rename_prefix, cfg.rename_suffix
                ));
            } else {
                ops.push("Sequential rename".to_string());
            }
        }
        if cfg.adjustments_enabled {
            if cfg.rotation > 0 {
                let rot = match cfg.rotation {
                    1 => "Rotate 90",
                    2 => "Rotate 180",
                    3 => "Rotate 270",
                    4 => "Auto-orient",
                    _ => "Rotate",
                };
                ops.push(rot.to_string());
            }
            if cfg.flip > 0 {
                let fl = match cfg.flip {
                    1 => "Flip horizontal",
                    2 => "Flip vertical",
                    _ => "Flip",
                };
                ops.push(fl.to_string());
            }
            if cfg.brightness != 0 {
                ops.push(format!("Brightness {:+}", cfg.brightness));
            }
            if cfg.contrast != 0 {
                ops.push(format!("Contrast {:+}", cfg.contrast));
            }
            if cfg.saturation != 0 {
                ops.push(format!("Saturation {:+}", cfg.saturation));
            }
            if cfg.sharpen {
                ops.push("Sharpen".to_string());
            }
            if cfg.grayscale {
                ops.push("Grayscale".to_string());
            }
            if cfg.sepia {
                ops.push("Sepia".to_string());
            }
            if cfg.crop_aspect_ratio > 0 {
                let ratio = match cfg.crop_aspect_ratio {
                    1 => "1:1",
                    2 => "4:3",
                    3 => "3:2",
                    4 => "16:9",
                    5 => "9:16",
                    6 => "3:4",
                    7 => "2:3",
                    _ => "Custom",
                };
                ops.push(format!("Crop {}", ratio));
            }
            if cfg.trim_whitespace {
                ops.push("Trim whitespace".to_string());
            }
            if cfg.canvas_padding > 0 {
                ops.push(format!("Padding {}px", cfg.canvas_padding));
            }
        }

        // Find the ops-summary-list ListBox and populate it
        walk_widgets(&page.child(), &|widget| {
            if let Some(list_box) = widget.downcast_ref::<gtk::ListBox>()
                && list_box.widget_name().as_str() == "ops-summary-list"
            {
                // Clear existing rows
                while let Some(child) = list_box.first_child() {
                    list_box.remove(&child);
                }

                if ops.is_empty() {
                    let row = adw::ActionRow::builder()
                        .title("No operations configured")
                        .subtitle("Go back and configure your workflow settings")
                        .build();
                    row.add_prefix(&gtk::Image::from_icon_name("dialog-information-symbolic"));
                    list_box.append(&row);
                } else {
                    for op in &ops {
                        let row = adw::ActionRow::builder()
                            .title(op.as_str())
                            .build();
                        row.add_prefix(&gtk::Image::from_icon_name("emblem-ok-symbolic"));
                        list_box.append(&row);
                    }
                }
            }
        });
    }
}

// --- Utility functions ---

enum ProcessingMessage {
    Progress {
        current: usize,
        total: usize,
        file: String,
    },
    Done(pixstrip_core::executor::BatchResult),
    Error(String),
}

fn find_widget_by_type<T: IsA<gtk::Widget>>(page: &adw::NavigationPage) -> Option<T> {
    let result: RefCell<Option<T>> = RefCell::new(None);
    walk_widgets(&page.child(), &|widget| {
        if result.borrow().is_none()
            && let Some(w) = widget.downcast_ref::<T>()
        {
            *result.borrow_mut() = Some(w.clone());
        }
    });
    result.into_inner()
}

pub fn walk_widgets(widget: &Option<gtk::Widget>, f: &dyn Fn(&gtk::Widget)) {
    let Some(w) = widget else { return };
    f(w);
    let mut child = w.first_child();
    while let Some(c) = child {
        walk_widgets(&Some(c.clone()), f);
        child = c.next_sibling();
    }
}


#[allow(deprecated)] // ShortcutLabel deprecated in 4.18 with no replacement yet
fn show_shortcuts_window(window: &adw::ApplicationWindow) {
    let dialog = adw::Dialog::builder()
        .title("Keyboard Shortcuts")
        .content_width(460)
        .content_height(520)
        .build();

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

    let scroll = gtk::ScrolledWindow::builder()
        .hscrollbar_policy(gtk::PolicyType::Never)
        .vexpand(true)
        .build();

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

    let sections: &[(&str, &[(&str, &str)])] = &[
        ("Wizard Navigation", &[
            ("<Alt>Right", "Next step"),
            ("<Alt>Left", "Previous step"),
            ("<Alt>1", "Jump to step (1-9)"),
            ("<Control>Return", "Process images"),
            ("Escape", "Cancel or go back"),
        ]),
        ("File Management", &[
            ("<Control>o", "Add files"),
            ("<Control>v", "Paste image from clipboard"),
            ("<Control>a", "Select all images"),
            ("<Control><Shift>a", "Deselect all images"),
            ("Delete", "Remove selected images"),
        ]),
        ("Application", &[
            ("<Control>comma", "Settings"),
            ("F1", "Keyboard shortcuts"),
            ("<Control>z", "Undo last batch"),
            ("<Control>q", "Quit"),
        ]),
    ];

    for (section_title, shortcuts) in sections {
        let group = gtk::Box::builder()
            .orientation(gtk::Orientation::Vertical)
            .spacing(6)
            .build();

        let title_label = gtk::Label::builder()
            .label(*section_title)
            .css_classes(["title-4"])
            .halign(gtk::Align::Start)
            .margin_bottom(2)
            .build();
        group.append(&title_label);

        for (accel, description) in *shortcuts {
            let row = gtk::Box::builder()
                .orientation(gtk::Orientation::Horizontal)
                .spacing(12)
                .build();

            let desc_label = gtk::Label::builder()
                .label(*description)
                .halign(gtk::Align::Start)
                .hexpand(true)
                .build();

            let shortcut_label = gtk::ShortcutLabel::builder()
                .accelerator(*accel)
                .halign(gtk::Align::End)
                .build();

            row.append(&desc_label);
            row.append(&shortcut_label);
            group.append(&row);
        }

        content.append(&group);
    }

    scroll.set_child(Some(&content));
    toolbar_view.set_content(Some(&scroll));
    dialog.set_child(Some(&toolbar_view));
    dialog.present(Some(window));
}

fn apply_accessibility_settings() {
    let config_store = pixstrip_core::storage::ConfigStore::new();
    let config = config_store.load().unwrap_or_default();

    let Some(settings) = gtk::Settings::default() else {
        return;
    };

    if config.high_contrast {
        settings.set_gtk_theme_name(Some("HighContrast"));
    }

    if config.large_text {
        // Increase font DPI by 25% for large text mode
        let current_dpi = settings.gtk_xft_dpi();
        if current_dpi > 0 {
            settings.set_gtk_xft_dpi(current_dpi * 5 / 4);
        }
    }

    if config.reduced_motion {
        settings.set_gtk_enable_animations(false);
    }
}

fn show_step_help(window: &adw::ApplicationWindow, step: usize) {
    let (title, icon_name, body) = match step {
        0 => ("Workflow", "view-grid-symbolic", concat!(
            "Pick a built-in preset to start quickly, or select the Custom card to choose ",
            "which operations to include.\n\n",
            "Built-in presets auto-advance to the Images step with recommended settings. ",
            "Custom mode shows toggle switches for each operation (Resize, Adjustments, Convert, ",
            "Compress, Metadata, Watermark, Rename).\n\n",
            "Your saved presets appear below the built-in ones. Use Import to load a .pixstrip-preset file, ",
            "or drag one onto this page."
        )),
        1 => ("Images", "image-x-generic-symbolic", concat!(
            "Add the images you want to process.\n\n",
            "- Drag and drop files or folders onto this area\n",
            "- Drag image URLs from a web browser\n",
            "- Click Browse Files or press Ctrl+O\n",
            "- Press Ctrl+V to paste from clipboard\n\n",
            "When dropping a folder with subfolders, you'll be asked whether to include them. ",
            "Use checkboxes on each thumbnail to include or exclude images. ",
            "Ctrl+A selects all, Ctrl+Shift+A deselects all."
        )),
        2 => ("Resize", "view-fullscreen-symbolic", concat!(
            "Scale images to specific dimensions with a live preview.\n\n",
            "Pick a category and preset size, or enter custom width and height. ",
            "Toggle between pixel and percentage units. Lock the aspect ratio to keep proportions.\n\n",
            "Choose Exact Size or Fit Within Box mode. Enable Allow Upscaling to enlarge smaller images. ",
            "Expand Advanced Settings for resize algorithm (Lanczos3, CatmullRom, etc.) and output DPI."
        )),
        3 => ("Adjustments", "preferences-color-symbolic", concat!(
            "Fine-tune image appearance with a live preview.\n\n",
            "Orientation: rotate (including auto-orient from EXIF) and flip.\n",
            "Color: adjust brightness, contrast, and saturation with sliders.\n",
            "Effects: toggle grayscale, sepia, or sharpen.\n",
            "Crop and Canvas: crop to an aspect ratio, trim whitespace borders, or add padding."
        )),
        4 => ("Convert", "document-save-as-symbolic", concat!(
            "Change image file format.\n\n",
            "Select a target format from the card grid (JPEG, PNG, WebP, AVIF) or use the ",
            "Other Formats dropdown for GIF, TIFF, and BMP. Keep Original preserves each file's format.\n\n",
            "Enable Progressive JPEG for gradual loading in browsers. Use Format Mapping to override ",
            "the output format for specific input types (e.g. convert PNG to WebP but keep JPEG as-is)."
        )),
        5 => ("Compress", "drive-harddisk-symbolic", concat!(
            "Reduce file size while preserving quality.\n\n",
            "Use the quality slider to set the overall level from Low to Maximum. ",
            "The split preview shows a side-by-side before/after comparison - drag the divider ",
            "or use Left/Right arrow keys to compare.\n\n",
            "Expand Per-Format Quality for fine control over JPEG quality, PNG compression level, ",
            "WebP quality and effort, and AVIF quality and speed."
        )),
        6 => ("Metadata", "dialog-password-symbolic", concat!(
            "Control what image metadata is kept or removed.\n\n",
            "- Strip All: remove everything for smallest files and maximum privacy\n",
            "- Privacy: strip GPS and camera serial, keep copyright\n",
            "- Photographer: keep copyright and camera model, strip GPS and software\n",
            "- Keep All: preserve all original metadata\n",
            "- Custom: choose exactly which categories to strip (GPS, camera, software, timestamps, copyright)"
        )),
        7 => ("Watermark", "emblem-photos-symbolic", concat!(
            "Add a text or image watermark with a live preview.\n\n",
            "Text mode: enter your text, choose a font and size.\n",
            "Image mode: select a logo file (PNG with transparency works best).\n\n",
            "Position the watermark using the 3x3 grid. Expand Advanced Options for text color, ",
            "opacity, rotation, tiling, margin, and scale controls."
        )),
        8 => ("Rename", "document-edit-symbolic", concat!(
            "Rename output files with a live preview showing before and after names.\n\n",
            "Simple options: add a prefix or suffix, replace spaces, filter special characters, ",
            "convert case, and add a sequential counter.\n\n",
            "Expand Advanced for a template engine with variables like {name}, {counter}, {date}, ",
            "{exif_date}, {camera}, {width}, {height}, and more. Also includes find-and-replace with regex."
        )),
        9 => ("Output", "folder-download-symbolic", concat!(
            "Review and start processing.\n\n",
            "The operation summary lists all enabled steps and their settings. ",
            "Choose an output folder or use the default 'processed' subfolder.\n\n",
            "Toggle Preserve Directory Structure to keep subfolder hierarchy in output. ",
            "Set overwrite behavior for existing files. Press Process or Ctrl+Enter to start."
        )),
        _ => ("Help", "help-about-symbolic", "No help available for this step."),
    };

    let dialog = adw::Dialog::builder()
        .title(format!("Help: {}", title))
        .content_width(420)
        .content_height(360)
        .build();

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

    let icon = gtk::Image::builder()
        .icon_name(icon_name)
        .pixel_size(64)
        .halign(gtk::Align::Center)
        .build();
    icon.add_css_class("accent");
    icon.set_accessible_role(gtk::AccessibleRole::Presentation);
    content.append(&icon);

    let heading = gtk::Label::builder()
        .label(title)
        .css_classes(["title-2"])
        .halign(gtk::Align::Center)
        .build();
    content.append(&heading);

    let body_label = gtk::Label::builder()
        .label(body)
        .wrap(true)
        .halign(gtk::Align::Center)
        .justify(gtk::Justification::Center)
        .xalign(0.5)
        .vexpand(true)
        .build();
    content.append(&body_label);

    let close_button = gtk::Button::builder()
        .label("Got it")
        .halign(gtk::Align::Center)
        .build();
    close_button.add_css_class("suggested-action");
    close_button.add_css_class("pill");

    let dlg = dialog.clone();
    close_button.connect_clicked(move |_| {
        dlg.close();
    });

    content.append(&close_button);

    dialog.set_child(Some(&content));
    dialog.present(Some(window));
}

fn format_duration(ms: u64) -> String {
    if ms < 1000 {
        format!("{}ms", ms)
    } else if ms < 60_000 {
        format!("{:.1}s", ms as f64 / 1000.0)
    } else {
        let mins = ms / 60_000;
        let secs = (ms % 60_000) / 1000;
        format!("{}m {}s", mins, secs)
    }
}

fn build_watch_folder_panel() -> gtk::Box {
    let panel = gtk::Box::builder()
        .orientation(gtk::Orientation::Vertical)
        .spacing(0)
        .build();

    panel.append(&gtk::Separator::new(gtk::Orientation::Horizontal));

    let inner = gtk::Box::builder()
        .orientation(gtk::Orientation::Vertical)
        .spacing(8)
        .margin_top(8)
        .margin_bottom(8)
        .margin_start(12)
        .margin_end(12)
        .build();

    let header_box = gtk::Box::builder()
        .orientation(gtk::Orientation::Horizontal)
        .spacing(8)
        .build();

    let header_label = gtk::Label::builder()
        .label("Watch Folders")
        .css_classes(["heading"])
        .hexpand(true)
        .halign(gtk::Align::Start)
        .build();
    header_box.append(&header_label);

    // Quick add button
    let add_btn = gtk::Button::builder()
        .icon_name("list-add-symbolic")
        .tooltip_text("Add watch folder")
        .build();
    add_btn.add_css_class("flat");
    add_btn.update_property(&[
        gtk::accessible::Property::Label("Add watch folder"),
    ]);
    header_box.append(&add_btn);

    inner.append(&header_box);

    // List of active watch folders
    let list_box = gtk::ListBox::builder()
        .selection_mode(gtk::SelectionMode::None)
        .css_classes(["boxed-list"])
        .build();

    let empty_label = gtk::Label::builder()
        .label("No watch folders active. Click + to add one.")
        .css_classes(["dim-label", "caption"])
        .halign(gtk::Align::Center)
        .wrap(true)
        .margin_top(4)
        .margin_bottom(4)
        .build();

    // Populate from config
    let config_store = pixstrip_core::storage::ConfigStore::new();
    let config = config_store.load().unwrap_or_default();
    let has_folders = !config.watch_folders.is_empty();

    for folder in &config.watch_folders {
        if !folder.active {
            continue;
        }
        let display_name = folder.path.file_name()
            .and_then(|n| n.to_str())
            .unwrap_or_else(|| folder.path.to_str().unwrap_or("Unknown"));

        let row = adw::ActionRow::builder()
            .title(display_name)
            .subtitle(&folder.preset_name)
            .build();
        let folder_icon = gtk::Image::from_icon_name("folder-visiting-symbolic");
        folder_icon.set_accessible_role(gtk::AccessibleRole::Presentation);
        row.add_prefix(&folder_icon);

        // Status indicator
        let status_icon = gtk::Image::builder()
            .icon_name("emblem-ok-symbolic")
            .pixel_size(12)
            .build();
        status_icon.set_accessible_role(gtk::AccessibleRole::Presentation);
        let status = gtk::Label::builder()
            .label("Watching")
            .css_classes(["caption", "accent"])
            .valign(gtk::Align::Center)
            .build();
        let status_box = gtk::Box::builder()
            .orientation(gtk::Orientation::Horizontal)
            .spacing(4)
            .valign(gtk::Align::Center)
            .build();
        status_box.append(&status_icon);
        status_box.append(&status);
        row.add_suffix(&status_box);

        list_box.append(&row);
    }

    empty_label.set_visible(!has_folders || config.watch_folders.iter().all(|f| !f.active));
    list_box.set_visible(has_folders && config.watch_folders.iter().any(|f| f.active));

    inner.append(&list_box);
    inner.append(&empty_label);

    // Wire add button to open a folder chooser directly
    {
        let list_box_c = list_box.clone();
        let empty_label_c = empty_label.clone();
        add_btn.connect_clicked(move |btn| {
            let list_box_c = list_box_c.clone();
            let empty_label_c = empty_label_c.clone();
            let dialog = gtk::FileDialog::builder()
                .title("Choose Watch Folder")
                .modal(true)
                .build();

            if let Some(window) = btn.root().and_then(|r| r.downcast::<gtk::Window>().ok()) {
                dialog.select_folder(Some(&window), gtk::gio::Cancellable::NONE, move |result| {
                    if let Ok(file) = result
                        && let Some(path) = file.path()
                    {
                        // Use first available preset name
                        let default_preset = pixstrip_core::preset::Preset::all_builtins()
                            .first()
                            .map(|p| p.name.clone())
                            .unwrap_or_else(|| "Blog Photos".to_string());
                        let new_folder = pixstrip_core::watcher::WatchFolder {
                            path: path.clone(),
                            preset_name: default_preset,
                            recursive: false,
                            active: true,
                        };

                        // Save to config
                        let config_store = pixstrip_core::storage::ConfigStore::new();
                        let mut config = config_store.load().unwrap_or_default();
                        // Avoid duplicates
                        if !config.watch_folders.iter().any(|f| f.path == new_folder.path) {
                            config.watch_folders.push(new_folder.clone());
                            let _ = config_store.save(&config);
                        }

                        // Add row to the panel list
                        let display_name = path.file_name()
                            .and_then(|n| n.to_str())
                            .unwrap_or_else(|| path.to_str().unwrap_or("Unknown"))
                            .to_string();

                        let row = adw::ActionRow::builder()
                            .title(&display_name)
                            .subtitle(&new_folder.preset_name)
                            .build();
                        let folder_icon = gtk::Image::from_icon_name("folder-visiting-symbolic");
                        folder_icon.set_accessible_role(gtk::AccessibleRole::Presentation);
                        row.add_prefix(&folder_icon);

                        let dyn_status_icon = gtk::Image::builder()
                            .icon_name("emblem-ok-symbolic")
                            .pixel_size(12)
                            .build();
                        dyn_status_icon.set_accessible_role(gtk::AccessibleRole::Presentation);
                        let status = gtk::Label::builder()
                            .label("Watching")
                            .css_classes(["caption", "accent"])
                            .valign(gtk::Align::Center)
                            .build();
                        let dyn_status_box = gtk::Box::builder()
                            .orientation(gtk::Orientation::Horizontal)
                            .spacing(4)
                            .valign(gtk::Align::Center)
                            .build();
                        dyn_status_box.append(&dyn_status_icon);
                        dyn_status_box.append(&status);
                        row.add_suffix(&dyn_status_box);

                        list_box_c.append(&row);
                        list_box_c.set_visible(true);
                        empty_label_c.set_visible(false);
                    }
                });
            }
        });
    }

    panel.append(&inner);
    panel
}

fn build_queue_panel(_state: &AppState) -> (gtk::Box, gtk::ListBox) {
    let panel = gtk::Box::builder()
        .orientation(gtk::Orientation::Vertical)
        .spacing(0)
        .width_request(260)
        .build();

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

    let header_label = gtk::Label::builder()
        .label("Batch Queue")
        .css_classes(["heading"])
        .hexpand(true)
        .halign(gtk::Align::Start)
        .build();
    header_box.append(&header_label);

    panel.append(&header_box);
    panel.append(&gtk::Separator::new(gtk::Orientation::Horizontal));

    // Empty state
    let empty_label = gtk::Label::builder()
        .label("No batches queued")
        .css_classes(["dim-label"])
        .vexpand(true)
        .valign(gtk::Align::Center)
        .halign(gtk::Align::Center)
        .build();

    let list_box = gtk::ListBox::builder()
        .selection_mode(gtk::SelectionMode::None)
        .css_classes(["boxed-list"])
        .margin_start(8)
        .margin_end(8)
        .margin_top(8)
        .visible(false)
        .build();

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

    let inner = gtk::Box::builder()
        .orientation(gtk::Orientation::Vertical)
        .spacing(8)
        .build();
    inner.append(&empty_label);
    inner.append(&list_box);
    scrolled.set_child(Some(&inner));

    panel.append(&scrolled);

    (panel, list_box)
}

fn refresh_queue_list(ui: &WizardUi) {
    let list_box = &ui.queue_list_box;

    // Remove all existing rows
    while let Some(child) = list_box.first_child() {
        list_box.remove(&child);
    }

    let queue = ui.state.batch_queue.borrow();

    // Toggle empty state visibility
    if let Some(parent) = list_box.parent() {
        // Find sibling empty label
        if let Some(first_child) = parent.first_child() {
            if let Some(label) = first_child.downcast_ref::<gtk::Label>() {
                label.set_visible(queue.batches.is_empty());
            }
        }
    }
    list_box.set_visible(!queue.batches.is_empty());

    for (_i, batch) in queue.batches.iter().enumerate() {
        let status_icon = match &batch.status {
            BatchStatus::Pending => "content-loading-symbolic",
            BatchStatus::Processing => "emblem-synchronizing-symbolic",
            BatchStatus::Completed => "emblem-ok-symbolic",
            BatchStatus::Failed(_) => "dialog-error-symbolic",
        };

        let status_text = match &batch.status {
            BatchStatus::Pending => "Pending".to_string(),
            BatchStatus::Processing => "Processing...".to_string(),
            BatchStatus::Completed => "Completed".to_string(),
            BatchStatus::Failed(e) => format!("Failed: {}", e),
        };

        let row = adw::ActionRow::builder()
            .title(&batch.name)
            .subtitle(&format!("{} images - {}", batch.files.len(), status_text))
            .build();
        let batch_icon = gtk::Image::from_icon_name(status_icon);
        batch_icon.update_property(&[
            gtk::accessible::Property::Label(&status_text),
        ]);
        row.add_prefix(&batch_icon);

        // Add remove button for pending batches
        if batch.status == BatchStatus::Pending {
            let remove_btn = gtk::Button::builder()
                .icon_name("user-trash-symbolic")
                .tooltip_text("Remove from queue")
                .valign(gtk::Align::Center)
                .build();
            remove_btn.add_css_class("flat");

            let queue_ref = ui.state.batch_queue.clone();
            let ui_clone = ui.clone();
            let batch_name = batch.name.clone();
            remove_btn.connect_clicked(move |_| {
                let mut q = queue_ref.borrow_mut();
                if let Some(pos) = q.batches.iter().position(|b| b.name == batch_name && b.status == BatchStatus::Pending) {
                    q.batches.remove(pos);
                }
                drop(q);
                refresh_queue_list(&ui_clone);
            });

            row.add_suffix(&remove_btn);
        }

        list_box.append(&row);
    }

    // Show/hide queue button badge
    let has_pending = queue.batches.iter().any(|b| b.status == BatchStatus::Pending);
    ui.queue_button.set_visible(!queue.batches.is_empty() || has_pending);
}

fn add_current_batch_to_queue(ui: &WizardUi) {
    let files: Vec<std::path::PathBuf> = {
        let loaded = ui.state.loaded_files.borrow();
        let excluded = ui.state.excluded_files.borrow();
        loaded.iter().filter(|p| !excluded.contains(*p)).cloned().collect()
    };

    if files.is_empty() {
        ui.toast_overlay.add_toast(adw::Toast::new("No images to queue. Go to Step 2 to add images first."));
        return;
    }

    let output_dir = ui.state.output_dir.borrow().clone()
        .unwrap_or_else(|| {
            files[0].parent()
                .unwrap_or_else(|| std::path::Path::new("."))
                .join("processed")
        });

    let config = ui.state.job_config.borrow().clone();
    let batch_num = ui.state.batch_queue.borrow().batches.len() + 1;

    let batch = QueuedBatch {
        name: format!("Batch {}", batch_num),
        files,
        output_dir,
        job_config: config,
        status: BatchStatus::Pending,
    };

    ui.state.batch_queue.borrow_mut().batches.push(batch);
    ui.queue_button.set_visible(true);
    refresh_queue_list(ui);
    ui.toast_overlay.add_toast(adw::Toast::new("Batch added to queue"));
}

/// Check for pending batches in the queue and start processing the next one.
/// Returns true if a batch was started.
fn process_next_queued_batch(ui: &WizardUi) -> bool {
    // Find the first pending batch
    let next_idx = {
        let queue = ui.state.batch_queue.borrow();
        queue.batches.iter().position(|b| b.status == BatchStatus::Pending)
    };

    let Some(idx) = next_idx else {
        return false;
    };

    // Mark it as processing
    let batch = {
        let mut queue = ui.state.batch_queue.borrow_mut();
        queue.batches[idx].status = BatchStatus::Processing;
        queue.batches[idx].clone()
    };
    refresh_queue_list(ui);

    // Load the batch's config into state so start_processing uses it
    *ui.state.job_config.borrow_mut() = batch.job_config;
    *ui.state.loaded_files.borrow_mut() = batch.files;
    ui.state.excluded_files.borrow_mut().clear();
    *ui.state.output_dir.borrow_mut() = Some(batch.output_dir);

    ui.toast_overlay.add_toast(adw::Toast::new(&format!("Starting queued batch: {}", batch.name)));

    // Pop to a clean state if we're on results page
    while let Some(page) = ui.nav_view.visible_page() {
        let tag = page.tag();
        if tag.as_deref() == Some("processing") || tag.as_deref() == Some("results") {
            ui.nav_view.pop();
        } else {
            break;
        }
    }

    // Store the batch index for marking completion
    let batch_idx = idx;
    let ui_clone = ui.clone();

    // Start processing with a small delay so the UI updates
    glib::timeout_add_local_once(std::time::Duration::from_millis(100), move || {
        if let Some(root) = ui_clone.nav_view.root()
            && let Some(win) = root.downcast_ref::<adw::ApplicationWindow>()
        {
            run_processing(win, &ui_clone);
        }
    });

    // We need to mark the batch as completed when processing finishes.
    // This is handled in show_results via the queue check.
    let _ = batch_idx;

    true
}

/// Mark the currently-processing batch in the queue as completed or failed.
fn mark_current_queue_batch(ui: &WizardUi, success: bool, error_msg: Option<&str>) {
    let mut queue = ui.state.batch_queue.borrow_mut();
    if let Some(batch) = queue.batches.iter_mut().find(|b| b.status == BatchStatus::Processing) {
        batch.status = if success {
            BatchStatus::Completed
        } else {
            BatchStatus::Failed(error_msg.unwrap_or("Unknown error").to_string())
        };
    }
    drop(queue);
    refresh_queue_list(ui);
}
