pub fn gpio_power_label(power: &CapturePowerStatus) -> String {
    if !power.available {
        return "Unavailable".to_string();
    }
    if !power.enabled {
        return "Power Off".to_string();
    }
    match power.detected_devices {
        0 => "No Eyes".to_string(),
        1 => "1 Eye".to_string(),
        count => format!("{count} Eyes"),
    }
}

pub fn capture_power_detail(power: &CapturePowerStatus) -> String {
    if !power.available {
        return format!("{} is unavailable: {}", power.unit, power.detail);
    }
    let detected = if power.enabled {
        format!(" • {}", gpio_detection_detail(power.detected_devices))
    } else {
        String::new()
    };
    match power.mode.as_str() {
        "forced-on" => format!(
            "{} • awake • {}{} • leases {}",
            power.unit, power.detail, detected, power.active_leases
        ),
        "forced-off" => format!(
            "{} • dark • {}{} • leases {}",
            power.unit, power.detail, detected, power.active_leases
        ),
        _ => format!(
            "{} • auto • {}{} • leases {}",
            power.unit, power.detail, detected, power.active_leases
        ),
    }
}

#[derive(Clone, Copy, Debug, PartialEq, Eq)]
enum StatusLightState {
    Idle,
    Live,
    Connected,
    Warning,
    Caution,
}

impl StatusLightState {
    fn from_active(active: bool) -> Self {
        if active { Self::Live } else { Self::Idle }
    }

    fn css_class(self) -> &'static str {
        match self {
            Self::Idle => "status-light-idle",
            Self::Live => "status-light-live",
            Self::Connected => "status-light-connected",
            Self::Warning => "status-light-warning",
            Self::Caution => "status-light-caution",
        }
    }
}

fn server_light_state(state: &LauncherState, relay_live: bool) -> StatusLightState {
    if !state.server_available || !server_version_known(state) {
        StatusLightState::Idle
    } else if server_versions_match(state) {
        if relay_live {
            StatusLightState::Connected
        } else {
            StatusLightState::Live
        }
    } else if relay_live {
        StatusLightState::Caution
    } else {
        StatusLightState::Warning
    }
}

/// Confirms the server reported a usable version before claiming compatibility.
fn server_version_known(state: &LauncherState) -> bool {
    state
        .server_version
        .as_deref()
        .map(normalize_version)
        .is_some_and(|version| !version.is_empty())
}

/// Compares the reachable server version against this client build.
fn server_versions_match(state: &LauncherState) -> bool {
    state
        .server_version
        .as_deref()
        .map(normalize_version)
        .is_some_and(|server_version| server_version == normalize_version(crate::VERSION))
}

/// Compares versions independent of whether the source included a leading `v`.
fn normalize_version(version: &str) -> &str {
    version.trim().trim_start_matches('v')
}

fn server_version_label(state: &LauncherState) -> String {
    if !state.server_available {
        return "-".to_string();
    }
    let version = state
        .server_version
        .as_deref()
        .map(str::trim)
        .filter(|version| !version.is_empty());
    match version {
        Some(version) if version.starts_with('v') => version.to_string(),
        Some(version) => format!("v{version}"),
        None => "-".to_string(),
    }
}

fn gpio_light_state(power: &CapturePowerStatus) -> StatusLightState {
    if !power.available || !power.enabled {
        return StatusLightState::Idle;
    }
    match power.detected_devices {
        0 => StatusLightState::Warning,
        1 => StatusLightState::Caution,
        _ => StatusLightState::Live,
    }
}

fn gpio_detection_detail(detected_devices: u32) -> String {
    match detected_devices {
        0 => "no eyes detected".to_string(),
        1 => "1 eye detected".to_string(),
        count => format!("{count} eyes detected"),
    }
}

/// Highlights the currently active capture mode so it reads like a segmented control.
fn sync_power_mode_button_styles(widgets: &LauncherWidgets, mode: &str) {
    for button in [
        &widgets.power_auto_button,
        &widgets.power_on_button,
        &widgets.power_off_button,
    ] {
        button.remove_css_class("pill-toggle-active");
    }
    match mode {
        "forced-on" => widgets.power_on_button.add_css_class("pill-toggle-active"),
        "forced-off" => widgets.power_off_button.add_css_class("pill-toggle-active"),
        _ => widgets
            .power_auto_button
            .add_css_class("pill-toggle-active"),
    }
}

