// SPDX-License-Identifier: GPL-3.0-or-later

/// Schedule list screen — shows all schedule profiles with add/delete/edit.
/// Schedule editor screen — edit a single profile's monitors, brightness, and triggers.
use gtk4::prelude::*;
use gtk4::{self as gtk, Orientation};
use libadwaita as adw;
use libadwaita::prelude::AdwApplicationWindowExt;
use std::cell::RefCell;
use std::collections::HashMap;
use std::rc::Rc;
use std::sync::{Arc, Mutex};

use crate::gui::widgets::brightness_slider::build_brightness_slider;
use crate::i18n::{tr, tr_args};
use crate::schedule;
use crate::schedule::model::{ScheduleProfile, ScheduleTrigger};
use crate::settings::{Settings, SyncMode};
use chrono::Timelike;

type SliderData = Rc<RefCell<HashMap<String, (gtk::Adjustment, gtk::Label, f64, f64)>>>;

/// Format a NaiveTime as "HH:MM" (24h) or "H:MM AM/PM" based on user preference.
fn format_time(h: u32, m: u32, use_24h: bool) -> String {
    if use_24h {
        format!("{:02}:{:02}", h, m)
    } else {
        let (h12, suffix) = match h {
            0 => (12, "AM"),
            1..=11 => (h, "AM"),
            12 => (12, "PM"),
            _ => (h - 12, "PM"),
        };
        format!("{}:{:02} {}", h12, m, suffix)
    }
}

