// SPDX-License-Identifier: GPL-3.0-or-later
//! Main window for the Temperlux GUI.
//!
//! BDD Specs (GUI integration):
//!
//! G1: Slider fires hardware command
//!   Given a display row with DDC backend
//!   When the slider value changes to 75%
//!   Then a HardwareCommand::SetDdc is dispatched with correct raw value
//!   And the percentage label updates to "75%"
//!
//! G2: Rename dialog persists
//!   Given display 1 named "Monitor"
//!   When rename dialog is opened, text changed to "Left", and Save clicked
//!   Then settings.json contains custom_name "Left" for the display key
//!
//! G3: Preferences dialog re-clamps
//!   Given display 1 at 90% brightness
//!   When preferences sets max to 80%
//!   Then brightness is clamped to 80% and hardware command dispatched
//!
//! G4: Sync toggle persists
//!   Given sync is OFF
//!   When sync switch is toggled ON
//!   Then settings.json contains proportional_sync: true
//!
//! G5: Empty state shows placeholder
//!   Given no displays detected
//!   When the window is built
//!   Then "No displays detected" label is shown
//!
//! G6: Range label shown when caps are set
//!   Given display 1 with min=10%, max=90%
//!   When the display row is built
//!   Then "Range: 10% – 90%" label is visible

use gtk4::prelude::*;
use gtk4::{self as gtk, Orientation};
use libadwaita as adw;
use libadwaita::prelude::AdwApplicationWindowExt;
use std::cell::RefCell;
use std::rc::Rc;

use crate::crash;
use crate::display::backlight::RealBacklightProvider;
use crate::display::provider::BacklightProvider;
use crate::gui::debounce::{DisplayWorkerPool, backend_key_string};
use crate::i18n::{tr, tr_args};

type SliderMap = Rc<RefCell<Vec<(String, gtk::Adjustment, gtk::Label)>>>;
type SpinnerMap = Arc<Mutex<std::collections::HashMap<String, (gtk::SpinButton, gtk::SpinButton, f64, u64)>>>;
type SyncState = Arc<Mutex<Vec<(String, SyncEntry)>>>;

/// Compute the display text for a label.
/// Single source of truth for name formatting.
/// - key: backend key string (e.g. "ddc:1")
///   Number comes from the KEY (hardware display number), NEVER from list position.
fn compute_display_text(key: &str, _index: usize, hardware_name: &str) -> String {
    let settings = Settings::load().unwrap_or_default();
    let name = settings.get_display(key)
        .custom_name
        .unwrap_or_else(|| hardware_name.to_string());
    if settings.show_display_numbers {
        // Number is the hardware display number from the key, not list position
        let display_num = key.strip_prefix("ddc:")
            .or_else(|| key.strip_prefix("bl:"))
            .and_then(|s| s.parse::<u32>().ok())
            .unwrap_or(1);
        format!("{}. {}", display_num, name)
    } else {
        name
    }
}

/// Capture a widget to a PNG file in /tmp for UI review.
fn capture_widget_to_png(widget: &impl IsA<gtk4::Widget>, label: &str) -> Result<(), String> {
    use gtk4::gsk;
    use gtk4::graphene;

    let widget = widget.upcast_ref::<gtk4::Widget>();

    let width = widget.width();
    let height = widget.height();
    if width == 0 || height == 0 {
        return Err(format!("{} has zero dimensions ({}x{})", label, width, height));
    }

    let snapshot = gtk4::Snapshot::new();
    widget.size_allocate(&gtk4::Allocation::new(0, 0, width, height), -1);

    let node = snapshot
        .to_node()
        .ok_or(format!("Failed to create render node for {}", label))?;

    let native = widget
        .native()
        .ok_or(format!("{} has no native", label))?;
    let surface = native
        .surface()
        .ok_or(format!("{} has no surface", label))?;

    let renderer = gsk::CairoRenderer::new();
    renderer
        .realize(Some(&surface))
        .map_err(|e| format!("Failed to realize renderer: {:?}", e))?;

    let bounds = graphene::Rect::new(0.0, 0.0, width as f32, height as f32);
    let texture = renderer.render_texture(&node, Some(&bounds));

    renderer.unrealize();

    let timestamp = chrono::Local::now().format("%Y%m%d-%H%M%S");
    let path = format!("/tmp/temperlux-{}-{}.png", label, timestamp);

    texture
        .save_to_png(&path)
        .map_err(|e| format!("Failed to save PNG: {:?}", e))?;

    eprintln!("[screenshot] {} saved to {}", label, path);
    Ok(())
}

/// Render all Temperlux screens to PNG using mock data.
/// Designed to run under Xvfb from agent context.
fn run_dump_ui() {
    use crate::display::ddc_ffi::DdcDisplayInfo;
    use crate::gui::viewmodel::{MainViewModel, DisplayRow, BackendKey};
    use crate::display::DisplayType;
    use crate::settings::Settings;

    fn scrot_capture(label: &str) {
        let timestamp = chrono::Local::now().format("%Y%m%d-%H%M%S");
        let path = format!("/tmp/temperlux-{}-{}.png", label, timestamp);

        // Try gnome-screenshot first (better compositing than scrot under Xvfb)
        let gnomess = std::process::Command::new("gnome-screenshot")
            .args(["-w", "-f", &path])
            .output();
        if let Ok(o) = &gnomess {
            if o.status.success() && std::path::Path::new(&path).exists() {
                eprintln!("[dump-ui] {} captured (gnome-screenshot): {}", label, path);
                return;
            }
        }

        // Try ImageMagick import as second fallback
        let import_cmd = std::process::Command::new("import")
            .args(["-window", "root", &path])
            .output();
        if let Ok(o) = &import_cmd {
            if o.status.success() && std::path::Path::new(&path).exists() {
                eprintln!("[dump-ui] {} captured (import): {}", label, path);
                return;
            }
        }

        // Final fallback: scrot
        match std::process::Command::new("scrot").arg(&path).output() {
            Ok(output) if output.status.success() => {
                eprintln!("[dump-ui] {} captured (scrot): {}", label, path);
            }
            Ok(output) => {
                eprintln!("[dump-ui] {} scrot failed: {}", label,
                    String::from_utf8_lossy(&output.stderr).trim());
            }
            Err(e) => eprintln!("[dump-ui] {} scrot unavailable: {}", label, e),
        }
    }

    eprintln!("[dump-ui] starting UI dump with mock data");

    let app = adw::Application::builder()
        .application_id("dev.angussoftware.temperlux")
        .build();

    app.connect_activate(move |app| {
        // Load Angus brand colors (teal accent override)
        crate::gui::theme::load_brand_theme();
        // Apply saved theme mode and color theme preferences
        let saved_settings = Settings::load().unwrap_or_default();
        crate::gui::theme::apply_theme_mode(saved_settings.theme_mode);
        crate::gui::theme::apply_color_themes(saved_settings.light_theme, saved_settings.dark_theme);
        let app = app.clone();
        let settings = Settings::load().unwrap_or_default();

        // Mock display data — realistic monitor names for promo screenshots
        let mock_displays = vec![
            DdcDisplayInfo { display_num: 1, model_name: "LG UltraFine".into(), current_brightness: 65, max_brightness: 100 },
            DdcDisplayInfo { display_num: 2, model_name: "Dell U2723QE".into(), current_brightness: 80, max_brightness: 100 },
        ];

        let vm = MainViewModel::from_detected(&mock_displays, &RealBacklightProvider, &settings)
            .unwrap_or_else(|_| MainViewModel {
                rows: vec![
                    DisplayRow { index: 1, name: "LG UltraFine".into(), hardware_name: "LG UltraFine 27UQ850".into(), brightness_pct: 65.0, display_type: DisplayType::Ddc, raw_max: 100, min_pct: 0.0, max_pct: 100.0, backend_key: BackendKey::Ddc(1) },
                    DisplayRow { index: 2, name: "Dell U2723QE".into(), hardware_name: "Dell U2723QE".into(), brightness_pct: 80.0, display_type: DisplayType::Ddc, raw_max: 100, min_pct: 0.0, max_pct: 100.0, backend_key: BackendKey::Ddc(2) },
                ],
                sync_enabled: false,
            });

        let window = adw::ApplicationWindow::builder()
            .application(&app)
            .title("Temperlux")
            .default_width(480)
            .default_height(600)
            .build();

        let header = adw::HeaderBar::new();
        let content = gtk::Box::new(Orientation::Vertical, 0);

        // Header buttons — functional for dump-ui
        let schedule_btn = gtk::Button::from_icon_name("alarm-symbolic");
        schedule_btn.add_css_class("flat");
        let refresh_btn = gtk::Button::from_icon_name("view-refresh-symbolic");
        refresh_btn.add_css_class("flat");
        let prefs_btn = gtk::Button::from_icon_name("emblem-system-symbolic");
        prefs_btn.add_css_class("flat");
        let about_btn = gtk::Button::from_icon_name("help-about-symbolic");
        about_btn.add_css_class("flat");
        header.pack_start(&schedule_btn);
        header.pack_start(&about_btn);
        header.pack_end(&prefs_btn);
        header.pack_end(&refresh_btn);

        window.set_content(Some(&content));
        window.show();

        let display_info = std::sync::Arc::new(std::sync::Mutex::new(Vec::new()));
        let slider_map = Rc::new(RefCell::new(Vec::new()));

        // Step 1: Populate main window
        gtk4::glib::timeout_add_local_once(std::time::Duration::from_millis(300), move || {
            populate_window(
                &window, &content, &header, &prefs_btn,
                &RealHardwareExecutor, vm, None, &display_info, &slider_map,
            );

            // Step 2: Capture main window
            let window_clone = window.clone();
            gtk4::glib::timeout_add_local_once(std::time::Duration::from_millis(500), move || {
                scrot_capture("01-main");

                // Step 3: Open about dialog, capture
                let about = adw::AboutWindow::new();
                about.set_transient_for(Some(&window_clone));
                about.set_application_name("Temperlux");
                about.set_developer_name("Angus Software");
                about.set_version(env!("CARGO_PKG_VERSION"));
                about.set_comments("Brightness controller for DDC/CI and backlight displays.\nBased on Luminance by sidevesh.");
                about.set_website("https://angussoftware.com");
                about.set_developers(&["RhoMancer https://github.com/RhoMancer", "sidevesh https://github.com/sidevesh"]);
                about.set_license_type(gtk::License::Gpl30);
                about.set_copyright("© 2026 Angus Software");
                about.present();

                let window_clone2 = window_clone.clone();
                gtk4::glib::timeout_add_local_once(std::time::Duration::from_millis(500), move || {
                    scrot_capture("02-about");
                    // Close about by closing all windows except main
                    for w in gtk4::Window::list_toplevels() {
                        if let Some(win) = w.downcast_ref::<gtk4::Window>() {
                            if win != window_clone2.upcast_ref::<gtk4::Window>() {
                                win.close();
                            }
                        }
                    }

                    // Step 4: Open schedule list, capture
                    let info = display_info.lock().unwrap().clone();
                    let sched_parent = window_clone2.clone();
                    let _ddc_holder: std::sync::Arc<std::sync::Mutex<Option<crate::display::ddc_ffi::DdcService>>> =
                        std::sync::Arc::new(std::sync::Mutex::new(None));
                    let _sched_slider_map = slider_map.clone();
                    let apply_cb = move |_profile_id: &str| {};
                    crate::gui::schedule::show_schedule_list(
                        sched_parent.upcast_ref(), info, apply_cb,
                    );

                    gtk4::glib::timeout_add_local_once(std::time::Duration::from_millis(500), move || {
                        scrot_capture("03-schedule-list");

                        // Step 5: Click the first edit button in schedule list to open editor
                        // Step 5: Open schedule editor directly for first profile
                        let editor_parent = window_clone2.clone();
                        let profiles = crate::schedule::list_profiles().unwrap_or_default();
                        if let Some(first_profile) = profiles.into_iter().next() {
                            let info = display_info.lock().unwrap().clone();
                            crate::gui::schedule::show_schedule_editor(
                                editor_parent.upcast_ref(),
                                first_profile,
                                &info,
                                move || {},
                            );
                        }

                        // Step 6: Capture schedule editor (if opened) or just proceed
                        gtk4::glib::timeout_add_local_once(std::time::Duration::from_millis(500), move || {
                            scrot_capture("04-schedule-editor");

                            // Close schedule windows
                            let main_win = window_clone2.clone();
                            for w in gtk4::Window::list_toplevels() {
                                if let Some(win) = w.downcast_ref::<gtk4::Window>() {
                                    if win != main_win.upcast_ref::<gtk4::Window>() {
                                        win.close();
                                    }
                                }
                            }

                            // Step 7: Re-enable parent, open preferences, capture
                            let main_win = window_clone2.clone();
                            main_win.set_sensitive(true);
                            let prefs_btn_for_click = prefs_btn.clone();
                            gtk4::glib::timeout_add_local_once(std::time::Duration::from_millis(100), move || {
                                prefs_btn_for_click.emit_clicked();

                                gtk4::glib::timeout_add_local_once(std::time::Duration::from_millis(500), move || {
                                    scrot_capture("05-preferences");

                                    // Done
                                    eprintln!("[dump-ui] all screens captured");
                                    app.quit();
                                });
                            });
                        });
                    });
                });
            });
        });
    });

    let filtered_args: Vec<String> = std::env::args().filter(|a| a != "--dump-ui").collect();
    app.run_with_args(&filtered_args);
}

