use adw::prelude::*;
use std::cell::Cell;
use std::collections::HashMap;
use std::rc::Rc;

use crate::app::AppState;

pub fn build_rename_page(state: &AppState) -> adw::NavigationPage {
    let cfg = state.job_config.borrow();

    // === OUTER LAYOUT ===
    let outer = gtk::Box::builder()
        .orientation(gtk::Orientation::Vertical)
        .spacing(0)
        .vexpand(true)
        .build();

    // --- Enable toggle (full width) ---
    let enable_group = adw::PreferencesGroup::builder()
        .margin_start(12)
        .margin_end(12)
        .margin_top(12)
        .build();
    let enable_row = adw::SwitchRow::builder()
        .title("Enable Rename")
        .subtitle("Rename output files with prefix, suffix, or template")
        .active(cfg.rename_enabled)
        .tooltip_text("Toggle file renaming on or off")
        .build();
    enable_group.add(&enable_row);
    outer.append(&enable_group);

    // === LEFT SIDE: Preview ===

    let show_all: Rc<Cell<bool>> = Rc::new(Cell::new(false));

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

    let showing_label = gtk::Label::builder()
        .css_classes(["dim-label", "caption"])
        .halign(gtk::Align::Start)
        .hexpand(true)
        .build();

    let show_all_button = gtk::Button::builder()
        .label("Show all")
        .build();
    show_all_button.add_css_class("pill");
    show_all_button.add_css_class("caption");

    preview_header.append(&showing_label);
    preview_header.append(&show_all_button);

    // Preview rows container
    let preview_rows = gtk::Box::builder()
        .orientation(gtk::Orientation::Vertical)
        .spacing(2)
        .build();

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

    // Conflict banner
    let conflict_banner = gtk::Box::builder()
        .orientation(gtk::Orientation::Horizontal)
        .spacing(8)
        .margin_top(8)
        .visible(false)
        .build();
    conflict_banner.add_css_class("card");
    conflict_banner.set_accessible_role(gtk::AccessibleRole::Alert);

    let conflict_icon = gtk::Image::builder()
        .icon_name("dialog-warning-symbolic")
        .margin_start(8)
        .margin_top(4)
        .margin_bottom(4)
        .build();
    conflict_icon.add_css_class("warning");

    let conflict_label = gtk::Label::builder()
        .css_classes(["caption"])
        .halign(gtk::Align::Start)
        .hexpand(true)
        .wrap(true)
        .margin_top(4)
        .margin_bottom(4)
        .margin_end(8)
        .build();

    conflict_banner.append(&conflict_icon);
    conflict_banner.append(&conflict_label);

    // Stats label
    let stats_label = gtk::Label::builder()
        .css_classes(["dim-label", "caption"])
        .halign(gtk::Align::Start)
        .wrap(true)
        .margin_top(4)
        .build();

    let preview_box = gtk::Box::builder()
        .orientation(gtk::Orientation::Vertical)
        .spacing(4)
        .hexpand(true)
        .vexpand(true)
        .build();
    preview_box.append(&preview_header);
    preview_box.append(&preview_scroll);
    preview_box.append(&conflict_banner);
    preview_box.append(&stats_label);

    // === RIGHT SIDE: Controls (scrollable) ===

    let controls = gtk::Box::builder()
        .orientation(gtk::Orientation::Vertical)
        .spacing(12)
        .margin_start(12)
        .build();

    // Reset button at end of controls (added later)
    let reset_button = gtk::Button::builder()
        .label("Reset to defaults")
        .halign(gtk::Align::Start)
        .margin_top(4)
        .tooltip_text("Reset all rename options to their defaults")
        .build();
    reset_button.add_css_class("pill");

    // --- Simple Rename group ---
    let simple_group = adw::PreferencesGroup::builder()
        .title("Simple Rename")
        .description("Add prefix, suffix, and text transformations")
        .build();

    let prefix_row = adw::EntryRow::builder()
        .title("Prefix")
        .text(&cfg.rename_prefix)
        .tooltip_text("Text added before the original filename")
        .build();

    let suffix_row = adw::EntryRow::builder()
        .title("Suffix")
        .text(&cfg.rename_suffix)
        .tooltip_text("Text added after the original filename")
        .build();

    let replace_spaces_row = adw::ComboRow::builder()
        .title("Replace Spaces")
        .subtitle("How to handle spaces in filenames")
        .use_subtitle(true)
        .build();
    replace_spaces_row.set_model(Some(&gtk::StringList::new(&[
        "No change",
        "Underscores (_)",
        "Hyphens (-)",
        "Dots (.)",
        "CamelCase",
        "Remove spaces",
    ])));
    replace_spaces_row.set_list_factory(Some(&super::full_text_list_factory()));
    replace_spaces_row.set_selected(cfg.rename_replace_spaces);

    let special_chars_row = adw::ComboRow::builder()
        .title("Special Characters")
        .subtitle("Filter non-standard characters from filenames")
        .use_subtitle(true)
        .build();
    special_chars_row.set_model(Some(&gtk::StringList::new(&[
        "Keep all",
        "Filesystem-safe (remove / \\ : * ? \" < > |)",
        "Web-safe (a-z, 0-9, -, _, .)",
        "Hyphens + underscores (a-z, 0-9, -, _)",
        "Hyphens only (a-z, 0-9, -)",
        "Alphanumeric only (a-z, 0-9)",
    ])));
    special_chars_row.set_list_factory(Some(&super::full_text_list_factory()));
    special_chars_row.set_selected(cfg.rename_special_chars);

    let case_row = adw::ComboRow::builder()
        .title("Case Conversion")
        .subtitle("Convert filename case")
        .use_subtitle(true)
        .build();
    case_row.set_model(Some(&gtk::StringList::new(&["No change", "lowercase", "UPPERCASE", "Title Case"])));
    case_row.set_list_factory(Some(&super::full_text_list_factory()));
    case_row.set_selected(cfg.rename_case);

    // Counter toggle + expandable sub-options
    let counter_row = adw::ExpanderRow::builder()
        .title("Add Sequential Counter")
        .subtitle("Append a numbered sequence to filenames")
        .show_enable_switch(true)
        .enable_expansion(cfg.rename_counter_enabled)
        .expanded(cfg.rename_counter_enabled)
        .build();

    let counter_position_row = adw::ComboRow::builder()
        .title("Counter Position")
        .subtitle("Where the counter number appears")
        .use_subtitle(true)
        .build();
    counter_position_row.set_model(Some(&gtk::StringList::new(&[
        "Before prefix",
        "Before name",
        "After name",
        "After suffix",
        "Replace name",
    ])));
    counter_position_row.set_list_factory(Some(&super::full_text_list_factory()));
    counter_position_row.set_selected(cfg.rename_counter_position);

    let counter_start_row = adw::SpinRow::builder()
        .title("Counter Start")
        .subtitle("First number in sequence")
        .adjustment(&gtk::Adjustment::new(cfg.rename_counter_start as f64, 0.0, 99999.0, 1.0, 10.0, 0.0))
        .build();

    let counter_padding_row = adw::SpinRow::builder()
        .title("Counter Padding")
        .subtitle("Minimum digits (e.g., 3 = 001, 002, 003)")
        .adjustment(&gtk::Adjustment::new(cfg.rename_counter_padding as f64, 1.0, 10.0, 1.0, 1.0, 0.0))
        .build();

    counter_row.add_row(&counter_position_row);
    counter_row.add_row(&counter_start_row);
    counter_row.add_row(&counter_padding_row);

    simple_group.add(&prefix_row);
    simple_group.add(&suffix_row);
    simple_group.add(&replace_spaces_row);
    simple_group.add(&special_chars_row);
    simple_group.add(&case_row);
    simple_group.add(&counter_row);
    controls.append(&simple_group);

    // --- Advanced group ---
    let advanced_group = adw::PreferencesGroup::builder()
        .title("Advanced")
        .build();

    let advanced_expander = adw::ExpanderRow::builder()
        .title("Template Engine and Find/Replace")
        .subtitle("Advanced rename options with variables and regex")
        .show_enable_switch(false)
        .expanded(state.is_section_expanded("rename-advanced"))
        .build();

    {
        let st = state.clone();
        advanced_expander.connect_expanded_notify(move |row| {
            st.set_section_expanded("rename-advanced", row.is_expanded());
        });
    }

    let template_row = adw::EntryRow::builder()
        .title("Template")
        .text(&cfg.rename_template)
        .tooltip_text("Use variables like {name}, {date}, {counter:3} to build filenames")
        .build();

    // Template preset chips
    let preset_templates = [
        ("Date + Name", "{date}_{name}"),
        ("EXIF Date + Name", "{exif_date}_{name}"),
        ("Sequential", "{name}_{counter:4}"),
        ("Dimensions", "{name}_{width}x{height}"),
        ("Camera + Date", "{camera}_{exif_date}_{counter:3}"),
        ("Web-safe", "{name}_web"),
        ("Date + Counter", "{date}_{counter:4}"),
        ("Name + Size", "{name}_{width}x{height}_{counter:3}"),
    ];

    let presets_box = gtk::FlowBox::builder()
        .selection_mode(gtk::SelectionMode::None)
        .max_children_per_line(6)
        .min_children_per_line(2)
        .row_spacing(4)
        .column_spacing(4)
        .margin_top(4)
        .margin_bottom(4)
        .margin_start(12)
        .margin_end(12)
        .homogeneous(false)
        .build();

    for (label, template) in &preset_templates {
        let btn = gtk::Button::builder()
            .label(*label)
            .tooltip_text(*template)
            .build();
        btn.add_css_class("flat");

        let tr = template_row.clone();
        let tmpl = template.to_string();
        btn.connect_clicked(move |_| {
            tr.set_text(&tmpl);
            tr.set_position(tmpl.chars().count() as i32);
        });
        presets_box.append(&btn);
    }

    let presets_list_row = gtk::ListBoxRow::builder()
        .activatable(false)
        .selectable(false)
        .child(&presets_box)
        .build();

    // Variable chips (collapsible)
    let variables_expander = adw::ExpanderRow::builder()
        .title("Available Variables")
        .subtitle("Click to insert at cursor position in template")
        .show_enable_switch(false)
        .expanded(state.is_section_expanded("rename-variables"))
        .build();

    {
        let st = state.clone();
        variables_expander.connect_expanded_notify(move |row| {
            st.set_section_expanded("rename-variables", row.is_expanded());
        });
    }

    let variables = [
        ("{name}", "Original filename (no extension)"),
        ("{ext}", "Output file extension"),
        ("{original_ext}", "Original file extension"),
        ("{counter}", "Sequential counter number"),
        ("{counter:3}", "Counter, zero-padded to 3 digits"),
        ("{counter:4}", "Counter, zero-padded to 4 digits"),
        ("{date}", "Today's date (YYYY-MM-DD)"),
        ("{exif_date}", "Date photo was taken (from EXIF)"),
        ("{camera}", "Camera model (from EXIF)"),
        ("{width}", "Output image width in pixels"),
        ("{height}", "Output image height in pixels"),
    ];

    let vars_flow = gtk::FlowBox::builder()
        .selection_mode(gtk::SelectionMode::None)
        .max_children_per_line(3)
        .min_children_per_line(1)
        .row_spacing(4)
        .column_spacing(4)
        .margin_top(4)
        .margin_bottom(4)
        .margin_start(12)
        .margin_end(12)
        .homogeneous(false)
        .build();

    for (var_name, description) in &variables {
        let chip_box = gtk::Box::builder()
            .orientation(gtk::Orientation::Vertical)
            .spacing(1)
            .build();

        let name_label = gtk::Label::builder()
            .label(*var_name)
            .css_classes(["monospace", "caption"])
            .halign(gtk::Align::Start)
            .build();

        let desc_label = gtk::Label::builder()
            .label(*description)
            .css_classes(["dim-label"])
            .halign(gtk::Align::Start)
            .build();
        desc_label.add_css_class("caption");

        chip_box.append(&name_label);
        chip_box.append(&desc_label);

        let btn = gtk::Button::builder()
            .child(&chip_box)
            .has_frame(false)
            .build();
        btn.update_property(&[
            gtk::accessible::Property::Label(
                &format!("Insert {} - {}", var_name, description)
            ),
        ]);

        let tr = template_row.clone();
        let var_text = var_name.to_string();
        btn.connect_clicked(move |_| {
            let current = tr.text().to_string();
            if current.is_empty() {
                tr.set_text(&var_text);
                tr.set_position(var_text.chars().count() as i32);
            } else {
                let char_count = current.chars().count();
                let pos = (tr.position().max(0) as usize).min(char_count);
                let byte_pos = current.char_indices()
                    .nth(pos)
                    .map(|(i, _)| i)
                    .unwrap_or(current.len());
                let mut new_text = current.clone();
                new_text.insert_str(byte_pos, &var_text);
                tr.set_text(&new_text);
                tr.set_position((pos.saturating_add(var_text.chars().count())) as i32);
            }
        });

        vars_flow.append(&btn);
    }

    let vars_list_row = gtk::ListBoxRow::builder()
        .activatable(false)
        .selectable(false)
        .child(&vars_flow)
        .build();
    variables_expander.add_row(&vars_list_row);

    // Find/replace
    let find_row = adw::EntryRow::builder()
        .title("Find (regex)")
        .text(&cfg.rename_find)
        .tooltip_text("Regular expression pattern to match in filenames")
        .build();

    let replace_row = adw::EntryRow::builder()
        .title("Replace with")
        .text(&cfg.rename_replace)
        .tooltip_text("Replacement text for matched pattern")
        .build();

    advanced_expander.add_row(&template_row);
    advanced_expander.add_row(&presets_list_row);
    advanced_expander.add_row(&find_row);
    advanced_expander.add_row(&replace_row);

    advanced_group.add(&advanced_expander);
    advanced_group.add(&variables_expander);
    controls.append(&advanced_group);
    controls.append(&reset_button);

    // Scrollable controls
    let controls_scrolled = gtk::ScrolledWindow::builder()
        .hscrollbar_policy(gtk::PolicyType::Never)
        .vexpand(true)
        .width_request(420)
        .child(&controls)
        .build();

    // === Main layout: 60/40 side-by-side ===
    let main_box = gtk::Box::builder()
        .orientation(gtk::Orientation::Horizontal)
        .spacing(12)
        .margin_top(12)
        .margin_bottom(12)
        .margin_start(12)
        .margin_end(12)
        .vexpand(true)
        .build();

    preview_box.set_width_request(400);
    main_box.append(&preview_box);
    main_box.append(&controls_scrolled);
    outer.append(&main_box);

    drop(cfg);

    // === Preview update closure ===
    let update_preview = {
        let files = state.loaded_files.clone();
        let jc = state.job_config.clone();
        let rows_box = preview_rows.clone();
        let showing = showing_label.clone();
        let show_all_state = show_all.clone();
        let conflict_banner_c = conflict_banner.clone();
        let conflict_label_c = conflict_label.clone();
        let stats = stats_label.clone();
        let show_btn = show_all_button.clone();

        Rc::new(move || {
            let loaded = files.borrow();
            let cfg = jc.borrow();
            let total = loaded.len();

            // Clear existing rows
            while let Some(child) = rows_box.first_child() {
                rows_box.remove(&child);
            }

            if total == 0 {
                showing.set_label("No images loaded");
                stats.set_label("");
                conflict_banner_c.set_visible(false);
                show_btn.set_visible(false);
                return;
            }

            let display_count = if show_all_state.get() { total } else { total.min(5) };
            show_btn.set_visible(total > 5);
            if show_all_state.get() {
                show_btn.set_label("Show less");
                showing.set_label(&format!("All {} files", total));
            } else {
                show_btn.set_label("Show all");
                showing.set_label(&format!("Showing {} of {} files", display_count, total));
            }

            // Compute all renames for conflict detection
            let mut all_results: Vec<String> = Vec::with_capacity(total);
            let mut ext_counts: HashMap<String, usize> = HashMap::new();
            let mut longest_name = 0usize;

            // Pre-compile regex once for the entire batch (avoids recompiling per file)
            let rename_cfg = 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: None,
                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(),
            };
            let compiled_re = rename_cfg.compile_regex();

            for (i, path) in loaded.iter().enumerate() {
                let name = path.file_stem().and_then(|s| s.to_str()).unwrap_or("file");
                let ext = path.extension().and_then(|e| e.to_str()).unwrap_or("jpg");

                *ext_counts.entry(ext.to_string()).or_insert(0) += 1;

                let result = if !cfg.rename_template.is_empty() {
                    let counter = cfg.rename_counter_start + i as u32;
                    pixstrip_core::operations::rename::apply_template(
                        &cfg.rename_template, name, ext, counter, None,
                    )
                } else {
                    rename_cfg.apply_simple_compiled(name, ext, (i + 1) as u32, compiled_re.as_ref())
                };

                if result.len() > longest_name {
                    longest_name = result.len();
                }
                all_results.push(result);
            }

            // Detect conflicts
            let mut name_counts: HashMap<&str, usize> = HashMap::new();
            for result in &all_results {
                *name_counts.entry(result.as_str()).or_insert(0) += 1;
            }
            let conflicts: usize = name_counts.values().filter(|&&c| c > 1).map(|c| c).sum();

            if conflicts > 0 {
                conflict_banner_c.set_visible(true);
                let dupes: Vec<&str> = name_counts.iter()
                    .filter(|(_, c)| **c > 1)
                    .take(3)
                    .map(|(n, _)| *n)
                    .collect();
                let dupe_list = dupes.join(", ");
                let msg = if name_counts.values().filter(|c| **c > 1).count() > 3 {
                    format!("{} files have duplicate names (e.g. {}, ...)", conflicts, dupe_list)
                } else {
                    format!("{} files have duplicate names: {}", conflicts, dupe_list)
                };
                conflict_label_c.set_label(&msg);
            } else {
                conflict_banner_c.set_visible(false);
            }

            // Build preview rows (vertical: original on top, new name below)
            for (i, path) in loaded.iter().take(display_count).enumerate() {
                let name = path.file_stem().and_then(|s| s.to_str()).unwrap_or("file");
                let ext = path.extension().and_then(|e| e.to_str()).unwrap_or("jpg");

                let new_full = &all_results[i];

                let row = gtk::Box::builder()
                    .orientation(gtk::Orientation::Vertical)
                    .spacing(1)
                    .margin_top(3)
                    .margin_bottom(3)
                    .build();

                let orig_label = gtk::Label::builder()
                    .label(&format!("{}.{}", name, ext))
                    .css_classes(["monospace", "caption", "dim-label"])
                    .halign(gtk::Align::Start)
                    .ellipsize(gtk::pango::EllipsizeMode::Middle)
                    .max_width_chars(50)
                    .build();

                let new_line = gtk::Box::builder()
                    .orientation(gtk::Orientation::Horizontal)
                    .spacing(4)
                    .build();

                let arrow_label = gtk::Label::builder()
                    .label("->")
                    .css_classes(["dim-label", "caption"])
                    .build();

                let new_name_label = gtk::Label::builder()
                    .label(new_full.as_str())
                    .css_classes(["monospace", "caption"])
                    .halign(gtk::Align::Start)
                    .hexpand(true)
                    .ellipsize(gtk::pango::EllipsizeMode::Middle)
                    .max_width_chars(50)
                    .build();

                // Highlight conflicts with both color AND icon indicator
                let is_conflict = name_counts.get(new_full.as_str()).copied().unwrap_or(0) > 1;
                if is_conflict {
                    new_name_label.add_css_class("error");
                    let conflict_icon = gtk::Image::builder()
                        .icon_name("dialog-warning-symbolic")
                        .pixel_size(12)
                        .tooltip_text("Duplicate filename")
                        .build();
                    conflict_icon.add_css_class("warning");
                    new_line.append(&conflict_icon);
                }

                new_line.append(&arrow_label);
                new_line.append(&new_name_label);

                row.append(&orig_label);
                row.append(&new_line);
                rows_box.append(&row);
            }

            // Stats
            let mut ext_parts: Vec<String> = ext_counts
                .iter()
                .map(|(e, c)| format!("{} .{}", c, e))
                .collect();
            ext_parts.sort();

            stats.set_label(&format!(
                "{} files | {} conflicts | {} | Longest: {} chars",
                total,
                conflicts,
                ext_parts.join(", "),
                longest_name,
            ));
        })
    };

    // Debounced wrapper for text entry handlers (150ms)
    let debounce_source: Rc<Cell<Option<gtk::glib::SourceId>>> = Rc::new(Cell::new(None));
    let debounced_preview = {
        let up = update_preview.clone();
        let ds = debounce_source.clone();
        Rc::new(move || {
            if let Some(id) = ds.take() {
                id.remove();
            }
            let up2 = up.clone();
            let ds2 = ds.clone();
            let id = gtk::glib::timeout_add_local_once(
                std::time::Duration::from_millis(150),
                move || {
                    ds2.set(None);
                    up2();
                },
            );
            ds.set(Some(id));
        })
    };

    // Call once for initial state
    update_preview();

    // === Wire signals ===

    // Enable toggle
    {
        let jc = state.job_config.clone();
        enable_row.connect_active_notify(move |row| {
            jc.borrow_mut().rename_enabled = row.is_active();
        });
    }

    // Show all toggle
    {
        let sa = show_all.clone();
        let up = update_preview.clone();
        show_all_button.connect_clicked(move |_| {
            sa.set(!sa.get());
            up();
        });
    }

    // Reset button
    {
        let jc = state.job_config.clone();
        let up = update_preview.clone();
        let prefix_r = prefix_row.clone();
        let suffix_r = suffix_row.clone();
        let spaces_r = replace_spaces_row.clone();
        let special_r = special_chars_row.clone();
        let case_r = case_row.clone();
        let counter_r = counter_row.clone();
        let counter_pos_r = counter_position_row.clone();
        let counter_start_r = counter_start_row.clone();
        let counter_pad_r = counter_padding_row.clone();
        let template_r = template_row.clone();
        let find_r = find_row.clone();
        let replace_r = replace_row.clone();

        reset_button.connect_clicked(move |_| {
            {
                let mut cfg = jc.borrow_mut();
                cfg.rename_prefix = String::new();
                cfg.rename_suffix = String::new();
                cfg.rename_counter_enabled = false;
                cfg.rename_counter_start = 1;
                cfg.rename_counter_padding = 3;
                cfg.rename_counter_position = 3;
                cfg.rename_replace_spaces = 0;
                cfg.rename_special_chars = 0;
                cfg.rename_case = 0;
                cfg.rename_template = String::new();
                cfg.rename_find = String::new();
                cfg.rename_replace = String::new();
            }
            prefix_r.set_text("");
            suffix_r.set_text("");
            spaces_r.set_selected(0);
            special_r.set_selected(0);
            case_r.set_selected(0);
            counter_r.set_enable_expansion(false);
            counter_r.set_expanded(false);
            counter_pos_r.set_selected(3);
            counter_start_r.set_value(1.0);
            counter_pad_r.set_value(3.0);
            template_r.set_text("");
            find_r.set_text("");
            replace_r.set_text("");
            up();
        });
    }

    // Prefix
    {
        let jc = state.job_config.clone();
        let up = debounced_preview.clone();
        prefix_row.connect_changed(move |row| {
            jc.borrow_mut().rename_prefix = row.text().to_string();
            up();
        });
    }

    // Suffix
    {
        let jc = state.job_config.clone();
        let up = debounced_preview.clone();
        suffix_row.connect_changed(move |row| {
            jc.borrow_mut().rename_suffix = row.text().to_string();
            up();
        });
    }

    // Replace spaces
    {
        let jc = state.job_config.clone();
        let up = update_preview.clone();
        replace_spaces_row.connect_selected_notify(move |row| {
            jc.borrow_mut().rename_replace_spaces = row.selected();
            up();
        });
    }

    // Special chars
    {
        let jc = state.job_config.clone();
        let up = update_preview.clone();
        special_chars_row.connect_selected_notify(move |row| {
            jc.borrow_mut().rename_special_chars = row.selected();
            up();
        });
    }

    // Case conversion
    {
        let jc = state.job_config.clone();
        let up = update_preview.clone();
        case_row.connect_selected_notify(move |row| {
            jc.borrow_mut().rename_case = row.selected();
            up();
        });
    }

    // Counter enable
    {
        let jc = state.job_config.clone();
        let up = update_preview.clone();
        counter_row.connect_enable_expansion_notify(move |row| {
            jc.borrow_mut().rename_counter_enabled = row.enables_expansion();
            up();
        });
    }

    // Counter position
    {
        let jc = state.job_config.clone();
        let up = update_preview.clone();
        counter_position_row.connect_selected_notify(move |row| {
            jc.borrow_mut().rename_counter_position = row.selected();
            up();
        });
    }

    // Counter start
    {
        let jc = state.job_config.clone();
        let up = update_preview.clone();
        counter_start_row.connect_value_notify(move |row| {
            jc.borrow_mut().rename_counter_start = row.value() as u32;
            up();
        });
    }

    // Counter padding
    {
        let jc = state.job_config.clone();
        let up = update_preview.clone();
        counter_padding_row.connect_value_notify(move |row| {
            jc.borrow_mut().rename_counter_padding = row.value() as u32;
            up();
        });
    }

    // Template
    {
        let jc = state.job_config.clone();
        let up = debounced_preview.clone();
        template_row.connect_changed(move |row| {
            jc.borrow_mut().rename_template = row.text().to_string();
            up();
        });
    }

    // Find regex
    {
        let jc = state.job_config.clone();
        let up = debounced_preview.clone();
        find_row.connect_changed(move |row| {
            let text = row.text().to_string();
            if !text.is_empty() {
                if regex::Regex::new(&text).is_err() {
                    row.add_css_class("error");
                } else {
                    row.remove_css_class("error");
                }
            } else {
                row.remove_css_class("error");
            }
            jc.borrow_mut().rename_find = text;
            up();
        });
    }

    // Replace
    {
        let jc = state.job_config.clone();
        let up = debounced_preview.clone();
        replace_row.connect_changed(move |row| {
            jc.borrow_mut().rename_replace = row.text().to_string();
            up();
        });
    }

    let page = adw::NavigationPage::builder()
        .title("Rename")
        .tag("step-rename")
        .child(&outer)
        .build();

    // Sync enable toggle and refresh preview when navigating to this page
    {
        let up = update_preview.clone();
        let jc = state.job_config.clone();
        let er = enable_row.clone();
        page.connect_map(move |_| {
            let enabled = jc.borrow().rename_enabled;
            er.set_active(enabled);
            up();
        });
    }

    page
}
