use adw::prelude::*;
use gtk::glib;
use std::cell::{Cell, RefCell};
use std::rc::Rc;

use crate::app::AppState;
use crate::utils::format_size;
use pixstrip_core::types::{ImageFormat, QualityPreset};

/// Which format and quality to use for the compressed side of the preview.
#[derive(Clone, Copy)]
enum PreviewCompression {
    Jpeg(u8),
    Png(u8),
    WebP(u8),
    Avif(u8),
}

pub fn build_compress_page(state: &AppState) -> adw::NavigationPage {
    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 cfg = state.job_config.borrow();

    // Enable toggle
    let enable_row = adw::SwitchRow::builder()
        .title("Enable Compression")
        .subtitle("Reduce file size with quality control")
        .active(cfg.compress_enabled)
        .tooltip_text("Toggle compression on or off")
        .build();

    let enable_group = adw::PreferencesGroup::new();
    enable_group.add(&enable_row);
    content.append(&enable_group);

    // --- Quality slider (1-8 range: Low to Maximum) ---
    let quality_group = adw::PreferencesGroup::builder()
        .title("Quality Level")
        .description("Higher quality means larger files. This sets the overall quality target.")
        .build();

    let initial_val = match cfg.quality_preset {
        QualityPreset::Low | QualityPreset::WebOptimized => 1.0,
        QualityPreset::Medium => 3.0,
        QualityPreset::High => 5.0,
        QualityPreset::Maximum => 8.0,
    };

    let quality_scale = gtk::Scale::builder()
        .orientation(gtk::Orientation::Horizontal)
        .adjustment(&gtk::Adjustment::new(initial_val, 1.0, 8.0, 1.0, 1.0, 0.0))
        .draw_value(false)
        .hexpand(true)
        .build();
    quality_scale.set_round_digits(0);

    quality_scale.add_mark(1.0, gtk::PositionType::Bottom, Some("Low"));
    quality_scale.add_mark(2.0, gtk::PositionType::Bottom, None);
    quality_scale.add_mark(3.0, gtk::PositionType::Bottom, Some("Medium"));
    quality_scale.add_mark(4.0, gtk::PositionType::Bottom, None);
    quality_scale.add_mark(5.0, gtk::PositionType::Bottom, Some("High"));
    quality_scale.add_mark(6.0, gtk::PositionType::Bottom, None);
    quality_scale.add_mark(7.0, gtk::PositionType::Bottom, None);
    quality_scale.add_mark(8.0, gtk::PositionType::Bottom, Some("Max"));
    quality_scale.update_property(&[
        gtk::accessible::Property::Label("Compression quality, from Low to Maximum"),
    ]);

    let quality_label = gtk::Label::builder()
        .label(quality_description(initial_val as u32))
        .css_classes(["dim-label"])
        .margin_top(4)
        .build();

    let quality_box = gtk::Box::builder()
        .orientation(gtk::Orientation::Vertical)
        .spacing(4)
        .margin_top(8)
        .margin_bottom(8)
        .margin_start(12)
        .margin_end(12)
        .build();
    quality_box.append(&quality_scale);
    quality_box.append(&quality_label);

    quality_group.add(&quality_box);
    content.append(&quality_group);

    // --- Per-format quality (collapsible advanced section) ---
    let performat_group = adw::PreferencesGroup::builder()
        .title("Per-Format Quality")
        .build();

    let performat_expander = adw::ExpanderRow::builder()
        .title("Per-Format Quality")
        .subtitle("Fine-tune quality for each output format individually")
        .show_enable_switch(false)
        .expanded(state.is_section_expanded("compress-performat"))
        .build();

    {
        let st = state.clone();
        performat_expander.connect_expanded_notify(move |row| {
            st.set_section_expanded("compress-performat", row.is_expanded());
        });
    }

    // All per-format scales: no marks, uniform fixed width, value shown in subtitle
    let setup_scale = |scale: &gtk::Scale| {
        scale.set_draw_value(false);
        scale.set_hexpand(false);
        scale.set_valign(gtk::Align::Center);
        scale.set_width_request(200);
    };

    // JPEG quality (1-100)
    let jpeg_row = adw::ActionRow::builder()
        .title("JPEG Quality")
        .subtitle(&format!("{}", cfg.jpeg_quality))
        .build();
    let jpeg_scale = gtk::Scale::with_range(gtk::Orientation::Horizontal, 1.0, 100.0, 1.0);
    jpeg_scale.set_value(cfg.jpeg_quality as f64);
    setup_scale(&jpeg_scale);
    jpeg_scale.update_property(&[gtk::accessible::Property::Label("JPEG quality, 1 to 100")]);
    jpeg_row.add_suffix(&jpeg_scale);

    // PNG compression level (1-6)
    let png_row = adw::ActionRow::builder()
        .title("PNG Compression")
        .subtitle(&format!("{}", cfg.png_level))
        .build();
    let png_scale = gtk::Scale::with_range(gtk::Orientation::Horizontal, 1.0, 6.0, 1.0);
    png_scale.set_value(cfg.png_level as f64);
    png_scale.set_round_digits(0);
    setup_scale(&png_scale);
    png_scale.update_property(&[gtk::accessible::Property::Label("PNG compression level, 1 to 6")]);
    png_row.add_suffix(&png_scale);

    // WebP quality (1-100)
    let webp_row = adw::ActionRow::builder()
        .title("WebP Quality")
        .subtitle(&format!("{}", cfg.webp_quality))
        .build();
    let webp_scale = gtk::Scale::with_range(gtk::Orientation::Horizontal, 1.0, 100.0, 1.0);
    webp_scale.set_value(cfg.webp_quality as f64);
    setup_scale(&webp_scale);
    webp_scale.update_property(&[gtk::accessible::Property::Label("WebP quality, 1 to 100")]);
    webp_row.add_suffix(&webp_scale);

    // WebP encoding effort (0-6)
    let webp_effort_row = adw::ActionRow::builder()
        .title("WebP Encoding Effort")
        .subtitle(&format!("{}", cfg.webp_effort))
        .build();
    let webp_effort_scale = gtk::Scale::with_range(gtk::Orientation::Horizontal, 0.0, 6.0, 1.0);
    webp_effort_scale.set_value(cfg.webp_effort as f64);
    webp_effort_scale.set_round_digits(0);
    setup_scale(&webp_effort_scale);
    webp_effort_scale.update_property(&[gtk::accessible::Property::Label("WebP encoding effort, 0 to 6")]);
    webp_effort_row.add_suffix(&webp_effort_scale);

    // AVIF quality (1-100)
    let avif_row = adw::ActionRow::builder()
        .title("AVIF Quality")
        .subtitle(&format!("{}", cfg.avif_quality))
        .build();
    let avif_scale = gtk::Scale::with_range(gtk::Orientation::Horizontal, 1.0, 100.0, 1.0);
    avif_scale.set_value(cfg.avif_quality as f64);
    setup_scale(&avif_scale);
    avif_scale.update_property(&[gtk::accessible::Property::Label("AVIF quality, 1 to 100")]);
    avif_row.add_suffix(&avif_scale);

    // AVIF encoding speed (1-10)
    let avif_speed_row = adw::ActionRow::builder()
        .title("AVIF Encoding Speed")
        .subtitle(&format!("{}", cfg.avif_speed))
        .build();
    let avif_speed_scale = gtk::Scale::with_range(gtk::Orientation::Horizontal, 1.0, 10.0, 1.0);
    avif_speed_scale.set_value(cfg.avif_speed as f64);
    avif_speed_scale.set_round_digits(0);
    setup_scale(&avif_speed_scale);
    avif_speed_scale.update_property(&[gtk::accessible::Property::Label("AVIF encoding speed, 1 to 10")]);
    avif_speed_row.add_suffix(&avif_speed_scale);

    performat_expander.add_row(&jpeg_row);
    performat_expander.add_row(&png_row);
    performat_expander.add_row(&webp_row);
    performat_expander.add_row(&webp_effort_row);
    performat_expander.add_row(&avif_row);
    performat_expander.add_row(&avif_speed_row);

    performat_group.add(&performat_expander);
    content.append(&performat_group);

    // --- Compression preview - Squoosh-style split comparison ---
    let preview_group = adw::PreferencesGroup::builder()
        .title("Quality Preview")
        .description("Drag the divider to compare. Drag the image to pan.")
        .build();

    // Size labels above the preview
    let size_box = gtk::CenterBox::builder()
        .margin_start(12)
        .margin_end(12)
        .margin_bottom(4)
        .build();
    let original_size_label = gtk::Label::builder()
        .label("Original")
        .css_classes(["dim-label", "caption"])
        .halign(gtk::Align::Start)
        .build();
    let compressed_size_label = gtk::Label::builder()
        .label("Compressed")
        .css_classes(["dim-label", "caption"])
        .halign(gtk::Align::End)
        .build();
    size_box.set_start_widget(Some(&original_size_label));
    size_box.set_end_widget(Some(&compressed_size_label));

    // State for the split preview
    let divider_pos = Rc::new(RefCell::new(0.5f64));
    let original_pixbuf: Rc<RefCell<Option<gtk::gdk_pixbuf::Pixbuf>>> = Rc::new(RefCell::new(None));
    let compressed_pixbuf: Rc<RefCell<Option<gtk::gdk_pixbuf::Pixbuf>>> = Rc::new(RefCell::new(None));
    let divider_dragging = Rc::new(Cell::new(false));
    let image_dragging = Rc::new(Cell::new(false));
    let divider_hint_visible = Rc::new(Cell::new(true));

    // Pan state for cover-fill preview
    let pan_x: Rc<Cell<f64>> = Rc::new(Cell::new(0.0));
    let pan_y: Rc<Cell<f64>> = Rc::new(Cell::new(0.0));
    let drag_start_pan_x: Rc<Cell<f64>> = Rc::new(Cell::new(0.0));
    let drag_start_pan_y: Rc<Cell<f64>> = Rc::new(Cell::new(0.0));
    let img_dims: Rc<Cell<(f64, f64)>> = Rc::new(Cell::new((0.0, 0.0)));

    // Current preview compression mode
    let preview_comp: Rc<Cell<PreviewCompression>> = Rc::new(Cell::new(
        PreviewCompression::Jpeg(cfg.jpeg_quality),
    ));

    let preview_drawing = gtk::DrawingArea::builder()
        .height_request(400)
        .hexpand(true)
        .vexpand(true)
        .build();
    preview_drawing.update_property(&[
        gtk::accessible::Property::Label("Compression quality comparison. Drag the vertical divider to compare original and compressed image. Drag elsewhere to pan."),
    ]);

    // Hint label shown over the preview until the user first interacts with the divider
    let divider_hint_label = gtk::Label::builder()
        .label("Drag the divider to compare before and after")
        .css_classes(["dim-label", "caption"])
        .halign(gtk::Align::Center)
        .valign(gtk::Align::Center)
        .build();
    divider_hint_label.update_property(&[
        gtk::accessible::Property::Label("Hint: drag the divider to compare before and after compression"),
    ]);

    // Draw function - cover fill with pan support
    {
        let dp = divider_pos.clone();
        let orig = original_pixbuf.clone();
        let comp = compressed_pixbuf.clone();
        let px = pan_x.clone();
        let py = pan_y.clone();
        preview_drawing.set_draw_func(move |_drawing, cr, width, height| {
            let w = width as f64;
            let h = height as f64;

            // Background
            cr.set_source_rgba(0.2, 0.2, 0.2, 1.0);
            let _ = cr.paint();

            let pos = *dp.borrow();
            let divider_x = w * pos;

            // Helper to draw a pixbuf with cover fill and pan offset
            let draw_pixbuf = |cr: &gtk::cairo::Context,
                               pixbuf: &gtk::gdk_pixbuf::Pixbuf,
                               clip_x: f64,
                               clip_w: f64| {
                let tw = pixbuf.width() as f64;
                let th = pixbuf.height() as f64;
                let scale = (w / tw).max(h / th);
                let max_pan_x = ((tw * scale - w) / 2.0).max(0.0);
                let max_pan_y = ((th * scale - h) / 2.0).max(0.0);
                let clamped_px = px.get().clamp(-max_pan_x, max_pan_x);
                let clamped_py = py.get().clamp(-max_pan_y, max_pan_y);
                let ox = (w - tw * scale) / 2.0 + clamped_px;
                let oy = (h - th * scale) / 2.0 + clamped_py;

                let _ = cr.save();
                cr.rectangle(clip_x, 0.0, clip_w, h);
                cr.clip();
                cr.translate(ox, oy);
                cr.scale(scale, scale);
                cr.set_source_pixbuf(pixbuf, 0.0, 0.0);
                let _ = cr.paint();
                let _ = cr.restore();
            };

            if let Some(ref pb) = *orig.borrow() {
                draw_pixbuf(cr, pb, 0.0, divider_x);
            }
            if let Some(ref pb) = *comp.borrow() {
                draw_pixbuf(cr, pb, divider_x, w - divider_x);
            }

            // Divider line
            cr.set_source_rgba(1.0, 1.0, 1.0, 0.9);
            cr.set_line_width(2.0);
            cr.move_to(divider_x, 0.0);
            cr.line_to(divider_x, h);
            let _ = cr.stroke();

            // Handle circle
            cr.arc(divider_x, h / 2.0, 12.0, 0.0, std::f64::consts::TAU);
            cr.set_source_rgba(1.0, 1.0, 1.0, 0.95);
            let _ = cr.fill_preserve();
            cr.set_source_rgba(0.3, 0.3, 0.3, 1.0);
            cr.set_line_width(1.5);
            let _ = cr.stroke();

            // Arrows on handle
            cr.set_source_rgba(0.3, 0.3, 0.3, 1.0);
            cr.set_line_width(2.0);
            cr.move_to(divider_x - 4.0, h / 2.0);
            cr.line_to(divider_x - 8.0, h / 2.0);
            let _ = cr.stroke();
            cr.move_to(divider_x + 4.0, h / 2.0);
            cr.line_to(divider_x + 8.0, h / 2.0);
            let _ = cr.stroke();

            // Labels with background pills for readability
            cr.set_font_size(11.0);
            let draw_label = |cr: &gtk::cairo::Context, text: &str, x: f64, y: f64| {
                let Ok(extents) = cr.text_extents(text) else { return };
                let pad_x = 6.0;
                let pad_y = 4.0;
                let rx = x - pad_x;
                let ry = y - extents.height() - pad_y;
                let rw = extents.width() + pad_x * 2.0;
                let rh = extents.height() + pad_y * 2.0;
                let radius = 4.0;

                cr.new_sub_path();
                cr.arc(rx + rw - radius, ry + radius, radius, -std::f64::consts::FRAC_PI_2, 0.0);
                cr.arc(rx + rw - radius, ry + rh - radius, radius, 0.0, std::f64::consts::FRAC_PI_2);
                cr.arc(rx + radius, ry + rh - radius, radius, std::f64::consts::FRAC_PI_2, std::f64::consts::PI);
                cr.arc(rx + radius, ry + radius, radius, std::f64::consts::PI, 3.0 * std::f64::consts::FRAC_PI_2);
                cr.close_path();
                cr.set_source_rgba(0.0, 0.0, 0.0, 0.6);
                let _ = cr.fill();

                cr.set_source_rgba(1.0, 1.0, 1.0, 0.95);
                cr.move_to(x, y);
                let _ = cr.show_text(text);
            };
            if divider_x > 60.0 {
                draw_label(cr, "Original", 8.0, 18.0);
            }
            if w - divider_x > 80.0 {
                draw_label(cr, "Compressed", divider_x + 8.0, 18.0);
            }
        });
    }

    // Drag gesture: near divider moves divider, elsewhere pans image
    let drag_gesture = gtk::GestureDrag::new();
    {
        let dp = divider_pos.clone();
        let dd = divider_dragging.clone();
        let id = image_dragging.clone();
        let drawing = preview_drawing.clone();
        let dspx = drag_start_pan_x.clone();
        let dspy = drag_start_pan_y.clone();
        let px = pan_x.clone();
        let py = pan_y.clone();
        let hint_vis = divider_hint_visible.clone();
        let hint_lbl = divider_hint_label.clone();
        drag_gesture.connect_drag_begin(move |_, x, _| {
            let w = drawing.width() as f64;
            let current = *dp.borrow() * w;
            if (x - current).abs() < 30.0 {
                dd.set(true);
                id.set(false);
                // Hide the hint on first divider interaction
                if hint_vis.get() {
                    hint_vis.set(false);
                    hint_lbl.set_visible(false);
                }
            } else {
                dd.set(false);
                id.set(true);
                dspx.set(px.get());
                dspy.set(py.get());
            }
        });
    }
    {
        let dp = divider_pos.clone();
        let dd = divider_dragging.clone();
        let id = image_dragging.clone();
        let drawing = preview_drawing.clone();
        let px = pan_x.clone();
        let py = pan_y.clone();
        let dspx = drag_start_pan_x.clone();
        let dspy = drag_start_pan_y.clone();
        let dims = img_dims.clone();
        drag_gesture.connect_drag_update(move |gesture, offset_x, offset_y| {
            if dd.get() {
                if let Some((start_x, _)) = gesture.start_point() {
                    let w = drawing.width() as f64;
                    if w > 0.0 {
                        let new_pos = ((start_x + offset_x) / w).clamp(0.05, 0.95);
                        *dp.borrow_mut() = new_pos;
                        drawing.queue_draw();
                    }
                }
            } else if id.get() {
                let (tw, th) = dims.get();
                let w = drawing.width() as f64;
                let h = drawing.height() as f64;
                if tw > 0.0 && th > 0.0 && w > 0.0 && h > 0.0 {
                    let scale = (w / tw).max(h / th);
                    let max_px = ((tw * scale - w) / 2.0).max(0.0);
                    let max_py = ((th * scale - h) / 2.0).max(0.0);
                    let new_x = (dspx.get() + offset_x).clamp(-max_px, max_px);
                    let new_y = (dspy.get() + offset_y).clamp(-max_py, max_py);
                    px.set(new_x);
                    py.set(new_y);
                    drawing.queue_draw();
                }
            }
        });
    }
    {
        let dd = divider_dragging.clone();
        let id = image_dragging.clone();
        drag_gesture.connect_drag_end(move |_, _, _| {
            dd.set(false);
            id.set(false);
        });
    }
    preview_drawing.add_controller(drag_gesture);

    // Keyboard support for divider: Left/Right to move divider, Space to reset to center
    {
        let dp = divider_pos.clone();
        let drawing = preview_drawing.clone();
        let hint_vis = divider_hint_visible.clone();
        let hint_lbl = divider_hint_label.clone();
        let key = gtk::EventControllerKey::new();
        key.connect_key_pressed(move |_, keyval, _, _| {
            let step = 0.02;
            match keyval {
                gtk::gdk::Key::Left => {
                    let new_pos = (*dp.borrow() - step).clamp(0.05, 0.95);
                    *dp.borrow_mut() = new_pos;
                    drawing.queue_draw();
                    if hint_vis.get() {
                        hint_vis.set(false);
                        hint_lbl.set_visible(false);
                    }
                    return gtk::glib::Propagation::Stop;
                }
                gtk::gdk::Key::Right => {
                    let new_pos = (*dp.borrow() + step).clamp(0.05, 0.95);
                    *dp.borrow_mut() = new_pos;
                    drawing.queue_draw();
                    if hint_vis.get() {
                        hint_vis.set(false);
                        hint_lbl.set_visible(false);
                    }
                    return gtk::glib::Propagation::Stop;
                }
                gtk::gdk::Key::space => {
                    *dp.borrow_mut() = 0.5;
                    drawing.queue_draw();
                    if hint_vis.get() {
                        hint_vis.set(false);
                        hint_lbl.set_visible(false);
                    }
                    return gtk::glib::Propagation::Stop;
                }
                _ => {}
            }
            gtk::glib::Propagation::Proceed
        });
        preview_drawing.set_focusable(true);
        preview_drawing.add_controller(key);
    }

    let preview_overlay = gtk::Overlay::builder()
        .child(&preview_drawing)
        .build();
    preview_overlay.add_overlay(&divider_hint_label);

    let preview_frame = gtk::Frame::builder()
        .halign(gtk::Align::Fill)
        .build();
    preview_frame.set_child(Some(&preview_overlay));

    preview_group.add(&size_box);
    preview_group.add(&preview_frame);

    // Thumbnail strip for selecting preview image
    let thumb_scrolled = gtk::ScrolledWindow::builder()
        .hscrollbar_policy(gtk::PolicyType::Automatic)
        .vscrollbar_policy(gtk::PolicyType::Never)
        .max_content_height(60)
        .build();

    let thumb_box = gtk::Box::builder()
        .orientation(gtk::Orientation::Horizontal)
        .spacing(4)
        .margin_top(4)
        .margin_bottom(4)
        .halign(gtk::Align::Center)
        .build();
    thumb_scrolled.set_child(Some(&thumb_box));

    let preview_index: Rc<RefCell<usize>> = Rc::new(RefCell::new(0));

    {
        let files = state.loaded_files.borrow();
        let max_thumbs = files.len().min(10);
        for i in 0..max_thumbs {
            let pic = gtk::Picture::builder()
                .content_fit(gtk::ContentFit::Cover)
                .width_request(50)
                .height_request(50)
                .build();
            pic.set_filename(Some(&files[i]));
            let frame = gtk::Frame::builder()
                .child(&pic)
                .build();
            if i == 0 {
                frame.add_css_class("accent");
            }

            let file_name = files[i].file_name().and_then(|n| n.to_str()).unwrap_or("image");
            let btn = gtk::Button::builder()
                .child(&frame)
                .has_frame(false)
                .tooltip_text(file_name)
                .build();
            let selected_label = if i == 0 { "currently selected" } else { "" };
            btn.update_property(&[
                gtk::accessible::Property::Label(
                    &if selected_label.is_empty() {
                        format!("Preview thumbnail: {}", file_name)
                    } else {
                        format!("Preview thumbnail: {} ({})", file_name, selected_label)
                    }
                ),
            ]);

            thumb_box.append(&btn);
        }
        thumb_scrolled.set_visible(max_thumbs > 1);
    }

    let no_image_label = gtk::Label::builder()
        .label("Add images first to see compression preview")
        .css_classes(["dim-label"])
        .halign(gtk::Align::Center)
        .margin_top(4)
        .build();
    preview_group.add(&no_image_label);
    preview_group.add(&thumb_scrolled);

    content.append(&preview_group);

    drop(cfg);

    // --- Wire signals ---

    {
        let jc = state.job_config.clone();
        enable_row.connect_active_notify(move |row| {
            jc.borrow_mut().compress_enabled = row.is_active();
        });
    }

    // Preview update closure - reads compression mode from preview_comp
    let preview_gen: Rc<Cell<u32>> = Rc::new(Cell::new(0));
    let update_preview = {
        let files = state.loaded_files.clone();
        let orig_pb = original_pixbuf.clone();
        let comp_pb = compressed_pixbuf.clone();
        let drawing = preview_drawing.clone();
        let orig_label = original_size_label.clone();
        let comp_label = compressed_size_label.clone();
        let no_img_label = no_image_label.clone();
        let pidx = preview_index.clone();
        let dims = img_dims.clone();
        let px = pan_x.clone();
        let py = pan_y.clone();
        let pc = preview_comp.clone();
        let bind_gen = preview_gen.clone();

        Rc::new(move |reset_pan: bool| {
            let loaded = files.borrow();
            if loaded.is_empty() {
                no_img_label.set_visible(true);
                return;
            }
            no_img_label.set_visible(false);

            let idx = (*pidx.borrow()).min(loaded.len().saturating_sub(1));
            let sample_path = loaded[idx].clone();
            let comp = pc.get();

            if reset_pan {
                px.set(0.0);
                py.set(0.0);
            }

            let my_gen = bind_gen.get().wrapping_add(1);
            bind_gen.set(my_gen);
            let gen_check = bind_gen.clone();

            let orig_pb = orig_pb.clone();
            let comp_pb = comp_pb.clone();
            let drawing = drawing.clone();
            let orig_label = orig_label.clone();
            let comp_label = comp_label.clone();
            let dims = dims.clone();

            let (tx, rx) = std::sync::mpsc::channel::<PreviewResult>();

            std::thread::spawn(move || {
                let result = generate_preview(&sample_path, comp);
                let _ = tx.send(result);
            });

            glib::timeout_add_local(std::time::Duration::from_millis(100), move || {
                if gen_check.get() != my_gen {
                    return glib::ControlFlow::Break;
                }
                match rx.try_recv() {
                    Ok(result) => {
                        match result {
                            PreviewResult::Success {
                                original_bytes,
                                compressed_bytes,
                                original_size,
                                compressed_size,
                                format_label,
                            } => {
                                if let Ok(pb) = load_pixbuf_from_bytes(&original_bytes) {
                                    dims.set((pb.width() as f64, pb.height() as f64));
                                    *orig_pb.borrow_mut() = Some(pb);
                                }
                                if let Ok(pb) = load_pixbuf_from_bytes(&compressed_bytes) {
                                    *comp_pb.borrow_mut() = Some(pb);
                                }

                                orig_label.set_label(&format!(
                                    "Original: {}",
                                    format_size(original_size)
                                ));
                                let savings_pct = if original_size > 0 {
                                    (1.0 - compressed_size as f64 / original_size as f64) * 100.0
                                } else {
                                    0.0
                                };
                                let savings_text = if savings_pct >= 0.0 {
                                    format!("-{:.0}%", savings_pct)
                                } else {
                                    format!("+{:.0}%", -savings_pct)
                                };
                                comp_label.set_label(&format!(
                                    "{}: {} ({})",
                                    format_label,
                                    format_size(compressed_size),
                                    savings_text
                                ));

                                drawing.queue_draw();
                            }
                            PreviewResult::Error(_) => {
                                orig_label.set_label("Original");
                                comp_label.set_label("Preview unavailable");
                            }
                        }
                        glib::ControlFlow::Break
                    }
                    Err(std::sync::mpsc::TryRecvError::Empty) => glib::ControlFlow::Continue,
                    Err(std::sync::mpsc::TryRecvError::Disconnected) => glib::ControlFlow::Break,
                }
            });
        })
    };

    // Wire thumbnail buttons
    {
        let mut child = thumb_box.first_child();
        let mut idx = 0usize;
        while let Some(widget) = child {
            if let Some(btn) = widget.downcast_ref::<gtk::Button>() {
                let pidx = preview_index.clone();
                let up = update_preview.clone();
                let tb = thumb_box.clone();
                let current_idx = idx;
                btn.connect_clicked(move |_| {
                    *pidx.borrow_mut() = current_idx;
                    up(true);
                    let mut c = tb.first_child();
                    let mut j = 0usize;
                    while let Some(w) = c {
                        if let Some(b) = w.downcast_ref::<gtk::Button>() {
                            if let Some(f) = b.child().and_then(|c| c.downcast::<gtk::Frame>().ok()) {
                                if j == current_idx {
                                    f.add_css_class("accent");
                                } else {
                                    f.remove_css_class("accent");
                                }
                            }
                        }
                        c = w.next_sibling();
                        j += 1;
                    }
                });
                idx += 1;
            }
            child = widget.next_sibling();
        }
    }

    // Initial preview load
    {
        let up = update_preview.clone();
        glib::idle_add_local_once(move || {
            up(true);
        });
    }

    // Main quality slider: update preset, sync per-format sliders, preview as JPEG
    {
        let jc = state.job_config.clone();
        let label = quality_label;
        let up = update_preview.clone();
        let pc = preview_comp.clone();
        let js = jpeg_scale.clone();
        let ps = png_scale.clone();
        let ws = webp_scale.clone();
        let wes = webp_effort_scale.clone();
        let avs = avif_scale.clone();
        let ass = avif_speed_scale.clone();
        quality_scale.connect_value_changed(move |scale| {
            let val = scale.value().round() as u32;
            let preset = match val {
                1 | 2 => QualityPreset::Low,
                3 | 4 => QualityPreset::Medium,
                5 | 6 => QualityPreset::High,
                _ => QualityPreset::Maximum,
            };
            jc.borrow_mut().quality_preset = preset;
            label.set_label(&quality_description(val));
            // Sync per-format sliders (their handlers update job_config)
            js.set_value(preset.jpeg_quality() as f64);
            ps.set_value(preset.png_level() as f64);
            ws.set_value(preset.webp_quality() as f64);
            wes.set_value(preset.webp_effort() as f64);
            avs.set_value(preset.avif_quality() as f64);
            ass.set_value(preset.avif_speed() as f64);
            // Preview as JPEG at the preset quality
            pc.set(PreviewCompression::Jpeg(preset.jpeg_quality()));
            up(false);
        });
    }

    // Per-slider debounce counters (separate to avoid cross-slider cancellation)
    let jpeg_debounce: Rc<Cell<u32>> = Rc::new(Cell::new(0));
    let png_debounce: Rc<Cell<u32>> = Rc::new(Cell::new(0));
    let webp_debounce: Rc<Cell<u32>> = Rc::new(Cell::new(0));
    let avif_debounce: Rc<Cell<u32>> = Rc::new(Cell::new(0));

    // Per-format slider handlers: update config, set preview to that format, refresh (debounced)
    {
        let jc = state.job_config.clone();
        let row = jpeg_row.clone();
        let pc = preview_comp.clone();
        let up = update_preview.clone();
        let did = jpeg_debounce.clone();
        jpeg_scale.connect_value_changed(move |scale| {
            let val = scale.value().round() as u8;
            jc.borrow_mut().jpeg_quality = val;
            row.set_subtitle(&format!("{}", val));
            pc.set(PreviewCompression::Jpeg(val));
            let up = up.clone();
            let did = did.clone();
            let id = did.get().wrapping_add(1);
            did.set(id);
            glib::timeout_add_local_once(std::time::Duration::from_millis(150), move || {
                if did.get() == id { up(false); }
            });
        });
    }
    {
        let jc = state.job_config.clone();
        let row = png_row.clone();
        let pc = preview_comp.clone();
        let up = update_preview.clone();
        let did = png_debounce.clone();
        png_scale.connect_value_changed(move |scale| {
            let val = scale.value().round() as u8;
            jc.borrow_mut().png_level = val;
            row.set_subtitle(&format!("{}", val));
            pc.set(PreviewCompression::Png(val));
            let up = up.clone();
            let did = did.clone();
            let id = did.get().wrapping_add(1);
            did.set(id);
            glib::timeout_add_local_once(std::time::Duration::from_millis(150), move || {
                if did.get() == id { up(false); }
            });
        });
    }
    {
        let jc = state.job_config.clone();
        let row = webp_row.clone();
        let pc = preview_comp.clone();
        let up = update_preview.clone();
        let did = webp_debounce.clone();
        webp_scale.connect_value_changed(move |scale| {
            let val = scale.value().round() as u8;
            jc.borrow_mut().webp_quality = val;
            row.set_subtitle(&format!("{}", val));
            pc.set(PreviewCompression::WebP(val));
            let up = up.clone();
            let did = did.clone();
            let id = did.get().wrapping_add(1);
            did.set(id);
            glib::timeout_add_local_once(std::time::Duration::from_millis(150), move || {
                if did.get() == id { up(false); }
            });
        });
    }
    {
        let jc = state.job_config.clone();
        let row = webp_effort_row.clone();
        webp_effort_scale.connect_value_changed(move |scale| {
            let val = scale.value().round() as u8;
            jc.borrow_mut().webp_effort = val;
            row.set_subtitle(&format!("{}", val));
        });
    }
    {
        let jc = state.job_config.clone();
        let row = avif_row.clone();
        let pc = preview_comp.clone();
        let up = update_preview.clone();
        let did = avif_debounce.clone();
        avif_scale.connect_value_changed(move |scale| {
            let val = scale.value().round() as u8;
            jc.borrow_mut().avif_quality = val;
            row.set_subtitle(&format!("{}", val));
            pc.set(PreviewCompression::Avif(val));
            let up = up.clone();
            let did = did.clone();
            let id = did.get().wrapping_add(1);
            did.set(id);
            glib::timeout_add_local_once(std::time::Duration::from_millis(150), move || {
                if did.get() == id { up(false); }
            });
        });
    }
    {
        let jc = state.job_config.clone();
        let row = avif_speed_row.clone();
        avif_speed_scale.connect_value_changed(move |scale| {
            let val = scale.value().round() as u8;
            jc.borrow_mut().avif_speed = val;
            row.set_subtitle(&format!("{}", val));
        });
    }

    scrolled.set_child(Some(&content));

    let page = adw::NavigationPage::builder()
        .title("Compress")
        .tag("step-compress")
        .child(&scrolled)
        .build();

    // On page map: sync enable toggle, refresh thumbnail strip, preview, and show/hide per-format rows
    {
        let up = update_preview.clone();
        let jc = state.job_config.clone();
        let pg = performat_group.clone();
        let jr = jpeg_row;
        let pr = png_row;
        let wr = webp_row;
        let wer = webp_effort_row;
        let ar = avif_row;
        let asr = avif_speed_row;
        let lf = state.loaded_files.clone();
        let tb = thumb_box.clone();
        let ts = thumb_scrolled.clone();
        let pidx = preview_index.clone();
        let up2 = update_preview.clone();
        let er = enable_row.clone();
        page.connect_map(move |_| {
            let enabled = jc.borrow().compress_enabled;
            er.set_active(enabled);
            // Rebuild thumbnail strip from current file list
            while let Some(child) = tb.first_child() {
                tb.remove(&child);
            }
            let files = lf.borrow();
            let max_thumbs = files.len().min(10);
            for i in 0..max_thumbs {
                let pic = gtk::Picture::builder()
                    .content_fit(gtk::ContentFit::Cover)
                    .width_request(50)
                    .height_request(50)
                    .build();
                pic.set_filename(Some(&files[i]));
                let frame = gtk::Frame::builder()
                    .child(&pic)
                    .build();
                if i == *pidx.borrow() {
                    frame.add_css_class("accent");
                }
                let pidx_c = pidx.clone();
                let up_c = up2.clone();
                let tb_c = tb.clone();
                let current_idx = i;
                let file_name = files[i].file_name().and_then(|n| n.to_str()).unwrap_or("image");
                let btn = gtk::Button::builder()
                    .child(&frame)
                    .has_frame(false)
                    .tooltip_text(file_name)
                    .build();
                let is_selected = i == *pidx.borrow();
                btn.update_property(&[
                    gtk::accessible::Property::Label(
                        &if is_selected {
                            format!("Preview thumbnail: {} (currently selected)", file_name)
                        } else {
                            format!("Preview thumbnail: {}", file_name)
                        }
                    ),
                ]);
                btn.connect_clicked(move |_| {
                    *pidx_c.borrow_mut() = current_idx;
                    up_c(true);
                    let mut c = tb_c.first_child();
                    let mut j = 0usize;
                    while let Some(w) = c {
                        if let Some(b) = w.downcast_ref::<gtk::Button>() {
                            if let Some(f) = b.child().and_then(|c| c.downcast::<gtk::Frame>().ok()) {
                                if j == current_idx {
                                    f.add_css_class("accent");
                                } else {
                                    f.remove_css_class("accent");
                                }
                            }
                        }
                        c = w.next_sibling();
                        j += 1;
                    }
                });
                tb.append(&btn);
            }
            ts.set_visible(max_thumbs > 1);
            // Clamp preview index if files were removed
            {
                let mut idx = pidx.borrow_mut();
                if *idx >= files.len() && !files.is_empty() {
                    *idx = 0;
                }
            }
            drop(files);

            up(true);

            let cfg = jc.borrow();

            let mut has_jpeg = false;
            let mut has_png = false;
            let mut has_webp = false;
            let mut has_avif = false;

            match cfg.convert_format {
                None => {
                    has_jpeg = true;
                    has_png = true;
                    has_webp = true;
                    has_avif = true;
                }
                Some(ImageFormat::Jpeg) => has_jpeg = true,
                Some(ImageFormat::Png) => has_png = true,
                Some(ImageFormat::WebP) => has_webp = true,
                Some(ImageFormat::Avif) => has_avif = true,
                Some(ImageFormat::Gif) | Some(ImageFormat::Tiff) | Some(ImageFormat::Bmp) => {}
            }

            for (_, &choice_idx) in &cfg.format_mappings {
                match choice_idx {
                    0 => {}
                    1 => {
                        has_jpeg = true;
                        has_png = true;
                        has_webp = true;
                        has_avif = true;
                    }
                    2 => has_jpeg = true,
                    3 => has_png = true,
                    4 => has_webp = true,
                    5 => has_avif = true,
                    _ => {}
                }
            }

            jr.set_visible(has_jpeg);
            pr.set_visible(has_png);
            wr.set_visible(has_webp);
            wer.set_visible(has_webp);
            ar.set_visible(has_avif);
            asr.set_visible(has_avif);

            pg.set_visible(has_jpeg || has_png || has_webp || has_avif);
        });
    }

    page
}