/// Update all name labels from sync_state (single source of truth).
/// Call after any reorder or settings change that affects display order/names.
fn refresh_name_labels(sync_state: &SyncState) {
    let state = sync_state.lock().unwrap();
    for (i, (key, entry)) in state.iter().enumerate() {
        if let Some(ref lbl) = entry.name_label {
            lbl.set_text(&compute_display_text(key, i, &entry.hardware_name));
        }
    }
}
use crate::gui::viewmodel::{BackendKey, HardwareCommand, MainViewModel};
use crate::settings::{Settings, SyncMode as SettingsSyncMode};
use std::sync::{Arc, Mutex};

/// Trait for executing hardware commands. Injectable for testing.
pub trait HardwareExecutor: std::fmt::Debug + Clone + Send + 'static {
    fn execute(&self, cmds: &[HardwareCommand]);
}

/// Real hardware executor that talks to ddcutil/backlight.
#[derive(Debug, Clone)]
pub struct RealHardwareExecutor;

impl HardwareExecutor for RealHardwareExecutor {
    fn execute(&self, cmds: &[HardwareCommand]) {
        for cmd in cmds {
            let cmd = cmd.clone();
            std::thread::spawn(move || {
                let bl = RealBacklightProvider;
                match cmd {
                    HardwareCommand::SetDdc { index, raw } => {
                        // Use ddcutil CLI for one-off hardware commands (prefs dialog re-clamp)
                        let display_num = index + 1; // ddcutil is 1-based
                        let clamped = raw.clamp(0, 65535);
                        let output = std::process::Command::new("ddcutil")
                            .args([
                                "setvcp",
                                "10",
                                &clamped.to_string(),
                                "--display",
                                &display_num.to_string(),
                                "--sleep-multiplier",
                                "0",
                            ])
                            .output();
                        match output {
                            Ok(o) if !o.status.success() => {
                                log::error!("ddcutil setvcp failed for display {}: {}", display_num, String::from_utf8_lossy(&o.stderr));
                            }
                            Err(e) => {
                                log::error!("Failed to run ddcutil setvcp for display {}: {}", display_num, e);
                            }
                            _ => {}
                        }
                    }
                    HardwareCommand::SetBacklight { name, raw } => {
                        if let Err(e) = bl.set_brightness(&name, raw) {
                            log::error!("Backlight set_brightness failed for {}: {}", name, e);
                        }
                    }
                }
            });
        }
    }
}

/// Capture hardware commands for testing.
#[cfg(test)]
#[derive(Debug, Clone, Default)]
pub struct MockHardwareExecutor {
    pub commands: std::sync::Arc<std::sync::Mutex<Vec<HardwareCommand>>>,
}

#[cfg(test)]
impl HardwareExecutor for MockHardwareExecutor {
    fn execute(&self, cmds: &[HardwareCommand]) {
        let mut lock = self.commands.lock().unwrap();
        lock.extend(cmds.iter().cloned());
    }
}

/// Global debug flag — set by `--debug` CLI arg.
static DEBUG: std::sync::atomic::AtomicBool = std::sync::atomic::AtomicBool::new(false);

/// Check if debug mode is enabled.
pub fn is_debug() -> bool {
    DEBUG.load(std::sync::atomic::Ordering::Relaxed)
}

/// Debug println — only prints when --debug is passed.
macro_rules! debug_log {
    ($($arg:tt)*) => {
        if is_debug() {
            eprintln!($($arg)*);
        }
    };
}

pub fn run() {
    install_crash_handler();

    // --dump-ui mode: render screens with mock data and save to PNG, then exit.
    // Designed to run under Xvfb from agent context (no real display needed).
    let dump_ui = std::env::args().any(|a| a == "--dump-ui");
    if dump_ui {
        run_dump_ui();
        return;
    }

    // Debug mode via env var: TEMPERLUX_DEBUG=1 temperlux-gui
    if std::env::var("TEMPERLUX_DEBUG").is_ok() {
        DEBUG.store(true, std::sync::atomic::Ordering::Relaxed);
        eprintln!("temperlux-gui: debug mode enabled");

        // Also enable file logging for thread-safe diagnostics
        let log_file = std::fs::File::create("/tmp/temperlux-gui.log").expect("create log file");
        env_logger::Builder::from_env(env_logger::Env::default().default_filter_or("info"))
            .target(env_logger::Target::Pipe(Box::new(log_file)))
            .format_timestamp_millis()
            .init();
        eprintln!("temperlux-gui: logging to /tmp/temperlux-gui.log");
    }

    let app = adw::Application::builder()
        .application_id("dev.angussoftware.temperlux")
        .build();

    app.connect_activate(|app| {
        // Load Angus brand colors (teal accent override)
        crate::gui::theme::load_brand_theme();
        // Apply saved theme mode and color theme preferences
        let saved_settings = Settings::load().unwrap_or_default();
        crate::gui::theme::apply_theme_mode(saved_settings.theme_mode);
        crate::gui::theme::apply_color_themes(saved_settings.light_theme, saved_settings.dark_theme);
        build_ui(app, &RealHardwareExecutor);
    });

    debug_log!("temperlux-gui: starting");

    // Filter out our custom args before passing to GTK (avoids "Unknown option" error)
    let filtered_args: Vec<String> = std::env::args().filter(|a| {
        !a.starts_with("--perf-log") && a != "--perf-log-readback"
    }).collect();
    let filtered: Vec<&str> = filtered_args.iter().map(|s| s.as_str()).collect();
    app.run_with_args(&filtered);
}

fn install_crash_handler() {
    std::panic::set_hook(Box::new(|panic_info| {
        let info_str = panic_info.to_string();
        if let Ok(crash_file) = crash::write_crash_dump(&info_str) {
            log::error!("PANIC: {}. Crash dump: {}", info_str, crash_file.display());
            show_crash_dialog_gtk(&info_str, &crash_file);
        } else {
            log::error!("PANIC: {}. Could not write crash dump.", info_str);
        }
    }));
}

fn show_crash_dialog_gtk(panic_msg: &str, crash_file: &std::path::Path) {
    if gtk::init().is_err() {
        return;
    }

    let dialog = gtk::MessageDialog::builder()
        .title(tr("gui.crash_title"))
        .message_type(gtk::MessageType::Error)
        .text(tr("gui.crash_message"))
        .build();

    let detail_label = gtk::Label::new(Some(&tr_args("gui.crash_error_label", &[
        ("msg", &panic_msg.chars().take(200).collect::<String>()),
        ("path", &crash_file.display().to_string()),
    ])));
    detail_label.set_wrap(true);
    detail_label.set_selectable(true);
    detail_label.set_xalign(0.0);

    let area: gtk::Box = dialog.message_area().downcast().unwrap();
    area.append(&detail_label);

    let open_btn = dialog.add_button(&tr("gui.open_log"), gtk::ResponseType::Other(1));
    open_btn.add_css_class("suggested-action");
    let report_btn = dialog.add_button(&tr("gui.report_issue"), gtk::ResponseType::Other(2));
    report_btn.add_css_class("suggested-action");
    dialog.add_button(&tr("actions.close"), gtk::ResponseType::Close);

    let crash_path = crash_file.to_path_buf();
    dialog.connect_response(move |dlg, resp| {
        match resp {
            gtk::ResponseType::Other(1) => {
                if let Some(parent) = crash_path.parent() {
                    let _ = open_file_manager(parent);
                }
            }
            gtk::ResponseType::Other(2) => {
                let log = crash::log_path();
                let url = crash::generate_issue_url(&crash_path, &log);
                let _ = open_url(&url);
            }
            _ => {}
        }
        dlg.close();
    });

    dialog.show();
    let context = gtk::glib::MainContext::default();
    context.iteration(true);
}