/// Reports which local staging checks are active right now.
fn local_test_detail(
    camera_running: bool,
    microphone_running: bool,
    speaker_running: bool,
    microphone_replay_running: bool,
) -> String {
    let mut active = Vec::new();
    if camera_running {
        active.push("camera preview");
    }
    if microphone_running {
        active.push("mic monitor");
    }
    if speaker_running {
        active.push("speaker tone");
    }
    if microphone_replay_running {
        active.push("mic replay");
    }

    if active.is_empty() {
        "Local checks are idle. Use Start Preview, Monitor Mic, Replay, or Play Tone before you launch."
            .to_string()
    } else {
        format!(
            "Local checks running: {}. Stop them whenever staging is complete.",
            active.join(", ")
        )
    }
}

fn install_popout_drag(window: &gtk::ApplicationWindow, widget: &impl IsA<gtk::Widget>) {
    let drag = gtk::GestureClick::new();
    drag.set_button(0);
    let native = window.clone();
    drag.connect_pressed(move |gesture, _press, x, y| {
        let Some(device) = gesture.current_event_device() else {
            return;
        };
        let Some(surface) = native.surface() else {
            return;
        };
        let Some(toplevel) = surface.dynamic_cast_ref::<gdk::Toplevel>() else {
            return;
        };
        let timestamp = gesture
            .current_event()
            .map(|event| event.time())
            .unwrap_or(0);
        toplevel.begin_move(&device, 1, x, y, timestamp);
    });
    widget.add_controller(drag);
}

fn apply_popout_window_geometry(
    window: &gtk::ApplicationWindow,
    root: &gtk::Box,
    picture: &gtk::Picture,
    size: BreakoutSizeChoice,
    display_limit: super::state::PreviewSourceSize,
) {
    picture.set_size_request(size.width, size.height);
    root.set_size_request(size.width, size.height);
    window.set_default_size(size.width, size.height);
    if should_cover_display(size, display_limit) {
        fullscreen_on_largest_monitor(window);
    } else {
        window.unfullscreen();
    }
}

fn schedule_popout_window_geometry(
    window: gtk::ApplicationWindow,
    root: gtk::Box,
    picture: gtk::Picture,
    size: BreakoutSizeChoice,
    display_limit: super::state::PreviewSourceSize,
) {
    for delay_ms in [0_u64, 25, 150] {
        let window = window.clone();
        let root = root.clone();
        let picture = picture.clone();
        glib::timeout_add_local_once(std::time::Duration::from_millis(delay_ms), move || {
            apply_popout_window_geometry(&window, &root, &picture, size, display_limit);
            window.present();
        });
    }
}

fn fullscreen_on_largest_monitor(window: &gtk::ApplicationWindow) {
    let Some(display) = gdk::Display::default() else {
        window.fullscreen();
        return;
    };
    let monitors = display.monitors();
    let monitor = (0..monitors.n_items())
        .filter_map(|idx| monitors.item(idx))
        .filter_map(|obj| obj.downcast::<gdk::Monitor>().ok())
        .max_by_key(|monitor| {
            let geometry = monitor.geometry();
            let scale = monitor.scale_factor().max(1);
            geometry.width().max(1) as i64
                * scale as i64
                * geometry.height().max(1) as i64
                * scale as i64
        });
    if let Some(monitor) = monitor.as_ref() {
        window.fullscreen_on_monitor(monitor);
    } else {
        window.fullscreen();
    }
}

fn should_cover_display(
    size: BreakoutSizeChoice,
    display_limit: super::state::PreviewSourceSize,
) -> bool {
    matches!(size.preset, super::state::BreakoutSizePreset::FillDisplay)
        || (size.width >= display_limit.width.max(1) as i32
            && size.height >= display_limit.height.max(1) as i32)
}

pub fn present_popout_windows(popouts: &Rc<RefCell<[Option<PopoutWindowHandle>; 2]>>) {
    for handle in popouts.borrow().iter().flatten() {
        handle.window.present();
    }
}