fn quality_description(val: u32) -> String {
    match val {
        1 => "Low - ~50-60% smaller files. Visible quality loss. Good for email attachments and quick sharing.".into(),
        2 => "Low+ - ~45-55% smaller files. Slightly better than Low, still compact.".into(),
        3 => "Medium - ~30-40% smaller files. Good balance of quality and size. Recommended for most uses.".into(),
        4 => "Medium+ - ~25-35% smaller files. A step above Medium with better detail retention.".into(),
        5 => "High - ~15-25% smaller files. Minimal quality loss. Good for printing and high-quality output.".into(),
        6 => "High+ - ~10-20% smaller files. Very good quality with modest size reduction.".into(),
        7 => "Near Maximum - ~5-15% smaller files. Excellent quality, nearly indistinguishable from original.".into(),
        _ => "Maximum - ~5-10% smaller files. Best possible quality, largest files. Archival and professional use.".into(),
    }
}

enum PreviewResult {
    Success {
        original_bytes: Vec<u8>,
        compressed_bytes: Vec<u8>,
        original_size: u64,
        compressed_size: u64,
        format_label: String,
    },
    Error(#[allow(dead_code)] String),
}

fn generate_preview(path: &std::path::Path, comp: PreviewCompression) -> PreviewResult {
    let img = match image::open(path) {
        Ok(img) => img,
        Err(e) => return PreviewResult::Error(e.to_string()),
    };

    let preview_img = if img.width() > 800 || img.height() > 800 {
        img.resize(800, 800, image::imageops::FilterType::Triangle)
    } else {
        img
    };

    // Encode original as PNG for lossless reference
    let mut orig_buf = Vec::new();
    if preview_img
        .write_to(&mut std::io::Cursor::new(&mut orig_buf), image::ImageFormat::Png)
        .is_err()
    {
        return PreviewResult::Error("Failed to encode original".into());
    }

    // Use the preview buffer size for comparison (not on-disk size)
    // so both original and compressed refer to the same downscaled image
    let original_size = orig_buf.len() as u64;

    // Encode compressed in the requested format
    let mut comp_buf = Vec::new();
    let format_label;

    match comp {
        PreviewCompression::Jpeg(quality) => {
            format_label = format!("JPEG Q{}", quality);
            let cursor = std::io::Cursor::new(&mut comp_buf);
            let encoder = image::codecs::jpeg::JpegEncoder::new_with_quality(cursor, quality);
            let rgb = preview_img.to_rgb8();
            if image::ImageEncoder::write_image(
                encoder,
                rgb.as_raw(),
                rgb.width(),
                rgb.height(),
                image::ExtendedColorType::Rgb8,
            )
            .is_err()
            {
                return PreviewResult::Error("JPEG compression failed".into());
            }
        }
        PreviewCompression::Png(level) => {
            format_label = format!("PNG L{}", level);
            let cursor = std::io::Cursor::new(&mut comp_buf);
            let ct = match level {
                1 => image::codecs::png::CompressionType::Fast,
                2 | 3 => image::codecs::png::CompressionType::Default,
                _ => image::codecs::png::CompressionType::Best,
            };
            let encoder = image::codecs::png::PngEncoder::new_with_quality(
                cursor,
                ct,
                image::codecs::png::FilterType::Adaptive,
            );
            let rgba = preview_img.to_rgba8();
            if image::ImageEncoder::write_image(
                encoder,
                rgba.as_raw(),
                rgba.width(),
                rgba.height(),
                image::ExtendedColorType::Rgba8,
            )
            .is_err()
            {
                return PreviewResult::Error("PNG compression failed".into());
            }
        }
        PreviewCompression::WebP(quality) => {
            // image crate only has lossless WebP encoding, so approximate with
            // JPEG at equivalent quality for the visual preview (size estimate only)
            format_label = format!("~WebP Q{}", quality);
            let cursor = std::io::Cursor::new(&mut comp_buf);
            let encoder = image::codecs::jpeg::JpegEncoder::new_with_quality(cursor, quality);
            let rgb = preview_img.to_rgb8();
            if image::ImageEncoder::write_image(
                encoder,
                rgb.as_raw(),
                rgb.width(),
                rgb.height(),
                image::ExtendedColorType::Rgb8,
            )
            .is_err()
            {
                return PreviewResult::Error("WebP preview failed".into());
            }
        }
        PreviewCompression::Avif(quality) => {
            // AVIF encoding not available in image crate - approximate with JPEG (size estimate only)
            format_label = format!("~AVIF Q{}", quality);
            let cursor = std::io::Cursor::new(&mut comp_buf);
            let encoder = image::codecs::jpeg::JpegEncoder::new_with_quality(cursor, quality);
            let rgb = preview_img.to_rgb8();
            if image::ImageEncoder::write_image(
                encoder,
                rgb.as_raw(),
                rgb.width(),
                rgb.height(),
                image::ExtendedColorType::Rgb8,
            )
            .is_err()
            {
                return PreviewResult::Error("AVIF preview failed".into());
            }
        }
    }

    let compressed_size = comp_buf.len() as u64;

    PreviewResult::Success {
        original_bytes: orig_buf,
        compressed_bytes: comp_buf,
        original_size,
        compressed_size,
        format_label,
    }
}

fn load_pixbuf_from_bytes(bytes: &[u8]) -> Result<gtk::gdk_pixbuf::Pixbuf, String> {
    let stream = gtk::gio::MemoryInputStream::from_bytes(&glib::Bytes::from(bytes));
    gtk::gdk_pixbuf::Pixbuf::from_stream(
        &stream,
        gtk::gio::Cancellable::NONE,
    )
    .map_err(|e| e.to_string())
}