fn open_file_manager(path: &std::path::Path) -> Result<(), std::io::Error> {
    std::process::Command::new("xdg-open").arg(path).spawn()?;
    Ok(())
}

fn open_url(url: &str) -> Result<(), std::io::Error> {
    std::process::Command::new("xdg-open").arg(url).spawn()?;
    Ok(())
}

/// Progress messages from the detection thread.
enum DetectProgress {
    /// Found a display during this pass.
    Found { name: String, current: u16 },
    /// Detection complete with the full viewmodel and DDC service.
    Done {
        result: Result<MainViewModel, String>,
        ddc_service: Option<crate::display::ddc_ffi::DdcService>,
    },
}

/// Build the main UI. Shows window immediately with spinner, detects in background.
pub fn build_ui<E: HardwareExecutor>(app: &adw::Application, executor: &E) {
    let settings = Settings::load().unwrap_or_default();

    let window = adw::ApplicationWindow::builder()
        .application(app)
        .title(tr("gui.title"))
        .default_width(480)
        .default_height(600)
        .build();

    let header = adw::HeaderBar::new();

    // Shared holder for DdcService — refresh button needs it to shutdown before rebuild
    let ddc_service_holder: Arc<Mutex<Option<crate::display::ddc_ffi::DdcService>>> =
        Arc::new(Mutex::new(None));

    // Shared map of display_key → (Adjustment, Label) for updating sliders from schedule apply
    let slider_map: SliderMap =
        Rc::new(RefCell::new(Vec::new()));

    // Clean up DdcService on window close (release I2C locks)
    let close_ddc = ddc_service_holder.clone();
    window.connect_close_request(move |_| {
        if let Some(mut svc) = close_ddc.lock().unwrap().take() {
            svc.shutdown();
        }
        gtk::glib::Propagation::Proceed
    });

    // Refresh button
    let refresh_btn = gtk::Button::from_icon_name("view-refresh-symbolic");
    refresh_btn.set_tooltip_text(Some(&tr("gui.refresh_tooltip")));
    refresh_btn.add_css_class("flat");
    let refresh_app = app.clone();
    let refresh_exec = executor.clone();
    let refresh_ddc = ddc_service_holder.clone();
    refresh_btn.connect_clicked(move |_| {
        // Shut down DdcService FIRST — it holds libddcutil I2C locks.
        // shutdown() now joins the thread, so all handles are released before we rebuild.
        if let Some(mut svc) = refresh_ddc.lock().unwrap().take() {
            svc.shutdown();
        }
        // Give the I2C bus a moment to settle after handle release
        std::thread::sleep(std::time::Duration::from_millis(300));
        for win in refresh_app.windows() {
            win.close();
        }
        build_ui(&refresh_app, &refresh_exec);
    });
    header.pack_start(&refresh_btn);

    // Shared display info for schedule editor — populated by populate_window
    let display_info: std::sync::Arc<std::sync::Mutex<Vec<(String, String)>>> =
        std::sync::Arc::new(std::sync::Mutex::new(Vec::new()));

    // Schedule button
    let schedule_btn = gtk::Button::from_icon_name("alarm-symbolic");
    schedule_btn.set_tooltip_text(Some(&tr("schedules.label")));
    schedule_btn.add_css_class("flat");
    let schedule_parent = window.clone();
    let schedule_info = display_info.clone();
    let apply_ddc_holder = ddc_service_holder.clone();
    let apply_slider_map = slider_map.clone();
    schedule_btn.connect_clicked(move |_| {
        let info = schedule_info.lock().unwrap().clone();
        let apply_ddc = apply_ddc_holder.clone();
        let apply_sliders = apply_slider_map.clone();
        let apply_cb = move |profile_id: &str| {
            if let Ok(Some(profile)) = crate::schedule::get_profile(profile_id) {
                // Apply DDC commands
                {
                    let ddc_guard = apply_ddc.lock().unwrap();
                    if let Some(svc) = ddc_guard.as_ref() {
                        for entry in &profile.entries {
                            if let Some(num_str) = entry.display_key.strip_prefix("ddc:") {
                                if let Ok(disp_num) = num_str.parse::<i32>() {
                                    let raw = entry.brightness.round().clamp(0.0, 100.0) as u16;
                                    svc.set_brightness(disp_num, raw, 0);
                                }
                            }
                        }
                    } else {
                        eprintln!("[schedule-apply] DDC service not available");
                    }
                }
                // Update sliders on main window
                for entry in &profile.entries {
                    if let Some((_, adj, label)) = apply_sliders.borrow().iter().find(|(k, _, _)| *k == entry.display_key) {
                        adj.set_value(entry.brightness);
                        label.set_text(&format!("{:.0}%", entry.brightness));
                    }
                }
            }
        };
        crate::gui::schedule::show_schedule_list(schedule_parent.upcast_ref(), info, apply_cb);
    });
    header.pack_start(&schedule_btn);

    // Background schedule polling — check every 60s for due profiles
    let poll_settings = settings.clone();
    let poll_ddc = ddc_service_holder.clone();
    // Track when each profile was last applied to prevent re-firing within the same trigger window
    let last_applied: Rc<RefCell<std::collections::HashMap<String, chrono::DateTime<chrono::Local>>>> =
        Rc::new(RefCell::new(std::collections::HashMap::new()));
    gtk::glib::timeout_add_local(std::time::Duration::from_secs(60), move || {
        eprintln!("[schedule-poll] checking...");
        let ctx = crate::schedule::trigger::TriggerContext::new(
            poll_settings.latitude,
            poll_settings.longitude,
        );
        let now = chrono::Local::now();
        match crate::schedule::check_due_profiles(&ctx) {
            Ok(profiles) if !profiles.is_empty() => {
                let ddc_guard = poll_ddc.lock().unwrap();
                let ddc_svc = match ddc_guard.as_ref() {
                    Some(svc) => svc,
                    None => {
                        eprintln!("[schedule-poll] DDC service not available");
                        return gtk::glib::ControlFlow::Continue;
                    }
                };

                for profile in &profiles {
                    // Skip if already applied within the last 2 minutes (covers the 90s match window)
                    if let Some(last) = last_applied.borrow().get(&profile.id) {
                        let elapsed = now.signed_duration_since(*last);
                        if elapsed.num_seconds() < 120 {
                            eprintln!("[schedule-poll] skipping '{}', applied {}s ago",
                                profile.name, elapsed.num_seconds());
                            continue;
                        }
                    }

                    eprintln!("[schedule-poll] applying '{}'", profile.name);
                    for entry in &profile.entries {
                        if let Some(num_str) = entry.display_key.strip_prefix("ddc:") {
                            if let Ok(disp_num) = num_str.parse::<i32>() {
                                let raw = entry.brightness.round().clamp(0.0, 100.0) as u16;
                                ddc_svc.set_brightness(disp_num, raw, 0);
                                eprintln!("[schedule-poll]   {} → {:.0}%", entry.display_key, entry.brightness);
                            }
                        }
                    }
                    last_applied.borrow_mut().insert(profile.id.clone(), now);
                }
            }
            Ok(profiles) => {
                eprintln!("[schedule-poll] {} profiles, 0 due", profiles.len());
            }
            Err(e) => {
                eprintln!("[schedule-poll] error: {}", e);
            }
        }
        gtk::glib::ControlFlow::Continue
    });

    // Preferences button — hidden but packed so btn.root() works in its handler.
    // Triggered via primary menu "Preferences" action.
    let prefs_btn = gtk::Button::from_icon_name("preferences-system-symbolic");
    prefs_btn.add_css_class("flat");
    prefs_btn.set_visible(false);
    header.pack_end(&prefs_btn);

    // Primary menu (⋮) — Preferences + About (with icons)
    let menu = gtk4::gio::Menu::new();

    let prefs_item = gtk4::gio::MenuItem::new(Some(&tr("gui.preferences")), Some("app.preferences"));
    prefs_item.set_icon(&gtk4::gio::Icon::for_string("preferences-system-symbolic").unwrap());
    menu.append_item(&prefs_item);

    let about_item = gtk4::gio::MenuItem::new(Some(&tr("gui.about")), Some("app.about"));
    about_item.set_icon(&gtk4::gio::Icon::for_string("help-about-symbolic").unwrap());
    menu.append_item(&about_item);

    let menu_btn = gtk::MenuButton::new();
    menu_btn.set_icon_name("open-menu-symbolic");
    menu_btn.set_menu_model(Some(&menu));
    menu_btn.set_tooltip_text(Some(&tr("gui.menu_tooltip")));
    header.pack_end(&menu_btn);

    // Preferences action
    let _prefs_parent = window.clone();
    let prefs_btn_clone = prefs_btn.clone();
    let prefs_action = gtk4::gio::SimpleAction::new("preferences", None);
    prefs_action.connect_activate(move |_, _| {
        prefs_btn_clone.emit_clicked();
    });
    app.add_action(&prefs_action);

    // About action
    let about_parent = window.clone();
    let about_action = gtk4::gio::SimpleAction::new("about", None);
    about_action.connect_activate(move |_, _| {
        let about = adw::AboutWindow::new();
        about.set_transient_for(Some(&about_parent));
        about.set_application_name(&tr("gui.title"));
        about.set_developer_name(&tr("gui.about_developer"));
        about.set_version(env!("CARGO_PKG_VERSION"));
        about.set_comments(&tr("gui.about_comments"));
        about.set_website("https://angussoftware.com");
        about.set_support_url("https://github.com/RhoMancer/temperlux/issues");
        about.set_issue_url("https://github.com/RhoMancer/temperlux/issues/new");
        about.set_developers(&["RhoMancer https://github.com/RhoMancer", "sidevesh https://github.com/sidevesh"]);
        about.set_artists(&["Angus Software"]);
        about.set_license_type(gtk::License::Gpl30);
        about.set_copyright("© 2026 Angus Software");
        about.set_application_icon("dev.angussoftware.temperlux");
        about.present();
    });
    app.add_action(&about_action);

    // Loading spinner — shown immediately
    let spinner = gtk::Spinner::new();
    spinner.set_spinning(true);
    spinner.set_size_request(32, 32);
    let loading_label = gtk::Label::new(Some(&tr("gui.scanning")));
    loading_label.add_css_class("dim-label");
    let found_label = gtk::Label::new(None);
    found_label.add_css_class("dim-label");
    found_label.add_css_class("caption-heading");

    let loading_box = gtk::Box::new(Orientation::Vertical, 8);
    loading_box.set_valign(gtk::Align::Center);
    loading_box.set_halign(gtk::Align::Center);
    loading_box.append(&spinner);
    loading_box.append(&loading_label);
    loading_box.append(&found_label);

    let content = gtk::Box::new(Orientation::Vertical, 0);
    content.append(&header);
    content.append(&loading_box);
    content.set_vexpand(true);

    window.set_content(Some(&content));
    window.show();
    debug_log!("temperlux-gui: window shown with spinner");

    // F12: capture screenshot of the window to /tmp for UI review
    {
        let screenshot_window = window.clone();
        let key_controller = gtk4::EventControllerKey::new();
        key_controller.connect_key_pressed(move |_ctrl, key, _keycode, _state| {
            if key == gtk4::gdk::Key::F12 {
                if let Err(e) = capture_widget_to_png(&screenshot_window, "main") {
                    eprintln!("[screenshot] failed: {}", e);
                }
                gtk4::glib::Propagation::Stop
            } else {
                gtk4::glib::Propagation::Proceed
            }
        });
        window.add_controller(key_controller);
    }

    // Detect displays in background thread with progress updates
    let (tx, rx) = std::sync::mpsc::channel::<DetectProgress>();
    let settings_bg = settings.clone();

    std::thread::spawn(move || {
        debug_log!("temperlux-gui: detection thread started");
        let start = std::time::Instant::now();

        let bl = RealBacklightProvider;

        // Start DDC service — opens handles AND enters command loop on this thread.
        // Display info is returned; handles stay on the service thread.
        let (ddc_service, ffi_display_info) = match crate::display::ddc_ffi::DdcService::start() {
            Ok((svc, info)) => (Some(svc), info),
            Err(e) => {
                let _ = tx.send(DetectProgress::Done {
                    result: Err(format!("DDC FFI detect failed: {}", e)),
                    ddc_service: None,
                });
                return;
            }
        };

        debug_log!("temperlux-gui: FFI detected {} displays", ffi_display_info.len());

        // Send progress for each found display
        for disp in &ffi_display_info {
            let _ = tx.send(DetectProgress::Found {
                name: disp.model_name.clone(),
                current: disp.current_brightness,
            });
        }

        // Build viewmodel directly from FFI display info
        let result = MainViewModel::from_detected(&ffi_display_info, &bl, &settings_bg)
            .map_err(|e| e.to_string());

        debug_log!("temperlux-gui: detection took {:.1}s", start.elapsed().as_secs_f64());
        let _ = tx.send(DetectProgress::Done {
            result,
            ddc_service,
        });
    });

    // Poll for progress/result on main thread
    let content_clone = content.clone();
    let header_clone = header.clone();
    let prefs_btn_clone = prefs_btn.clone();
    let executor_clone = executor.clone();
    let window_weak = window.downgrade();
    let found_label_clone = found_label.clone();
    let found_count = std::cell::Cell::new(0u8);
    let ddc_holder_clone = ddc_service_holder.clone();
    let info_clone = display_info.clone();
    let slider_map_clone = slider_map.clone();

    gtk::glib::timeout_add_local(std::time::Duration::from_millis(100), move || {
        let window = match window_weak.upgrade() {
            Some(w) => w,
            None => return gtk::glib::ControlFlow::Break,
        };

        // Drain all pending messages
        loop {
            match rx.try_recv() {
                Ok(DetectProgress::Found { name, current }) => {
                    let count = found_count.get() + 1;
                    found_count.set(count);
                    found_label_clone.set_text(&tr_args("gui.found_displays", &[("count", &count.to_string())]));
                    debug_log!("temperlux-gui: found display '{}' at {}%", name, current);
                }
                Ok(DetectProgress::Done { result: Ok(vm), ddc_service }) => {
                    debug_log!("temperlux-gui: populating {} displays", vm.rows.len());
                    // Store DdcService so refresh button can shut it down
                    *ddc_holder_clone.lock().unwrap() = ddc_service.clone();
                    populate_window(
                        &window,
                        &content_clone,
                        &header_clone,
                        &prefs_btn_clone,
                        &executor_clone,
                        vm,
                        ddc_service,
                        &info_clone,
                        &slider_map_clone,
                    );
                    return gtk::glib::ControlFlow::Break;
                }
                Ok(DetectProgress::Done { result: Err(e), ddc_service: _ }) => {
                    debug_log!("temperlux-gui: detection failed: {}", e);
                    show_error_state(&content_clone, &tr_args("gui.detection_failed", &[("error", &e)]));
                    return gtk::glib::ControlFlow::Break;
                }
                Err(std::sync::mpsc::TryRecvError::Empty) => {
                    return gtk::glib::ControlFlow::Continue;
                }
                Err(std::sync::mpsc::TryRecvError::Disconnected) => {
                    show_error_state(&content_clone, &tr("gui.detection_crashed"));
                    return gtk::glib::ControlFlow::Break;
                }
            }
        }
    });
}