/// Show the schedule list as a new window.
pub fn show_schedule_list(
    parent: &gtk::Window,
    display_info: Vec<(String, String)>,
    on_apply: impl Fn(&str) + 'static,
) {
    let window = adw::ApplicationWindow::builder()
        .title(tr("schedules.label"))
        .transient_for(parent)
        .default_width(450)
        .default_height(550)
        .build();

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

    // Back button
    let back_window = window.clone();
    let back_btn = gtk::Button::from_icon_name("go-previous-symbolic");
    back_btn.set_tooltip_text(Some(&tr("actions.back")));
    back_btn.add_css_class("flat");
    back_btn.connect_clicked(move |_| {
        back_window.close();
    });
    header.pack_start(&back_btn);

    // Add button
    let add_btn = gtk::Button::from_icon_name("list-add-symbolic");
    add_btn.set_tooltip_text(Some(&tr("schedules.new")));
    add_btn.add_css_class("flat");
    header.pack_start(&add_btn);

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

    // --- Location section ---
    let settings = Settings::load().unwrap_or_default();

    let loc_frame = gtk::Box::new(Orientation::Vertical, 6);
    loc_frame.set_margin_start(16);
    loc_frame.set_margin_end(16);
    loc_frame.set_margin_top(8);

    let loc_heading = gtk::Label::new(Some(&tr("gui.location")));
    loc_heading.add_css_class("heading");
    loc_heading.set_halign(gtk::Align::Start);
    loc_frame.append(&loc_heading);

    let loc_desc = gtk::Label::new(Some(&tr("gui.location_desc")));
    loc_desc.add_css_class("dim-label");
    loc_desc.add_css_class("caption");
    loc_desc.set_halign(gtk::Align::Start);
    loc_frame.append(&loc_desc);

    let loc_row = gtk::Box::new(Orientation::Horizontal, 8);

    let lat_label = gtk::Label::new(Some(&tr("gui.lat")));
    lat_label.set_width_chars(4);
    let lat_spin = gtk::SpinButton::with_range(-90.0, 90.0, 0.01);
    lat_spin.set_value(settings.latitude);
    lat_spin.set_digits(2);
    lat_spin.set_hexpand(true);

    let lon_label = gtk::Label::new(Some(&tr("gui.lon")));
    lon_label.set_width_chars(4);
    let lon_spin = gtk::SpinButton::with_range(-180.0, 180.0, 0.01);
    lon_spin.set_value(settings.longitude);
    lon_spin.set_digits(2);
    lon_spin.set_hexpand(true);

    loc_row.append(&lat_label);
    loc_row.append(&lat_spin);
    loc_row.append(&lon_label);
    loc_row.append(&lon_spin);

    // Auto-detect button
    let detect_btn = gtk::Button::with_label(&tr("actions.detect"));
    detect_btn.add_css_class("suggested-action");
    detect_btn.set_tooltip_text(Some(&tr("gui.detect_tooltip")));
    loc_row.append(&detect_btn);

    loc_frame.append(&loc_row);

    // Status label for detect feedback
    let loc_status = gtk::Label::new(None);
    loc_status.add_css_class("dim-label");
    loc_status.add_css_class("caption");
    loc_status.set_halign(gtk::Align::Start);
    loc_frame.append(&loc_status);

    // Today's sunrise/sunset display — updates when lat/lon changes
    let sun_label = gtk::Label::new(None);
    sun_label.add_css_class("dim-label");
    sun_label.add_css_class("caption");
    sun_label.set_halign(gtk::Align::Start);
    loc_frame.append(&sun_label);

    let update_sun_label = |lat: f64, lon: f64, label: &gtk::Label| {
        let _ctx = schedule::trigger::TriggerContext::new(lat, lon);
        let now = chrono::Local::now();
        let use_24h = Settings::load().unwrap_or_default().use_24h_time;
        match schedule::sunrise::calculate(now.date_naive(), lat, lon) {
            Some(solar) => {
                label.set_text(&tr_args("gui.today_sunrise", &[
                    ("sunrise", &format_time(solar.sunrise.hour(), solar.sunrise.minute(), use_24h)),
                    ("sunset", &format_time(solar.sunset.hour(), solar.sunset.minute(), use_24h)),
                ]));
            }
            None => {
                label.set_text(&tr("gui.sunrise_unavailable"));
            }
        }
    };
    update_sun_label(settings.latitude, settings.longitude, &sun_label);

    let loc_sep = gtk::Separator::new(Orientation::Horizontal);
    loc_frame.append(&loc_sep);

    content.append(&loc_frame);

    // Save lat/lon on change + update sunrise/sunset display
    let save_lat = lat_spin.clone();
    let save_lon = lon_spin.clone();
    let sun_lat = lat_spin.clone();
    let sun_lon = lon_spin.clone();
    let sun_ref = sun_label.clone();
    lat_spin.connect_value_changed(move |_| {
        save_lat_lon(save_lat.value(), save_lon.value());
        update_sun_label(sun_lat.value(), sun_lon.value(), &sun_ref);
    });
    let save_lat2 = lat_spin.clone();
    let save_lon2 = lon_spin.clone();
    let sun_lat2 = lat_spin.clone();
    let sun_lon2 = lon_spin.clone();
    let sun_ref2 = sun_label.clone();
    lon_spin.connect_value_changed(move |_| {
        save_lat_lon(save_lat2.value(), save_lon2.value());
        update_sun_label(sun_lat2.value(), sun_lon2.value(), &sun_ref2);
    });

    // Auto-detect handler
    let detect_lat = lat_spin.clone();
    let detect_lon = lon_spin.clone();
    let detect_status = loc_status.clone();
    let _detect_sun = sun_label.clone();
    detect_btn.connect_clicked(move |_| {
        detect_status.set_text(&tr("gui.detecting"));
        match detect_location() {
            Ok((lat, lon, city)) => {
                detect_lat.set_value(lat);
                detect_lon.set_value(lon);
                detect_status.set_text(&tr_args("gui.detected_city", &[("city", &city)]));
                // sun_label auto-updates via the spin button value_changed handler
            }
            Err(e) => {
                detect_status.set_text(&tr_args("gui.detection_failed", &[("error", &e)]));
            }
        }
    });

    // --- Profile list ---
    let list_box = gtk::Box::new(Orientation::Vertical, 8);
    list_box.set_margin_start(16);
    list_box.set_margin_end(16);
    list_box.set_margin_top(8);
    list_box.set_margin_bottom(12);

    let profiles = schedule::list_profiles().unwrap_or_default();

    // Empty state label — always present, visibility toggled dynamically
    let empty_label = gtk::Label::new(Some(&tr("schedules.empty")));
    empty_label.add_css_class("dim-label");
    empty_label.set_valign(gtk::Align::Center);
    empty_label.set_vexpand(true);

    // Track visible profile count for empty state toggling
    let visible_count = Rc::new(RefCell::new(profiles.len()));

    // Always append empty label, but hide it when profiles exist
    list_box.append(&empty_label);
    empty_label.set_visible(profiles.is_empty());

    // Shared display info from main window (passed to editor)
    let editor_info: Arc<Mutex<Vec<(String, String)>>> = Arc::new(Mutex::new(display_info));

    // Shared rebuild function — clears and repopulates the profile list
    // Uses RefCell<Option<>> to allow the rebuild closure to reference itself
    type RebuildFn = Rc<dyn Fn()>;
    let rebuild_cell: Rc<RefCell<Option<RebuildFn>>> = Rc::new(RefCell::new(None));

    // Build initial profile rows
    let on_apply = Rc::new(on_apply);
    for profile in &profiles {
        let rebuild_ref = rebuild_cell.clone();
        let row = build_profile_row(profile, &window, &empty_label, &visible_count, &editor_info, Rc::new(move || {
            if let Some(r) = rebuild_ref.borrow().as_ref() {
                r();
            }
        }), on_apply.clone());
        list_box.append(&row);
    }

    let rebuild: Rc<dyn Fn()> = {
        let list_box = list_box.clone();
        let empty_label = empty_label.clone();
        let visible_count = visible_count.clone();
        let editor_info = editor_info.clone();
        let window = window.clone();
        let rebuild_cell = rebuild_cell.clone();
        Rc::new(move || {
            // Remove all children except the empty label
            let mut child = list_box.first_child();
            while let Some(c) = child {
                let next = c.next_sibling();
                if c != empty_label {
                    list_box.remove(&c);
                }
                child = next;
            }
            let profiles = schedule::list_profiles().unwrap_or_default();
            empty_label.set_visible(profiles.is_empty());
            *visible_count.borrow_mut() = profiles.len();
            for profile in &profiles {
                let rebuild_ref = rebuild_cell.clone();
                let row = build_profile_row(profile, &window, &empty_label, &visible_count, &editor_info, Rc::new(move || {
                    if let Some(r) = rebuild_ref.borrow().as_ref() {
                        r();
                    }
                }), on_apply.clone());
                list_box.append(&row);
            }
        })
    };

    // Store the rebuild closure so rows can reference it
    *rebuild_cell.borrow_mut() = Some(rebuild.clone());

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

    // Add button opens editor
    let add_parent = window.clone();
    let add_info = editor_info.clone();
    let add_rebuild = rebuild.clone();
    add_btn.connect_clicked(move |_| {
        let info = add_info.lock().unwrap().clone();
        let new_profile = ScheduleProfile::new(String::new());
        let r = add_rebuild.clone();
        show_schedule_editor(add_parent.upcast_ref(), new_profile, &info, move || r());
    });

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

/// Save latitude and longitude to settings.
fn save_lat_lon(lat: f64, lon: f64) {
    let mut settings = Settings::load().unwrap_or_default();
    settings.latitude = lat;
    settings.longitude = lon;
    if let Err(e) = settings.save() {
        log::error!("Failed to save location: {}", e);
    }
}

/// Auto-detect location using IP geolocation (no API key needed).
fn detect_location() -> Result<(f64, f64, String), String> {
    let output = std::process::Command::new("curl")
        .args(["-s", "--max-time", "5", "https://ipapi.co/json/"])
        .output()
        .map_err(|e| format!("curl failed: {}", e))?;

    if !output.status.success() {
        return Err("curl request failed".into());
    }

    let json_str = String::from_utf8_lossy(&output.stdout);
    let json: serde_json::Value = serde_json::from_str(&json_str)
        .map_err(|e| format!("Parse error: {}", e))?;

    let lat = json["latitude"].as_f64().ok_or("Missing latitude")?;
    let lon = json["longitude"].as_f64().ok_or("Missing longitude")?;
    let city = json["city"].as_str().unwrap_or("Unknown").to_string();

    Ok((lat, lon, city))
}

/// Build a single profile row for the list.
fn build_profile_row(
    profile: &ScheduleProfile,
    parent_window: &adw::ApplicationWindow,
    empty_label: &gtk::Label,
    visible_count: &Rc<RefCell<usize>>,
    display_info: &Arc<Mutex<Vec<(String, String)>>>,
    on_save: Rc<dyn Fn()>,
    on_apply: Rc<dyn Fn(&str)>,
) -> gtk::Box {
    let container = gtk::Box::new(Orientation::Vertical, 4);

    // Title row: name + enabled switch + edit button + delete button
    let title_row = gtk::Box::new(Orientation::Horizontal, 8);

    let untitled = tr("gui.untitled");
    let name_label = gtk::Label::new(Some(if profile.name.is_empty() { &untitled } else { &profile.name }));
    name_label.set_halign(gtk::Align::Start);
    name_label.add_css_class("heading");
    name_label.set_hexpand(true);

    let enabled_switch = gtk::Switch::new();
    enabled_switch.set_active(profile.enabled);
    enabled_switch.set_valign(gtk::Align::Center);

    let profile_id = profile.id.clone();
    enabled_switch.connect_state_notify(move |sw| {
        if let Ok(Some(mut p)) = schedule::get_profile(&profile_id) {
            p.enabled = sw.is_active();
            if let Err(e) = schedule::save_profile(p) {
                log::error!("Failed to toggle schedule: {}", e);
            }
        }
    });

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

    let apply_btn = gtk::Button::from_icon_name("media-playback-start-symbolic");
    apply_btn.add_css_class("flat");
    apply_btn.set_tooltip_text(Some(&tr("schedules.apply_now")));

    let delete_btn = gtk::Button::from_icon_name("user-trash-symbolic");
    delete_btn.add_css_class("flat");
    delete_btn.set_tooltip_text(Some(&tr("schedules.delete")));
    delete_btn.add_css_class("destructive-action");

    title_row.append(&name_label);
    title_row.append(&enabled_switch);
    title_row.append(&apply_btn);
    title_row.append(&edit_btn);
    title_row.append(&delete_btn);

    // Trigger summary
    let use_24h = Settings::load().unwrap_or_default().use_24h_time;
    let trigger_label = gtk::Label::new(Some(&profile.trigger_summary_with_format(use_24h)));
    trigger_label.set_halign(gtk::Align::Start);
    trigger_label.add_css_class("dim-label");
    trigger_label.add_css_class("caption");

    // Display entries summary
    if !profile.entries.is_empty() {
        let settings = Settings::load().unwrap_or_default();
        let entries_text: Vec<String> = profile.entries.iter()
            .map(|e| {
                let name = settings.get_display(&e.display_key)
                    .custom_name
                    .unwrap_or_else(|| e.display_key.clone());
                format!("{}={:.0}%", name, e.brightness)
            })
            .collect();
        let entries_label = gtk::Label::new(Some(&entries_text.join(", ")));
        entries_label.set_halign(gtk::Align::Start);
        entries_label.add_css_class("dim-label");
        entries_label.add_css_class("caption");
        container.append(&entries_label);
    }

    container.append(&title_row);
    container.append(&trigger_label);

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

    // Edit button opens editor
    let edit_profile = profile.clone();
    let edit_parent = parent_window.clone();
    let edit_info = display_info.clone();
    let edit_rebuild = on_save.clone();
    edit_btn.connect_clicked(move |_| {
        let info = edit_info.lock().unwrap().clone();
        let r = edit_rebuild.clone();
        show_schedule_editor(edit_parent.upcast_ref(), edit_profile.clone(), &info, move || r());
    });

    // Apply button — applies profile brightness using the main window's DdcService
    let apply_id = profile.id.clone();
    let apply_name = profile.name.clone();
    let apply_cb = on_apply.clone();
    apply_btn.connect_clicked(move |_| {
        eprintln!("[schedule] manually applying '{}'", apply_name);
        apply_cb(&apply_id);
        eprintln!("[schedule] '{}' applied", apply_name);
    });
    // Delete button
    let delete_id = profile.id.clone();
    let delete_container = container.clone();
    let delete_empty = empty_label.clone();
    let delete_count = visible_count.clone();
    delete_btn.connect_clicked(move |_| {
        if let Ok(true) = schedule::delete_profile(&delete_id) {
            delete_container.set_visible(false);
            let mut count = delete_count.borrow_mut();
            *count = count.saturating_sub(1);
            if *count == 0 {
                delete_empty.set_visible(true);
            }
        }
    });

    container
}

/// Show the schedule editor for a single profile.
pub fn show_schedule_editor(
    parent: &gtk::Window,
    profile: ScheduleProfile,
    display_info: &[(String, String)],
    on_save: impl Fn() + 'static,
) {
    let window = adw::ApplicationWindow::builder()
        .title(if profile.name.is_empty() { tr("schedules.new_title") } else { tr("schedules.edit_title") })
        .transient_for(parent)
        .default_width(450)
        .default_height(600)
        .build();

    // Disable parent window while editor is open
    parent.set_sensitive(false);
    let parent_weak = parent.clone();
    window.connect_close_request(move |_| {
        parent_weak.set_sensitive(true);
        gtk::glib::Propagation::Proceed
    });

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

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

    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);

    // Name field
    let name_box = gtk::Box::new(Orientation::Horizontal, 8);
    let name_label = gtk::Label::new(Some(&tr("gui.name")));
    name_label.set_width_chars(8);
    let name_entry = gtk::Entry::new();
    name_entry.set_hexpand(true);
    name_entry.set_text(&profile.name);
    name_entry.set_placeholder_text(Some(&tr("gui.name_placeholder")));
    name_box.append(&name_label);
    name_box.append(&name_entry);
    outer.append(&name_box);

    // --- Monitor selection ---
    let mon_heading = gtk::Label::new(Some(&tr("gui.monitors")));
    mon_heading.add_css_class("heading");
    mon_heading.set_halign(gtk::Align::Start);
    outer.append(&mon_heading);

    let mon_select_row = gtk::Box::new(Orientation::Horizontal, 8);

    // Display info passed from main window — no subprocess needed
    let display_info: Vec<(String, String)> = display_info.to_vec();
    let dropdown_strings: Vec<String> = if display_info.is_empty() {
        vec![tr("gui.no_displays_detected")]
    } else {
        display_info.iter().map(|(_, name)| name.clone()).collect()
    };
    let dropdown_ref_strs: Vec<&str> = dropdown_strings.iter().map(|s| s.as_str()).collect();
    let dropdown = gtk::DropDown::from_strings(&dropdown_ref_strs);
    dropdown.set_hexpand(true);
    mon_select_row.append(&dropdown);

    let add_mon_btn = gtk::Button::with_label(&tr("actions.add"));
    add_mon_btn.add_css_class("suggested-action");
    mon_select_row.append(&add_mon_btn);

    let select_all_btn = gtk::Button::with_label(&tr("actions.select_all"));
    mon_select_row.append(&select_all_btn);
    outer.append(&mon_select_row);

    // Sync mode dropdown — same as main screen
    let saved_settings = Settings::load().unwrap_or_default();
    let sync_row = gtk::Box::new(Orientation::Horizontal, 8);
    let sync_label = gtk::Label::new(Some(&tr("gui.sync_schedule")));
    sync_label.set_width_chars(5);
    let sync_off = tr("sync.off");
    let sync_same = tr("sync.same_value");
    let sync_prop = tr("sync.proportional");
    let sync_opts = [sync_off.as_str(), sync_same.as_str(), sync_prop.as_str()];
    let sync_combo = gtk::DropDown::from_strings(&sync_opts);
    sync_combo.set_tooltip_text(Some(&tr("sync.schedule_tooltip")));
    let saved_mode = match saved_settings.sync_mode {
        SyncMode::Off => 0u32,
        SyncMode::SameValue => 1,
        SyncMode::Proportional => 2,
    };
    sync_combo.set_selected(saved_mode);
    sync_row.append(&sync_label);
    sync_row.append(&sync_combo);
    outer.append(&sync_row);

    // Sliders container
    let sliders_container = gtk::Box::new(Orientation::Vertical, 8);
    outer.append(&sliders_container);

    // Shared slider data: display_key → (adjustment, label, min, max)
    let slider_data: SliderData = Rc::new(RefCell::new(HashMap::new()));

    // Add existing entries from profile
    for entry in &profile.entries {
        add_slider_for_display(&sliders_container, &slider_data, &entry.display_key, entry.brightness, &sync_combo);
    }

    // Add monitor button handler
    let add_info = display_info.clone();
    let add_sliders = sliders_container.clone();
    let add_data = slider_data.clone();
    let add_dropdown = dropdown.clone();
    let add_sync = sync_combo.clone();
    add_mon_btn.connect_clicked(move |_| {
        if add_info.is_empty() { return; }
        let idx = add_dropdown.selected() as usize;
        if idx >= add_info.len() { return; }
        let (key, _name) = &add_info[idx];
        if add_data.borrow().contains_key(key) { return; }
        add_slider_for_display(&add_sliders, &add_data, key, 50.0, &add_sync);
    });

    // Select all button handler
    let all_info = display_info.clone();
    let all_sliders = sliders_container.clone();
    let all_data = slider_data.clone();
    let all_sync = sync_combo.clone();
    select_all_btn.connect_clicked(move |_| {
        for (key, _) in &all_info {
            if !all_data.borrow().contains_key(key) {
                add_slider_for_display(&all_sliders, &all_data, key, 50.0, &all_sync);
            }
        }
    });

    // --- Triggers ---
    let sep1 = gtk::Separator::new(Orientation::Horizontal);
    outer.append(&sep1);

    let trigger_heading = gtk::Label::new(Some(&tr("gui.triggers")));
    trigger_heading.add_css_class("heading");
    trigger_heading.set_halign(gtk::Align::Start);
    outer.append(&trigger_heading);

    // Trigger list container
    let trigger_container = gtk::Box::new(Orientation::Vertical, 8);
    outer.append(&trigger_container);

    // Shared triggers state
    let triggers_cell: Rc<RefCell<Vec<ScheduleTrigger>>> =
        Rc::new(RefCell::new(profile.triggers.clone()));

    rebuild_trigger_list(&trigger_container, &triggers_cell);

    // Add trigger controls
    let add_trigger_row = gtk::Box::new(Orientation::Horizontal, 6);

    let trigger_type_combo = gtk::DropDown::from_strings(&[
        tr("triggers.time").as_str(),
        tr("triggers.sunrise").as_str(),
        tr("triggers.sunset").as_str(),
    ]);
    add_trigger_row.append(&trigger_type_combo);

    let time_entry = gtk::Entry::new();
    time_entry.set_placeholder_text(Some("HH:MM"));
    time_entry.set_width_chars(6);
    add_trigger_row.append(&time_entry);

    // AM/PM/24h format selector — defaults to user preference
    let ampm_combo = gtk::DropDown::from_strings(&["24h", "AM", "PM"]);
    ampm_combo.set_tooltip_text(Some(&tr("gui.time_format")));
    let prefs = Settings::load().unwrap_or_default();
    ampm_combo.set_selected(if prefs.use_24h_time { 0 } else { 1 }); // 0=24h, 1=AM
    add_trigger_row.append(&ampm_combo);

    let offset_spin = gtk::SpinButton::with_range(-300.0, 300.0, 5.0);
    offset_spin.set_value(0.0);
    offset_spin.set_tooltip_text(Some(&tr("triggers.offset_tooltip")));
    add_trigger_row.append(&offset_spin);

    let add_trigger_btn = gtk::Button::with_label("+");
    add_trigger_btn.add_css_class("suggested-action");
    let add_trigger_btn_for_handler = add_trigger_btn.clone();
    add_trigger_row.append(&add_trigger_btn);
    outer.append(&add_trigger_row);

    // Resolved sunrise/sunset time preview
    let sun_hint = gtk::Label::new(None);
    sun_hint.add_css_class("dim-label");
    sun_hint.add_css_class("caption");
    sun_hint.set_halign(gtk::Align::Start);
    sun_hint.set_visible(false);
    outer.append(&sun_hint);

    let update_sun_hint = |combo_sel: u32, offset: i32, label: &gtk::Label| {
        let settings = Settings::load().unwrap_or_default();
        let ctx = schedule::trigger::TriggerContext::new(settings.latitude, settings.longitude);
        let now = chrono::Local::now();
        let use_24h = settings.use_24h_time;
        match combo_sel {
            1 => {
                let trigger = ScheduleTrigger::Sunrise { offset_minutes: offset };
                match schedule::trigger::resolve_trigger_time(&trigger, &ctx, now.date_naive()) {
                    Some(t) => {
                        let desc = trigger.description();
                        label.set_text(&tr_args("gui.sunrise_resolves", &[
                            ("desc", &desc),
                            ("time", &format_time(t.hour(), t.minute(), use_24h)),
                        ]));
                        label.set_visible(true);
                    }
                    None => {
                        label.set_text(&tr("gui.sunrise_cannot_resolve"));
                        label.set_visible(true);
                    }
                }
            }
            2 => {
                let trigger = ScheduleTrigger::Sunset { offset_minutes: offset };
                match schedule::trigger::resolve_trigger_time(&trigger, &ctx, now.date_naive()) {
                    Some(t) => {
                        let desc = trigger.description();
                        label.set_text(&tr_args("gui.sunrise_resolves", &[
                            ("desc", &desc),
                            ("time", &format_time(t.hour(), t.minute(), use_24h)),
                        ]));
                        label.set_visible(true);
                    }
                    None => {
                        label.set_text(&tr("gui.sunset_cannot_resolve"));
                        label.set_visible(true);
                    }
                }
            }
            _ => { label.set_visible(false); }
        }
    };

    // Toggle visibility + update sun hint based on trigger type
    let time_for_toggle = time_entry.clone();
    let ampm_for_toggle = ampm_combo.clone();
    let offset_for_toggle = offset_spin.clone();
    let hint_for_type = sun_hint.clone();
    let offset_for_type = offset_spin.clone();
    trigger_type_combo.connect_selected_notify(move |combo| {
        let sel = combo.selected();
        match sel {
            0 => {
                time_for_toggle.set_visible(true);
                ampm_for_toggle.set_visible(true);
                offset_for_toggle.set_visible(false);
            }
            1 | 2 => {
                time_for_toggle.set_visible(false);
                ampm_for_toggle.set_visible(false);
                offset_for_toggle.set_visible(true);
            }
            _ => {}
        }
        update_sun_hint(sel, offset_for_type.value_as_int(), &hint_for_type);
    });

    // Update sun hint when offset changes
    let hint_for_offset = sun_hint.clone();
    let combo_for_offset = trigger_type_combo.clone();
    offset_spin.connect_value_changed(move |spin| {
        update_sun_hint(combo_for_offset.selected(), spin.value_as_int(), &hint_for_offset);
    });

    offset_spin.set_visible(false);

    // Sanitize time entry — only allow digits and colon, clear error on edit
    let sanitize_time_entry = time_entry.clone();
    time_entry.connect_changed(move |entry| {
        sanitize_time_entry.remove_css_class("error");
        sanitize_time_entry.set_tooltip_text(Some("HH:MM"));
        let text = entry.text().to_string();
        let sanitized: String = text.chars()
            .filter(|c| c.is_ascii_digit() || *c == ':')
            .take(5) // max HH:MM
            .collect();
        if sanitized != text {
            sanitize_time_entry.set_text(&sanitized);
            sanitize_time_entry.set_position(-1); // cursor to end
        }
    });

    // Add trigger button
    let add_triggers_cell = triggers_cell.clone();
    let add_trigger_container = trigger_container.clone();
    let add_time_entry = time_entry.clone();
    let add_ampm_combo = ampm_combo.clone();
    let add_offset_spin = offset_spin.clone();
    let add_type_combo = trigger_type_combo.clone();
    add_trigger_btn_for_handler.connect_clicked(move |_| {
        let trigger = match add_type_combo.selected() {
            0 => {
                let input = add_time_entry.text().to_string();
                let time_24h = match parse_time_input(&input, add_ampm_combo.selected()) {
                    Ok(t) => t,
                    Err(msg) => {
                        add_time_entry.add_css_class("error");
                        add_time_entry.set_tooltip_text(Some(&msg));
                        // Auto-clear error style after 2 seconds
                        let clear_entry = add_time_entry.clone();
                        gtk::glib::timeout_add_local(std::time::Duration::from_secs(2), move || {
                            clear_entry.remove_css_class("error");
                            gtk::glib::ControlFlow::Continue
                        });
                        return;
                    }
                };
                ScheduleTrigger::Absolute { time: time_24h }
            }
            1 => {
                ScheduleTrigger::Sunrise { offset_minutes: add_offset_spin.value_as_int() }
            }
            2 => {
                ScheduleTrigger::Sunset { offset_minutes: add_offset_spin.value_as_int() }
            }
            _ => return,
        };
        add_triggers_cell.borrow_mut().push(trigger);
        rebuild_trigger_list(&add_trigger_container, &add_triggers_cell);
    });

    // --- Save / Cancel ---
    let sep2 = gtk::Separator::new(Orientation::Horizontal);
    outer.append(&sep2);

    let btn_row = gtk::Box::new(Orientation::Horizontal, 8);
    btn_row.set_halign(gtk::Align::End);

    let cancel_btn = gtk::Button::with_label(&tr("actions.cancel"));
    let save_btn = gtk::Button::with_label(&tr("actions.save"));
    save_btn.add_css_class("suggested-action");

    btn_row.append(&cancel_btn);
    btn_row.append(&save_btn);
    outer.append(&btn_row);

    // Cancel
    let cancel_window = window.clone();
    cancel_btn.connect_clicked(move |_| {
        cancel_window.close();
    });

    // Save
    let save_window = window.clone();
    let save_name = name_entry.clone();
    let save_data = slider_data.clone();
    let save_triggers = triggers_cell.clone();
    let save_profile = Rc::new(RefCell::new(profile.clone()));
    let save_callback: Box<dyn Fn()> = Box::new(on_save);
    let save_callback = Rc::new(save_callback);
    save_btn.connect_clicked(move |_| {
        let mut p = save_profile.borrow_mut();
        p.name = save_name.text().to_string();
        p.entries.clear();
        for (key, (adj, _, _, _)) in save_data.borrow().iter() {
            p.set_entry(key.clone(), adj.value());
        }
        p.triggers = save_triggers.borrow().clone();

        if let Err(e) = schedule::save_profile(p.clone()) {
            log::error!("Failed to save profile: {}", e);
        }

        // Regenerate systemd timers so changes take effect immediately
        let settings = crate::settings::Settings::load().unwrap_or_default();
        let ctx = schedule::trigger::TriggerContext::new(settings.latitude, settings.longitude);
        match schedule::systemd::generate_timers(&ctx) {
            Ok(timers) if !timers.is_empty() => {
                let _ = std::process::Command::new("systemctl")
                    .args(["--user", "daemon-reload"])
                    .output();
                // Enable each generated timer
                for timer_info in &timers {
                    let timer_name = timer_info.split_once(' ').map(|(n, _)| n).unwrap_or(timer_info);
                    let _ = std::process::Command::new("systemctl")
                        .args(["--user", "enable", "--now", timer_name])
                        .output();
                }
            }
            Ok(_) => {} // No timers generated
            Err(e) => log::warn!("Failed to regenerate timers: {}", e),
        }
        save_callback();
        save_window.close();
    });

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

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

/// Add a brightness slider for a display to the container.
fn add_slider_for_display(
    container: &gtk::Box,
    data: &SliderData,
    display_key: &str,
    initial_value: f64,
    sync_combo: &gtk::DropDown,
) {
    // Resolve display name and brightness range from settings
    let settings = Settings::load().unwrap_or_default();
    let display_settings = settings.get_display(display_key);
    let display_name = display_settings
        .custom_name
        .unwrap_or_else(|| display_key.to_string());
    let min_val = display_settings.min_brightness;
    let max_val = display_settings.max_brightness;
    let clamped_value = initial_value.clamp(min_val, max_val);

    let (row, adjustment) = build_brightness_slider(&display_name, clamped_value, min_val, max_val);

    // Add remove button to the label row (first child of container)
    let remove_btn = gtk::Button::from_icon_name("window-close-symbolic");
    remove_btn.add_css_class("flat");
    if let Some(label_row) = row.first_child() {
        if let Some(label_box) = label_row.downcast_ref::<gtk::Box>() {
            label_box.append(&remove_btn);
        }
    }

    // Store in data with min/max for sync calculations
    data.borrow_mut().insert(
        display_key.to_string(),
        (adjustment.clone(), gtk::Label::new(None), min_val, max_val),
    );

    // Sync: when this slider changes, update all other sliders based on sync mode
    let _sync_adj = adjustment.clone();
    let sync_data = data.clone();
    let sync_combo_ref = sync_combo.clone();
    let sync_source_key = display_key.to_string();
    adjustment.connect_value_changed(move |adj| {
        let mode = sync_combo_ref.selected();
        if mode == 0 { return; } // Off — no sync

        let data_ref = sync_data.borrow();
        for (key, (other_adj, _, other_min, other_max)) in data_ref.iter() {
            if key == &sync_source_key { continue; }
            let new_val = match mode {
                1 => {
                    // Same Value: set to same percentage
                    adj.value().clamp(*other_min, *other_max)
                }
                2 => {
                    // Proportional: scale relative to min/max range
                    let src_range = adj.upper() - adj.lower();
                    let src_pct = if src_range > 0.0 {
                        (adj.value() - adj.lower()) / src_range
                    } else {
                        0.5
                    };
                    let dest_range = other_max - other_min;
                    other_min + src_pct * dest_range
                }
                _ => continue,
            };
            other_adj.set_value(new_val);
        }
    });

    // Remove button — hide row and remove from data
    let remove_key = display_key.to_string();
    let remove_data = data.clone();
    let remove_row = row.clone();
    remove_btn.connect_clicked(move |_| {
        remove_data.borrow_mut().remove(&remove_key);
        remove_row.unparent();
    });

    container.append(&row);
}

/// Rebuild the trigger list UI from the shared triggers state.
fn rebuild_trigger_list(
    container: &gtk::Box,
    triggers: &Rc<RefCell<Vec<ScheduleTrigger>>>,
) {
    // Remove existing children
    let mut child = container.first_child();
    while let Some(c) = child {
        let next = c.next_sibling();
        c.unparent();
        child = next;
    }

    let triggers_ref = triggers.borrow();
    for (i, trigger) in triggers_ref.iter().enumerate() {
        let row = gtk::Box::new(Orientation::Horizontal, 8);

        let use_24h = Settings::load().unwrap_or_default().use_24h_time;
        let desc = gtk::Label::new(Some(&trigger.description_with_format(use_24h)));
        desc.set_halign(gtk::Align::Start);
        desc.set_hexpand(true);

        let remove_btn = gtk::Button::from_icon_name("window-close-symbolic");
        remove_btn.add_css_class("flat");

        let remove_idx = i;
        let remove_container = container.clone();
        let remove_triggers = triggers.clone();
        remove_btn.connect_clicked(move |_| {
            remove_triggers.borrow_mut().remove(remove_idx);
            rebuild_trigger_list(&remove_container, &remove_triggers);
        });

        row.append(&desc);
        row.append(&remove_btn);
        container.append(&row);
    }

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

/// Parse time input with AM/PM/24h mode.
/// `ampm_selected`: 0 = 24h, 1 = AM, 2 = PM
/// Returns 24h "HH:MM" string on success.
fn parse_time_input(input: &str, ampm_selected: u32) -> Result<String, String> {
    use crate::i18n::{tr, tr_args};
    let input = input.trim();
    let parts: Vec<&str> = input.split(':').collect();
    if parts.len() != 2 {
        return Err(tr("gui.error_expected_time"));
    }

    let mut h: u32 = parts[0].parse::<u32>().map_err(|_| tr("gui.error_invalid_hour"))?;
    let m: u32 = parts[1].parse::<u32>().map_err(|_| tr("gui.error_invalid_minute"))?;

    match ampm_selected {
        0 => {
            // 24h mode: validate 0-23
            if h > 23 {
                return Err(tr_args("gui.error_hour_range_24h", &[("val", &h.to_string())]));
            }
        }
        1 => {
            // AM mode: 1-12 → convert to 0-11
            if !(1..=12).contains(&h) {
                return Err(tr_args("gui.error_hour_range_am", &[("val", &h.to_string())]));
            }
            if h == 12 { h = 0; } // 12 AM = 0:00
        }
        2 => {
            // PM mode: 1-12 → convert to 12-23
            if !(1..=12).contains(&h) {
                return Err(tr_args("gui.error_hour_range_pm", &[("val", &h.to_string())]));
            }
            if h != 12 { h += 12; } // 12 PM = 12:00, others +12
        }
        _ => return Err(tr("gui.error_invalid_ampm")),
    }

    if m > 59 {
        return Err(tr_args("gui.error_minute_range", &[("val", &m.to_string())]));
    }

    Ok(format!("{:02}:{:02}", h, m))
}

/// Get display keys with human-readable names from settings.
/// Returns Vec<(backend_key, display_name)>.
#[allow(dead_code)]
fn get_detected_display_keys() -> Vec<(String, String)> {
    // First, try to get keys from running temperlux list
    let keys: Vec<String> = match std::process::Command::new("temperlux")
        .args(["list"])
        .output()
    {
        Ok(output) if output.status.success() => {
            let stdout = String::from_utf8_lossy(&output.stdout);
            let mut keys = Vec::new();
            for line in stdout.lines() {
                let line = line.trim();
                if line.starts_with(char::is_numeric) {
                    if let Some(num_str) = line.split_whitespace().next() {
                        if let Ok(num) = num_str.parse::<u32>() {
                            if line.contains("DDC/CI") {
                                keys.push(format!("ddc:{}", num));
                            } else {
                                keys.push(format!("bl:{}", num));
                            }
                        }
                    }
                }
            }
            keys
        }
        _ => {
            log::warn!("Could not detect displays for schedule editor");
            Vec::new()
        }
    };

    // Resolve display names from settings
    let settings = Settings::load().unwrap_or_default();
    keys.into_iter()
        .map(|key| {
            let name = settings
                .get_display(&key)
                .custom_name
                .unwrap_or_else(|| key.clone());
            (key, name)
        })
        .collect()
}