/// Populate the window with detected displays (called on main thread after detection).
#[allow(clippy::too_many_arguments)]
fn populate_window<E: HardwareExecutor>(
    _window: &adw::ApplicationWindow,
    content: &gtk::Box,
    header: &adw::HeaderBar,
    prefs_btn: &gtk::Button,
    executor: &E,
    vm: MainViewModel,
    ddc_service: Option<crate::display::ddc_ffi::DdcService>,
    display_info: &std::sync::Arc<std::sync::Mutex<Vec<(String, String)>>>,
    slider_map: &SliderMap,
) {
    // Populate shared display info for schedule editor
    {
        let mut info = display_info.lock().unwrap();
        info.clear();
        for row in &vm.rows {
            let key = backend_key_string(&row.backend_key);
            info.push((key, row.name.clone()));
        }
    }
    // Clear slider map (for refresh rebuilds)
    slider_map.borrow_mut().clear();
    // Clear loading spinner
    while let Some(child) = content.last_child() {
        content.remove(&child);
    }

    // Sync dropdown — Off / Same Value / Proportional (shared keys with CLI)
    let sync_off = tr("sync.off");
    let sync_same = tr("sync.same_value");
    let sync_prop = tr("sync.proportional");
    let sync_options = [sync_off.as_str(), sync_same.as_str(), sync_prop.as_str()];
    let sync_combo = gtk::DropDown::from_strings(&sync_options);
    sync_combo.set_tooltip_text(Some(&tr("sync.tooltip")));

    // Load saved sync mode
    let saved_settings = Settings::load().unwrap_or_default();
    let saved_mode = if saved_settings.proportional_sync {
        2u32 // Proportional
    } else {
        match saved_settings.sync_mode {
            SettingsSyncMode::Off => 0,
            SettingsSyncMode::SameValue => 1,
            SettingsSyncMode::Proportional => 2,
        }
    };
    sync_combo.set_selected(saved_mode);

    let sync_label = gtk::Label::new(Some(&tr("sync.label")));
    let sync_box = gtk::Box::new(Orientation::Horizontal, 6);
    sync_box.set_margin_start(16);
    sync_box.set_margin_end(16);
    sync_box.set_margin_top(8);
    sync_box.set_margin_bottom(4);
    sync_box.append(&sync_label);
    sync_box.append(&sync_combo);

    sync_combo.connect_selected_notify(|combo| {
        let idx = combo.selected();
        let mode = match idx {
            0 => SettingsSyncMode::Off,
            1 => SettingsSyncMode::SameValue,
            _ => SettingsSyncMode::Proportional,
        };
        let mut s = Settings::load().unwrap_or_default();
        s.sync_mode = mode;
        s.proportional_sync = mode == SettingsSyncMode::Proportional;
        if let Err(e) = s.save() {
            log::error!("Failed to save sync setting: {}", e);
        }
    });

    // Preferences button — connected after display rows are built (needs sync_state)
    let prefs_btn_placeholder = prefs_btn.clone();

    // Display rows with background worker pool
    let mut worker_pool = DisplayWorkerPool::new();
    let list_box = gtk::Box::new(Orientation::Vertical, 12);
    list_box.set_margin_start(16);
    list_box.set_margin_end(16);
    list_box.set_margin_top(12);
    list_box.set_margin_bottom(12);

    // Use DDC service for brightness commands — handles stay on the service thread
    // (fixes libddcutil thread lock conflicts)
    let ddc_svc = ddc_service; // Option<DdcService>
    log::info!("populate_window: {} rows, DDC service={}", vm.rows.len(), ddc_svc.is_some());

    if vm.rows.is_empty() {
        let empty = gtk::Label::new(Some(&tr("gui.no_displays")));
        empty.add_css_class("dim-label");
        list_box.append(&empty);
    }

    // Shared sync state: maps display key → (adjustment, raw_max, min_pct, max_pct)
    // When sync is active, slider changes propagate to other displays via this map.
    #[allow(clippy::arc_with_non_send_sync)]
    let sync_state: SyncState = Arc::new(Mutex::new(Vec::new()));
    let sync_active: Arc<Mutex<bool>> = Arc::new(Mutex::new(false));
    let sync_combo_ref = sync_combo.clone();

    // Sort rows by display_order from settings
    let saved_order = Settings::load().map(|s| s.display_order).unwrap_or_default();
    let mut sorted_rows = vm.rows.clone();
    if !saved_order.is_empty() {
        sorted_rows.sort_by_key(|r| {
            let key = backend_key_string(&r.backend_key);
            saved_order.iter().position(|k| k == &key).unwrap_or(usize::MAX)
        });
    }
    // Re-index after sort
    for (i, row) in sorted_rows.iter_mut().enumerate() {
        row.index = i + 1;
    }

    for row in sorted_rows.iter() {
        let display_row = build_display_row_with_sync(
            row, &mut worker_pool, &ddc_svc,
            &sync_state, &sync_combo_ref, &sync_active,
            slider_map,
        );

        list_box.append(&display_row);
    }

    let scrolled = gtk::ScrolledWindow::new();
    scrolled.set_child(Some(&list_box));
    scrolled.set_vexpand(true);

    // Now connect prefs button with access to sync_state (adjustments)
    let prefs_rows = sorted_rows.clone();
    let prefs_executor = executor.clone();
    let prefs_sync = sync_state.clone();
    let prefs_list = list_box.clone();
    prefs_btn_placeholder.connect_clicked(move |btn| {
        let root = btn.root().and_downcast::<gtk::Window>().unwrap();
        show_preferences_dialog(&root, &prefs_rows, &prefs_executor, &prefs_sync, &prefs_list);
    });

    content.append(header);
    content.append(&sync_box);
    content.append(&scrolled);
}

/// Show an error message in place of display list.
fn show_error_state(content: &gtk::Box, message: &str) {
    while let Some(child) = content.last_child() {
        content.remove(&child);
    }

    let error_label = gtk::Label::new(Some(message));
    error_label.add_css_class("dim-label");
    error_label.set_valign(gtk::Align::Center);
    error_label.set_halign(gtk::Align::Center);
    error_label.set_vexpand(true);

    content.append(&error_label);
}

struct SyncEntry {
    adjustment: gtk::Adjustment,
    min_pct: f64,
    max_pct: f64,
    prev_value: f64,
    range_label: Option<gtk::Label>,
    name_label: Option<gtk::Label>,
    hardware_name: String,
}

/// Re-entrancy guard — prevents sync propagation from triggering infinite loops.
struct SyncGuard {
    active: Arc<Mutex<bool>>,
}

impl SyncGuard {
    fn new(active: &Arc<Mutex<bool>>) -> Option<Self> {
        let mut guard = active.lock().unwrap();
        if *guard {
            return None; // Already in sync propagation
        }
        *guard = true;
        Some(Self { active: active.clone() })
    }
}

impl Drop for SyncGuard {
    fn drop(&mut self) {
        *self.active.lock().unwrap() = false;
    }
}

fn build_display_row_with_sync(
    row: &crate::gui::viewmodel::DisplayRow,
    worker_pool: &mut DisplayWorkerPool,
    ddc_service: &Option<crate::display::ddc_ffi::DdcService>,
    sync_state: &SyncState,
    sync_combo: &gtk::DropDown,
    sync_active: &Arc<Mutex<bool>>,
    slider_map: &SliderMap,
) -> gtk::Box {
    let container = gtk::Box::new(Orientation::Vertical, 4);

    // Name row — use compute_display_text for consistent formatting
    let name_row = gtk::Box::new(Orientation::Horizontal, 8);
    let display_key = backend_key_string(&row.backend_key);
    let display_text = compute_display_text(&display_key, row.index - 1, &row.hardware_name);
    let name = gtk::Label::new(Some(&display_text));
    name.set_halign(gtk::Align::Start);
    name.add_css_class("heading");
    name.set_hexpand(true);

    let rename_btn = gtk::Button::from_icon_name("document-edit-symbolic");
    rename_btn.set_tooltip_text(Some(&tr("gui.rename_tooltip")));
    rename_btn.add_css_class("flat");

    let default_name = row.hardware_name.clone();
    let _display_name = name.clone();
    let rename_key = display_key.clone();
    let rename_sync = sync_state.clone();
    rename_btn.connect_clicked(move |btn| {
        let root = btn.root().and_downcast::<gtk::Window>().unwrap();
        // Load current custom name fresh from settings each time dialog opens
        let custom = Settings::load().unwrap_or_default()
            .get_display(&rename_key)
            .custom_name;
        let current = custom.unwrap_or_default();
        show_rename_dialog(&root, &current, &default_name, &rename_key, &rename_sync);
    });

    name_row.append(&name);
    name_row.append(&rename_btn);

    // Brightness slider
    let slider_row = gtk::Box::new(Orientation::Horizontal, 8);

    let adjustment = gtk::Adjustment::new(
        row.brightness_pct,
        row.min_pct,
        row.max_pct,
        1.0,
        10.0,
        0.0,
    );

    let scale = gtk::Scale::builder()
        .orientation(Orientation::Horizontal)
        .adjustment(&adjustment)
        .hexpand(true)
        .draw_value(false)
        .build();

    let pct_label = gtk::Label::new(Some(&format!("{:.0}%", row.brightness_pct)));
    pct_label.set_width_chars(5);

    // Register slider for schedule apply updates
    slider_map.borrow_mut().push((display_key.clone(), adjustment.clone(), pct_label.clone()));

    let pct_clone = pct_label.clone();
    let backend_key = row.backend_key.clone();
    let raw_max = row.raw_max;
    let min_pct = row.min_pct;
    let max_pct = row.max_pct;
    let display_key = backend_key_string(&row.backend_key);

    // Get display_num for DDC commands (to send to DdcService)
    let ddc_display_idx = match &backend_key {
        BackendKey::Ddc(idx) => Some(*idx),
        BackendKey::Backlight(_) => None,
    };
    let backlight_name = match &backend_key {
        BackendKey::Ddc(_) => None,
        BackendKey::Backlight(n) => Some(n.clone()),
    };

    // Range label — only shown when show_range_labels setting is enabled
    let range_label = gtk::Label::new(None);
    range_label.set_halign(gtk::Align::Start);
    range_label.add_css_class("dim-label");
    range_label.add_css_class("caption");
    let show_range = Settings::load().unwrap_or_default().show_range_labels;
    if show_range {
        range_label.set_text(&tr_args("gui.range_label", &[
            ("min", &format!("{:.0}", row.min_pct)),
            ("max", &format!("{:.0}", row.max_pct)),
        ]));
    } else {
        range_label.set_visible(false);
    }

    // Register this display's adjustment in sync state
    let range_label_clone = range_label.clone();
    sync_state.lock().unwrap().push((
        display_key.clone(),
        SyncEntry {
            adjustment: adjustment.clone(),
            min_pct,
            max_pct,
            prev_value: row.brightness_pct,
            range_label: Some(range_label_clone),
            name_label: Some(name.clone()),
            hardware_name: row.hardware_name.clone(),
        },
    ));

    // Create or get a background worker for this display
    let worker_key = display_key.clone();

    // Clone DdcService for the worker closure (it's just a channel sender)
    let ddc_svc_clone = ddc_service.clone();
    let log_key = worker_key.clone();

    let _worker_key_perf = display_key.clone();
    let worker = worker_pool.get_or_create(&worker_key, move |raw| {
        if let Some(ref svc) = ddc_svc_clone {
            // Send command to DDC service thread — handle stays on that thread
            if let Some(dispnum) = ddc_display_idx {
                log::info!("[SLIDER] key='{}' dispnum={} raw={} → DdcService", log_key, dispnum, raw);

                // Emit DdcDispatched perf event
                let ddc_seq = crate::perf::next_seq();
                if crate::perf::is_enabled() {
                    crate::perf::log(crate::perf::Event::DdcDispatched {
                        seq: ddc_seq,
                        t_ms: crate::perf::now_ms(),
                        parent_seq: 0, // Will be filled from worker dequeue seq
                        display_num: dispnum as i32,
                        raw_value: raw as u16,
                    });
                }

                svc.set_brightness(dispnum as i32, raw as u16, ddc_seq);
            }
        } else if let Some(ref name) = backlight_name {
            let bl = RealBacklightProvider;
            let bl_seq = crate::perf::next_seq();
            if crate::perf::is_enabled() {
                crate::perf::log(crate::perf::Event::BacklightWriteStart {
                    seq: bl_seq,
                    t_ms: crate::perf::now_ms(),
                    parent_seq: 0,
                    name: name.clone(),
                    raw: raw as u32,
                });
            }
            let start = std::time::Instant::now();
            let result = bl.set_brightness(name, raw as u32);
            let duration_us = start.elapsed().as_micros() as u64;
            if crate::perf::is_enabled() {
                crate::perf::log(crate::perf::Event::BacklightWriteEnd {
                    seq: crate::perf::next_seq(),
                    t_ms: crate::perf::now_ms(),
                    parent_seq: bl_seq,
                    name: name.clone(),
                    raw: raw as u32,
                    duration_us,
                    success: result.is_ok(),
                });
            }
            if let Err(e) = result {
                log::error!("Backlight set_brightness failed for {}: {}", name, e);
            }
        }
    });

    // Clone the sender for the closure
    let worker_tx = worker.clone();
    let sync_state_clone = sync_state.clone();
    let sync_combo_clone = sync_combo.clone();
    let sync_active_clone = sync_active.clone();
    let my_key = display_key.clone();

    scale.connect_value_changed(move |s| {
        // S4: Label updates immediately
        let val = s.value();
        pct_clone.set_text(&format!("{:.0}%", val));

        // Calculate raw value
        let clamped = val.clamp(min_pct, max_pct);
        let raw = (clamped / 100.0 * raw_max as f64).round() as i32;

        // Emit SliderChanged perf event
        let slider_seq = crate::perf::next_seq();
        if crate::perf::is_enabled() {
            crate::perf::log(crate::perf::Event::SliderChanged {
                seq: slider_seq,
                t_ms: crate::perf::now_ms(),
                display_key: my_key.clone(),
                gtk_value_pct: val,
                raw_value: raw,
            });
        }

        // S1/S2: Submit to background worker — non-blocking, coalesces automatically
        worker_tx.set(raw, slider_seq);

        // Sync: propagate to other displays
        let sync_mode = sync_combo_clone.selected();
        if sync_mode == 0 {
            // Just update prev_value
            let mut state = sync_state_clone.lock().unwrap();
            if let Some((_, entry)) = state.iter_mut().find(|(k, _)| k == &my_key) {
                entry.prev_value = val;
            }
            return; // Off
        }

        // Re-entrancy guard — prevent sync feedback loops
        let _guard = match SyncGuard::new(&sync_active_clone) {
            Some(g) => g,
            None => return,
        };

        // Get this display's previous value for proportional delta
        let _prev_val;
        {
            let mut state = sync_state_clone.lock().unwrap();
            if let Some((_, entry)) = state.iter_mut().find(|(k, _)| k == &my_key) {
                _prev_val = entry.prev_value;
                entry.prev_value = val;
            } else {
                return;
            }
        }

        let state = sync_state_clone.lock().unwrap();
        // Collect updates while locked, apply after releasing.
        // MUST NOT call set_value() while holding sync_state lock —
        // it triggers value_changed on other displays which try to re-lock.
        let mut updates: Vec<(gtk::Adjustment, f64)> = Vec::new();
        for (key, entry) in state.iter() {
            if key == &my_key {
                continue;
            }

            let new_pct = if sync_mode == 1 {
                // Same Value: set all displays to the same %
                clamped
            } else {
                // Proportional: map source position proportionally to target's range
                crate::sync::proportional_map(
                    val, min_pct, max_pct,
                    entry.min_pct, entry.max_pct,
                )
            };

            let clamped_other = new_pct.clamp(entry.min_pct, entry.max_pct);
            updates.push((entry.adjustment.clone(), clamped_other));
        }
        drop(state); // Release lock before touching GTK

        for (adj, val) in updates {
            adj.set_value(val);
        }
    });

    slider_row.append(&scale);
    slider_row.append(&pct_label);

    // Latency indicator — only shown when perf logging is enabled
    if crate::perf::is_enabled() {
        let latency_label = gtk::Label::new(None);
        latency_label.set_width_chars(8);
        latency_label.add_css_class("dim-label");
        latency_label.add_css_class("caption-heading");

        let latency_worker = worker.clone();
        let latency_lbl = latency_label.clone();
        let latency_svc = ddc_service.clone();
        let latency_dispnum = ddc_display_idx;
        gtk::glib::timeout_add_local(std::time::Duration::from_millis(200), move || {
            let ms = latency_worker.latency_ms();
            let svc_ms = if let Some(ref svc) = latency_svc {
                if let Some(dn) = latency_dispnum {
                    svc.get_latency_ms(dn as i32)
                } else {
                    0
                }
            } else {
                0
            };
            let display_ms = if svc_ms > 0 { svc_ms } else { ms };
            if display_ms > 0 {
                latency_lbl.set_text(&format!("{}ms", display_ms));
            }
            gtk::glib::ControlFlow::Continue
        });

        slider_row.append(&latency_label);
    }

    container.append(&name_row);
    container.append(&slider_row);

    // Range label — shown based on show_range_labels setting or when caps are active
    container.append(&range_label);

    // Error label — shows "Monitor not responding" if DDC writes fail after retries
    if let Some(ref svc) = ddc_service {
        if let Some(dn) = ddc_display_idx {
            let error_label = gtk::Label::new(None);
            error_label.set_halign(gtk::Align::Start);
            error_label.add_css_class("error");
            error_label.add_css_class("caption");
            error_label.set_visible(false);

            let err_lbl = error_label.clone();
            let err_svc = svc.clone();
            gtk::glib::timeout_add_local(std::time::Duration::from_millis(500), move || {
                match err_svc.get_error(dn as i32) {
                    Some(_) => {
                        err_lbl.set_text(&tr("gui.monitor_not_responding"));
                        err_lbl.set_visible(true);
                    }
                    None => {
                        err_lbl.set_visible(false);
                    }
                }
                gtk::glib::ControlFlow::Continue
            });

            container.append(&error_label);
        }
    }

    let sep = gtk::Separator::new(Orientation::Horizontal);
    container.append(&sep);
    container
}

fn show_rename_dialog(
    parent: &gtk::Window,
    current_name: &str,
    default_name: &str,
    row_backend_key: &str,
    sync_state: &SyncState,
) {
    let dialog = gtk::MessageDialog::builder()
        .transient_for(parent)
        .modal(true)
        .title(tr("gui.rename_title"))
        .message_type(gtk::MessageType::Question)
        .build();

    let entry = gtk::Entry::builder()
        .placeholder_text(default_name)
        .text(current_name)
        .hexpand(true)
        .build();

    let area: gtk::Box = dialog.message_area().downcast().unwrap();
    area.append(&entry);

    dialog.add_button(&tr("actions.cancel"), gtk::ResponseType::Cancel);
    dialog.add_button(&tr("actions.save"), gtk::ResponseType::Accept);
    if let Some(save_btn) = dialog.widget_for_response(gtk::ResponseType::Accept) {
        save_btn.add_css_class("suggested-action");
    }

    let entry_text = entry.clone();
    let rename_backend_key = row_backend_key.to_string();
    let rename_sync = sync_state.clone();
    dialog.connect_response(move |dlg, resp| {
        if resp == gtk::ResponseType::Accept {
            let new_name = entry_text.text().to_string();
            let mut settings = Settings::load().unwrap_or_default();
            let ds = settings.get_display(&rename_backend_key);

            let custom = if new_name.is_empty() {
                None
            } else {
                Some(new_name)
            };
            settings.set_display(&rename_backend_key, crate::settings::DisplaySettings {
                min_brightness: ds.min_brightness,
                max_brightness: ds.max_brightness,
                custom_name: custom,
            });
            if let Err(e) = settings.save() {
                log::error!("Failed to save rename: {}", e);
            }

            // Refresh all labels from single source of truth
            refresh_name_labels(&rename_sync);
        }
        dlg.close();
    });

    dialog.show();
}

fn show_preferences_dialog<E: HardwareExecutor>(
    parent: &gtk::Window,
    rows: &[crate::gui::viewmodel::DisplayRow],
    executor: &E,
    sync_state: &SyncState,
    list_box: &gtk::Box,
) {
    let dialog = gtk::Dialog::builder()
        .title(tr("gui.prefs_title"))
        .transient_for(parent)
        .modal(true)
        .default_width(350)
        .build();

    dialog.add_button(&tr("actions.cancel"), gtk::ResponseType::Cancel);
    dialog.add_button(&tr("actions.apply"), gtk::ResponseType::Apply);
    if let Some(apply_btn) = dialog.widget_for_response(gtk::ResponseType::Apply) {
        apply_btn.add_css_class("suggested-action");
    }

    let content_area = dialog.content_area();
    let outer = gtk::Box::new(Orientation::Vertical, 16);
    outer.set_margin_start(16);
    outer.set_margin_end(16);
    outer.set_margin_top(12);
    outer.set_margin_bottom(12);

    let saved_settings = Settings::load().unwrap_or_default();

    // Show display numbers toggle
    let numbers_check = gtk::CheckButton::with_label(&tr("gui.show_numbers"));
    numbers_check.set_active(saved_settings.show_display_numbers);
    outer.append(&numbers_check);

    // Autostart toggle
    let autostart_check = gtk::CheckButton::with_label(&tr("gui.start_on_login"));
    autostart_check.set_active(saved_settings.autostart);
    autostart_check.set_tooltip_text(Some(&tr("gui.start_on_login_tooltip")));
    outer.append(&autostart_check);

    let time_24h_check = gtk::CheckButton::with_label(&tr("gui.use_24h_time"));
    time_24h_check.set_active(saved_settings.use_24h_time);
    time_24h_check.set_tooltip_text(Some(&tr("gui.use_24h_time_tooltip")));
    outer.append(&time_24h_check);

    let range_check = gtk::CheckButton::with_label(&tr("gui.show_range_labels"));
    range_check.set_active(saved_settings.show_range_labels);
    range_check.set_tooltip_text(Some(&tr("gui.show_range_labels_tooltip")));
    outer.append(&range_check);

    // Theme mode dropdown (light/dark/system)
    let mode_label = gtk::Label::new(Some(&tr("gui.mode")));
    mode_label.set_halign(gtk::Align::Start);
    let theme_dropdown = gtk::DropDown::from_strings(&[
        tr("gui.match_system").as_str(),
        tr("gui.light").as_str(),
        tr("gui.dark").as_str(),
    ]);
    theme_dropdown.set_selected(match saved_settings.theme_mode {
        crate::settings::ThemeMode::System => 0,
        crate::settings::ThemeMode::Light => 1,
        crate::settings::ThemeMode::Dark => 2,
    });
    let mode_row = gtk::Box::new(Orientation::Horizontal, 8);
    mode_row.append(&mode_label);
    mode_row.append(&theme_dropdown);
    outer.append(&mode_row);

    // Light color theme dropdown (all themes, with mismatch warning)
    let light_label = gtk::Label::new(Some(&tr("gui.light_colors")));
    light_label.set_halign(gtk::Align::Start);
    let theme_names: Vec<&str> = crate::settings::ColorTheme::all()
        .iter().map(|(_, name)| *name).collect();
    let light_dropdown = gtk::DropDown::from_strings(&theme_names);
    let light_idx = crate::settings::ColorTheme::all()
        .iter().position(|(t, _)| *t == saved_settings.light_theme)
        .unwrap_or(0);
    light_dropdown.set_selected(light_idx as u32);
    // Warning label for dark theme in light slot
    let light_warn = gtk::Label::new(Some(&tr("gui.light_theme_warning")));
    light_warn.set_halign(gtk::Align::Start);
    light_warn.set_css_classes(&["warning"]);
    light_warn.set_visible(false);
    let light_row = gtk::Box::new(Orientation::Horizontal, 8);
    light_row.append(&light_label);
    light_row.append(&light_dropdown);
    outer.append(&light_row);
    outer.append(&light_warn);

    // Dark color theme dropdown (all themes, with mismatch warning)
    let dark_label = gtk::Label::new(Some(&tr("gui.dark_colors")));
    dark_label.set_halign(gtk::Align::Start);
    let dark_dropdown = gtk::DropDown::from_strings(&theme_names);
    let dark_idx = crate::settings::ColorTheme::all()
        .iter().position(|(t, _)| *t == saved_settings.dark_theme)
        .unwrap_or(0);
    dark_dropdown.set_selected(dark_idx as u32);
    // Warning label for light theme in dark slot
    let dark_warn = gtk::Label::new(Some(&tr("gui.dark_theme_warning")));
    dark_warn.set_halign(gtk::Align::Start);
    dark_warn.set_css_classes(&["warning"]);
    dark_warn.set_visible(false);
    let dark_row = gtk::Box::new(Orientation::Horizontal, 8);
    dark_row.append(&dark_label);
    dark_row.append(&dark_dropdown);
    outer.append(&dark_row);
    outer.append(&dark_warn);

    // Update warning visibility on selection change
    let light_warn_clone = light_warn.clone();
    let light_dd_for_closure = light_dropdown.clone();
    light_dropdown.connect_selected_notify(move |_| {
        let idx = light_dd_for_closure.selected() as usize;
        let (theme, _) = crate::settings::ColorTheme::all()
            .get(idx)
            .copied()
            .unwrap_or((crate::settings::ColorTheme::AngusLight, ""));
        light_warn_clone.set_visible(!theme.is_light_theme());
    });
    // Trigger initial state
    {
        let idx = light_dropdown.selected() as usize;
        let (theme, _) = crate::settings::ColorTheme::all()
            .get(idx)
            .copied()
            .unwrap_or((crate::settings::ColorTheme::AngusLight, ""));
        light_warn.set_visible(!theme.is_light_theme());
    }

    let dark_warn_clone = dark_warn.clone();
    let dark_dd_for_closure = dark_dropdown.clone();
    dark_dropdown.connect_selected_notify(move |_| {
        let idx = dark_dd_for_closure.selected() as usize;
        let (theme, _) = crate::settings::ColorTheme::all()
            .get(idx)
            .copied()
            .unwrap_or((crate::settings::ColorTheme::AngusDark, ""));
        dark_warn_clone.set_visible(!theme.is_dark_theme());
    });
    // Trigger initial state
    {
        let idx = dark_dropdown.selected() as usize;
        let (theme, _) = crate::settings::ColorTheme::all()
            .get(idx)
            .copied()
            .unwrap_or((crate::settings::ColorTheme::AngusDark, ""));
        dark_warn.set_visible(!theme.is_dark_theme());
    }

    let sep_top = gtk::Separator::new(Orientation::Horizontal);
    outer.append(&sep_top);

    // Build initial order from CURRENT sync_state (not stale rows)
    let initial_order: Vec<String> = {
        let state = sync_state.lock().unwrap();
        state.iter().map(|(k, _)| k.clone()).collect()
    };

    // Track pending order — arrows swap entries here, Apply saves it
    let pending_order: Arc<Mutex<Vec<String>>> = Arc::new(Mutex::new(initial_order));

    // Build per-display settings: key → (min_spin, max_spin)
    // We store spinners keyed by backend key so they survive reorder
    #[allow(clippy::arc_with_non_send_sync)]
    let spinner_map: SpinnerMap =
        Arc::new(Mutex::new(std::collections::HashMap::new()));

    // Pre-create all spinners
    for row in rows {
        let settings_key = match &row.backend_key {
            BackendKey::Ddc(i) => format!("ddc:{}", i),
            BackendKey::Backlight(n) => format!("bl:{}", n),
        };
        let (saved_min, saved_max) = {
            let ds = saved_settings.get_display(&settings_key);
            if ds.min_brightness > 0.0 || ds.max_brightness < 100.0 {
                (ds.min_brightness, ds.max_brightness)
            } else {
                (row.min_pct, row.max_pct)
            }
        };

        let min_spin = gtk::SpinButton::with_range(0.0, 100.0, 1.0);
        min_spin.set_value(saved_min);
        let max_spin = gtk::SpinButton::with_range(0.0, 100.0, 1.0);
        max_spin.set_value(saved_max);

        let min_clone = min_spin.clone();
        max_spin.connect_value_changed(move |max_s| {
            let max_val = max_s.value();
            if min_clone.value() > max_val { min_clone.set_value(max_val); }
        });
        let max_clone = max_spin.clone();
        min_spin.connect_value_changed(move |min_s| {
            let min_val = min_s.value();
            if max_clone.value() < min_val { max_clone.set_value(min_val); }
        });

        spinner_map.lock().unwrap().insert(settings_key, (min_spin, max_spin, row.brightness_pct, row.raw_max));
    }

    // Container for per-display rows (will be rebuilt on reorder)
    let rows_container = gtk::Box::new(Orientation::Vertical, 8);

    // Build the hardware_name lookup from rows
    let key_hw_names: std::collections::HashMap<String, String> = rows.iter().map(|r| {
        let key = match &r.backend_key {
            BackendKey::Ddc(i) => format!("ddc:{}", i),
            BackendKey::Backlight(n) => format!("bl:{}", n),
        };
        (key, r.hardware_name.clone())
    }).collect();

    // Rebuild function for the display rows section
    let rebuild_fn_container = rows_container.clone();
    let rebuild_fn_pending = pending_order.clone();
    let rebuild_fn_spinners = spinner_map.clone();
    let _rebuild_fn_hw = key_hw_names.clone();

    #[allow(clippy::too_many_arguments)]
    fn rebuild_prefs_rows(
        container: &gtk::Box,
        pending_order: &Arc<Mutex<Vec<String>>>,
        spinner_map: &SpinnerMap,
        hw_names: &std::collections::HashMap<String, String>,
        container_weak: gtk::Box,
        order_weak: Arc<Mutex<Vec<String>>>,
        spinners_weak: SpinnerMap,
        hw_weak: std::collections::HashMap<String, String>,
    ) {
        // Remove existing children
        let mut child = container.first_child();
        while let Some(c) = child {
            let next = c.next_sibling();
            c.unparent();
            child = next;
        }

        let keys = pending_order.lock().unwrap().clone();
        let n = keys.len();
        let spinners = spinner_map.lock().unwrap();

        for (i, key) in keys.iter().enumerate() {
            let hw_name = hw_names.get(key).cloned().unwrap_or(key.clone());
            // Always show number in prefs, regardless of setting
            let display_num = key.strip_prefix("ddc:")
                .or_else(|| key.strip_prefix("bl:"))
                .and_then(|s| s.parse::<u32>().ok())
                .unwrap_or(1);
            let name = Settings::load().unwrap_or_default()
                .get_display(key).custom_name
                .unwrap_or(hw_name);
            let label_text = format!("{}. {}", display_num, name);
            let label = gtk::Label::new(Some(&label_text));
            label.set_halign(gtk::Align::Start);
            label.add_css_class("heading");
            label.set_hexpand(true);

            // Up/down arrow buttons
            let up_btn = gtk::Button::from_icon_name("go-up-symbolic");
            up_btn.add_css_class("flat");
            if i == 0 { up_btn.set_sensitive(false); }

            let down_btn = gtk::Button::from_icon_name("go-down-symbolic");
            down_btn.add_css_class("flat");
            if i == n - 1 { down_btn.set_sensitive(false); }

            let up_key = key.clone();
            let up_order = order_weak.clone();
            let up_container = container_weak.clone();
            let up_spinners = spinners_weak.clone();
            let up_hw = hw_weak.clone();
            up_btn.connect_clicked(move |_| {
                let mut o = up_order.lock().unwrap();
                let pos = o.iter().position(|k| k == &up_key).unwrap();
                if pos > 0 { o.swap(pos, pos - 1); }
                drop(o);
                rebuild_prefs_rows(&up_container, &up_order, &up_spinners, &up_hw,
                    up_container.clone(), up_order.clone(), up_spinners.clone(), up_hw.clone());
            });

            let down_key = key.clone();
            let down_order = order_weak.clone();
            let down_container = container_weak.clone();
            let down_spinners = spinners_weak.clone();
            let down_hw = hw_weak.clone();
            down_btn.connect_clicked(move |_| {
                let mut o = down_order.lock().unwrap();
                let pos = o.iter().position(|k| *k == down_key).unwrap();
                if pos < o.len() - 1 { o.swap(pos, pos + 1); }
                drop(o);
                rebuild_prefs_rows(&down_container, &down_order, &down_spinners, &down_hw,
                    down_container.clone(), down_order.clone(), down_spinners.clone(), down_hw.clone());
            });

            // Name + arrows row
            let header_row = gtk::Box::new(Orientation::Horizontal, 4);
            header_row.append(&label);
            header_row.append(&up_btn);
            header_row.append(&down_btn);
            container.append(&header_row);

            // Min/Max spinners (clone widgets from map)
            if let Some((min_s, max_s, _, _)) = spinners.get(key) {
                let min_box = gtk::Box::new(Orientation::Horizontal, 8);
                let min_label = gtk::Label::new(Some(&tr("gui.min_percent")));
                min_label.set_width_chars(8);
                min_box.append(&min_label);
                min_box.append(min_s);
                container.append(&min_box);

                let max_box = gtk::Box::new(Orientation::Horizontal, 8);
                let max_label = gtk::Label::new(Some(&tr("gui.max_percent")));
                max_label.set_width_chars(8);
                max_box.append(&max_label);
                max_box.append(max_s);
                container.append(&max_box);
            }

            let sep = gtk::Separator::new(Orientation::Horizontal);
            container.append(&sep);
        }

        if keys.is_empty() {
            let empty = gtk::Label::new(Some(&tr("gui.no_displays_config")));
            empty.add_css_class("dim-label");
            container.append(&empty);
        }
    }

    rebuild_prefs_rows(
        &rebuild_fn_container, &rebuild_fn_pending, &rebuild_fn_spinners, &key_hw_names,
        rebuild_fn_container.clone(), rebuild_fn_pending.clone(), rebuild_fn_spinners.clone(), key_hw_names.clone(),
    );

    outer.append(&rows_container);
    content_area.append(&outer);

    let prefs_executor = executor.clone();
    let sync_state_clone = sync_state.clone();
    let numbers_check_clone = numbers_check.clone();
    let autostart_check_clone = autostart_check.clone();
    let time_24h_check_clone = time_24h_check.clone();
    let range_check_clone = range_check.clone();
    let theme_dropdown_clone = theme_dropdown.clone();
    let light_dropdown_clone = light_dropdown.clone();
    let dark_dropdown_clone = dark_dropdown.clone();
    let apply_order = pending_order.clone();
    let apply_spinners = spinner_map.clone();
    let apply_list = list_box.clone();

    dialog.connect_response(move |dlg, resp| {
        if resp == gtk::ResponseType::Apply {
            let mut settings = Settings::load().unwrap_or_default();
            settings.show_display_numbers = numbers_check_clone.is_active();
            settings.use_24h_time = time_24h_check_clone.is_active();
            settings.show_range_labels = range_check_clone.is_active();

            // Apply theme mode
            let theme_mode = match theme_dropdown_clone.selected() {
                1 => crate::settings::ThemeMode::Light,
                2 => crate::settings::ThemeMode::Dark,
                _ => crate::settings::ThemeMode::System,
            };
            settings.theme_mode = theme_mode;
            crate::gui::theme::apply_theme_mode(theme_mode);

            // Apply color themes
            let light_theme = crate::settings::ColorTheme::all()
                .get(light_dropdown_clone.selected() as usize)
                .map(|(t, _)| *t)
                .unwrap_or_default();
            let dark_theme = crate::settings::ColorTheme::all()
                .get(dark_dropdown_clone.selected() as usize)
                .map(|(t, _)| *t)
                .unwrap_or_default();
            settings.light_theme = light_theme;
            settings.dark_theme = dark_theme;
            crate::gui::theme::apply_color_themes(light_theme, dark_theme);

            // Apply autostart setting
            let autostart_enabled = autostart_check_clone.is_active();
            settings.autostart = autostart_enabled;
            if autostart_enabled {
                if let Err(e) = crate::autostart::enable() {
                    log::error!("Failed to enable autostart: {}", e);
                }
            } else {
                if let Err(e) = crate::autostart::disable() {
                    log::error!("Failed to disable autostart: {}", e);
                }
            }

            settings.show_range_labels = range_check_clone.is_active();

            // Save brightness ranges from spinners
            let show_range = settings.show_range_labels;
            let spinners = apply_spinners.lock().unwrap();
            for (key, (min_spin, max_spin, brightness_pct, raw_max)) in spinners.iter() {
                let min_val = min_spin.value();
                let max_val = max_spin.value();
                let existing = settings.get_display(key);
                settings.set_display(key, crate::settings::DisplaySettings {
                    min_brightness: min_val,
                    max_brightness: max_val,
                    custom_name: existing.custom_name,
                });

                // Update sync_state and apply GTK changes
                let display_key = key.clone();
                let ddc_index: usize = key.strip_prefix("ddc:")
                    .and_then(|s| s.parse().ok())
                    .unwrap_or(0);
                let gtk_updates: Option<(gtk::Adjustment, f64, f64, f64, Option<gtk::Label>, String)> = {
                    let mut state = sync_state_clone.lock().unwrap();
                    if let Some((_, entry)) = state.iter_mut().find(|(k, _)| k == &display_key) {
                        entry.min_pct = min_val;
                        entry.max_pct = max_val;
                        let current = entry.adjustment.value();
                        let clamped = current.clamp(min_val, max_val);
                        entry.prev_value = clamped;
                        let label_text = tr_args("gui.range_label", &[
                            ("min", &format!("{:.0}", min_val)),
                            ("max", &format!("{:.0}", max_val)),
                        ]);
                        Some((entry.adjustment.clone(), min_val, max_val, clamped, entry.range_label.clone(), label_text))
                    } else {
                        None
                    }
                };
                if let Some((adj, min_v, max_v, clamped, range_lbl, label_text)) = gtk_updates {
                    adj.set_lower(min_v);
                    adj.set_upper(max_v);
                    adj.set_value(clamped);
                    if let Some(lbl) = range_lbl {
                        lbl.set_text(&label_text);
                        lbl.set_visible(show_range);
                    }
                }

                if *brightness_pct > max_val {
                    let raw = (max_val / 100.0 * *raw_max as f64).round() as i32;
                    prefs_executor.execute(&[HardwareCommand::SetDdc { index: ddc_index, raw }]);
                } else if *brightness_pct < min_val {
                    let raw = (min_val / 100.0 * *raw_max as f64).round() as i32;
                    prefs_executor.execute(&[HardwareCommand::SetDdc { index: ddc_index, raw }]);
                }
            }
            drop(spinners);

            // Save display order
            let new_order = apply_order.lock().unwrap().clone();
            settings.display_order = new_order.clone();
            if let Err(e) = settings.save() {
                log::error!("Failed to save preferences: {}", e);
            }

            // Collect GTK children BEFORE reordering, map key→widget
            let child_map: std::collections::HashMap<String, gtk::Widget> = {
                let state = sync_state_clone.lock().unwrap();
                let mut children: Vec<gtk::Widget> = Vec::new();
                let mut child = apply_list.first_child();
                while let Some(c) = child {
                    children.push(c.clone());
                    child = c.next_sibling();
                }
                state.iter().zip(children.iter())
                    .map(|((k, _), c)| (k.clone(), c.clone()))
                    .collect()
            };

            // Reorder sync_state to match new order
            {
                let mut state = sync_state_clone.lock().unwrap();
                for (target_idx, key) in new_order.iter().enumerate() {
                    if let Some(current_idx) = state.iter().position(|(k, _)| k == key) {
                        if current_idx != target_idx {
                            state.swap(current_idx, target_idx);
                        }
                    }
                }
            }

            // Unparent all, re-append in new_order
            for widget in child_map.values() {
                widget.unparent();
            }
            for key in &new_order {
                if let Some(widget) = child_map.get(key) {
                    apply_list.append(widget);
                }
            }

            // Refresh all name labels
            refresh_name_labels(&sync_state_clone);
        }
        dlg.close();
    });

    dialog.show();
}

#[cfg(test)]
mod tests {
    use super::*;
    use crate::display::ddc_ffi::DdcDisplayInfo as FfiDdcDisplayInfo;
    use crate::display::provider::BacklightDisplayInfo;
    use std::sync::Arc;
    use std::sync::Mutex;

    /// Mock backlight provider for GUI tests.
    #[derive(Debug, Clone)]
    struct MockBl {
        displays: Vec<BacklightDisplayInfo>,
    }

    impl BacklightProvider for MockBl {
        fn detect(&self) -> anyhow::Result<Vec<BacklightDisplayInfo>> {
            Ok(self.displays.clone())
        }
        fn set_brightness(&self, _name: &str, _raw_value: u32) -> anyhow::Result<()> {
            Ok(())
        }
    }

    fn ddc_info(name: &str, current: u16, max: u16) -> FfiDdcDisplayInfo {
        FfiDdcDisplayInfo { display_num: 1, model_name: name.to_string(), current_brightness: current, max_brightness: max }
    }

    // G1: Slider fires hardware command
    #[test]
    fn slider_fires_hardware_command() {
        let executor = MockHardwareExecutor::default();

        // Simulate what the slider callback does
        let cmd = HardwareCommand::SetDdc { index: 1, raw: 75 };
        executor.execute(&[cmd.clone()]);

        let cmds = executor.commands.lock().unwrap();
        assert_eq!(cmds.len(), 1);
        assert_eq!(cmds[0], HardwareCommand::SetDdc { index: 1, raw: 75 });
    }

    // G5: Empty state shows placeholder
    #[test]
    fn empty_state_shows_placeholder() {
        let ddc_list: Vec<FfiDdcDisplayInfo> = vec![];
        let vm = MainViewModel::from_detected(
            &ddc_list,
            &MockBl { displays: vec![] },
            &Settings::default(),
        ).unwrap();

        assert!(vm.rows.is_empty());
        // The GUI code checks vm.rows.is_empty() and shows "No displays detected"
    }

    // G6: Range label shown when caps are set
    #[test]
    fn range_label_shown_when_capped() {
        let mut settings = Settings::default();
        settings.set_display("ddc:1", crate::settings::DisplaySettings {
            min_brightness: 10.0,
            max_brightness: 90.0,
            custom_name: None,
        });

        let ddc_list = vec![ddc_info("Test", 50, 100)];
        let vm = MainViewModel::from_detected(
            &ddc_list,
            &MockBl { displays: vec![] },
            &settings,
        ).unwrap();

        assert!((vm.rows[0].min_pct - 10.0).abs() < f64::EPSILON);
        assert!((vm.rows[0].max_pct - 90.0).abs() < f64::EPSILON);
        // GUI code: if row.min_pct > 0.0 || row.max_pct < 100.0 → show range label
    }

    // G4: Sync toggle state
    #[test]
    fn sync_toggle_state_matches_settings() {
        let mut settings = Settings::default();
        settings.proportional_sync = true;

        let ddc_list = vec![ddc_info("Test", 50, 100)];
        let vm = MainViewModel::from_detected(
            &ddc_list,
            &MockBl { displays: vec![] },
            &settings,
        ).unwrap();

        assert!(vm.sync_enabled);
    }

    // G1 extended: Verify raw value calculation matches viewmodel
    #[test]
    fn slider_raw_value_calculation() {
        let ddc_list = vec![ddc_info("Test", 50, 100)];
        let bl = MockBl { displays: vec![] };
        let vm = MainViewModel::from_detected(&ddc_list, &bl, &Settings::default()).unwrap();
        let row = &vm.rows[0];

        // Slider at 75% → raw = 75% of 100 = 75
        let val = 75.0_f64;
        let clamped = val.clamp(row.min_pct, row.max_pct);
        let raw = (clamped / 100.0 * row.raw_max as f64).round() as i32;
        assert_eq!(raw, 75);
    }

    // G1 extended: Slider with caps clamps raw value
    #[test]
    fn slider_with_caps_clamps_raw() {
        let mut settings = Settings::default();
        settings.set_display("ddc:1", crate::settings::DisplaySettings {
            min_brightness: 10.0,
            max_brightness: 80.0,
            custom_name: None,
        });

        let ddc_list = vec![ddc_info("Test", 50, 100)];
        let bl = MockBl { displays: vec![] };
        let vm = MainViewModel::from_detected(&ddc_list, &bl, &settings).unwrap();
        let row = &vm.rows[0];

        // Slider at 90% but caps at 80% → raw = 80% of 100 = 80
        let val = 90.0_f64;
        let clamped = val.clamp(row.min_pct, row.max_pct);
        let raw = (clamped / 100.0 * row.raw_max as f64).round() as i32;
        assert_eq!(raw, 80);
    }

    // G3: Prefs dialog re-clamp produces hardware command
    #[test]
    fn prefs_re_clamp_produces_command() {
        let executor = MockHardwareExecutor::default();

        // Create VM with display at 90%
        let ddc_list = vec![ddc_info("Test", 90, 100)];
        let bl = MockBl { displays: vec![] };
        let mut vm = MainViewModel::from_detected(&ddc_list, &bl, &Settings::default()).unwrap();

        // Set caps to max 80% — should re-clamp and produce command
        let result = vm.set_caps(1, 0.0, 80.0).unwrap();
        assert!(result.re_clamped.is_some());

        let cmd = result.re_clamped.unwrap();
        executor.execute(&[cmd]);

        let cmds = executor.commands.lock().unwrap();
        assert_eq!(cmds.len(), 1);
        assert_eq!(cmds[0], HardwareCommand::SetDdc { index: 1, raw: 80 });
    }

    // G1: Executor captures multiple commands
    #[test]
    fn executor_captures_multiple_commands() {
        let executor = MockHardwareExecutor::default();
        executor.execute(&[
            HardwareCommand::SetDdc { index: 1, raw: 50 },
            HardwareCommand::SetDdc { index: 1, raw: 75 },
        ]);

        let cmds = executor.commands.lock().unwrap();
        assert_eq!(cmds.len(), 2);
        assert_eq!(cmds[0], HardwareCommand::SetDdc { index: 1, raw: 50 });
        assert_eq!(cmds[1], HardwareCommand::SetDdc { index: 1, raw: 75 });
    }

    // G2: Rename flow produces correct settings key
    #[test]
    fn rename_flow_produces_settings_key() {
        let ddc_list = vec![ddc_info("Test", 50, 100)];
        let bl = MockBl { displays: vec![] };
        let mut vm = MainViewModel::from_detected(&ddc_list, &bl, &Settings::default()).unwrap();

        let key = vm.rename(1, "Left Monitor".to_string());
        assert_eq!(key, Some("ddc:1".to_string()));

        let mut settings = Settings::default();
        vm.apply_rename_to_settings(key.as_ref().unwrap(), "Left Monitor", &mut settings);
        let ds = settings.get_display("ddc:1");
        assert_eq!(ds.custom_name, Some("Left Monitor".to_string()));
    }
}
