#[cfg(test)]
/// Prefer the basename for `/dev/...` entries while keeping Pulse names intact.
fn compact_device_name(value: &str) -> String {
    let trimmed = value.trim();
    if trimmed.is_empty() {
        return "auto".to_string();
    }
    trimmed.rsplit('/').next().unwrap_or(trimmed).to_string()
}

/// Keeps `capitalize` explicit because it sits on launcher state/UI wiring, where device choices should remain explainable across refreshes.
/// Inputs are the typed parameters; output is the return value or side effect.
pub fn capitalize(value: &str) -> String {
    let mut chars = value.chars();
    match chars.next() {
        Some(first) => format!("{}{}", first.to_ascii_uppercase(), chars.as_str()),
        None => String::new(),
    }
}

/// Keeps `selected_combo_value` explicit because it sits on launcher state/UI wiring, where device choices should remain explainable across refreshes.
/// Inputs are the typed parameters; output is the return value or side effect.
pub fn selected_combo_value(combo: &gtk::ComboBoxText) -> Option<String> {
    combo
        .active_id()
        .map(|value| value.to_string())
        .or_else(|| combo.active_text().map(|value| value.to_string()))
        .and_then(|value| {
            let value = value.to_string();
            let trimmed = value.trim();
            if trimmed.is_empty()
                || trimmed.eq_ignore_ascii_case("auto")
                || trimmed.eq_ignore_ascii_case("all")
            {
                None
            } else {
                Some(trimmed.to_string())
            }
        })
}

/// Keeps `selected_server_addr` explicit because it sits on launcher state/UI wiring, where device choices should remain explainable across refreshes.
/// Inputs are the typed parameters; output is the return value or side effect.
pub fn selected_server_addr(entry: &gtk::Entry, fallback: &str) -> String {
    let current = entry.text();
    let trimmed = current.trim();
    if trimmed.is_empty() {
        normalize_server_addr(fallback)
    } else {
        let normalized = normalize_server_addr(trimmed);
        if normalized != trimmed {
            entry.set_text(&normalized);
        }
        normalized
    }
}

/// Keeps `normalize_server_addr` explicit because it sits on launcher state/UI wiring, where device choices should remain explainable across refreshes.
/// Inputs are the typed parameters; output is the return value or side effect.
pub fn normalize_server_addr(raw: &str) -> String {
    let trimmed = raw.trim();
    if trimmed.is_empty() || trimmed.contains("://") {
        trimmed.to_string()
    } else {
        format!("https://{trimmed}")
    }
}

pub fn input_control_path() -> PathBuf {
    std::env::var(INPUT_CONTROL_ENV)
        .map(PathBuf::from)
        .unwrap_or_else(|_| PathBuf::from(DEFAULT_INPUT_CONTROL_PATH))
}

pub fn input_state_path() -> PathBuf {
    std::env::var(INPUT_STATE_ENV)
        .map(PathBuf::from)
        .unwrap_or_else(|_| PathBuf::from(DEFAULT_INPUT_STATE_PATH))
}

pub fn input_toggle_control_path() -> PathBuf {
    std::env::var(TOGGLE_KEY_CONTROL_ENV)
        .map(PathBuf::from)
        .unwrap_or_else(|_| PathBuf::from(DEFAULT_TOGGLE_KEY_CONTROL_PATH))
}

pub fn wake_control_path() -> PathBuf {
    std::env::var(WAKE_CONTROL_ENV)
        .map(PathBuf::from)
        .unwrap_or_else(|_| PathBuf::from(DEFAULT_WAKE_CONTROL_PATH))
}

pub fn audio_gain_control_path() -> PathBuf {
    std::env::var(AUDIO_GAIN_CONTROL_ENV)
        .map(PathBuf::from)
        .unwrap_or_else(|_| PathBuf::from(DEFAULT_AUDIO_GAIN_CONTROL_PATH))
}

pub fn mic_gain_control_path() -> PathBuf {
    std::env::var(MIC_GAIN_CONTROL_ENV)
        .map(PathBuf::from)
        .unwrap_or_else(|_| PathBuf::from(DEFAULT_MIC_GAIN_CONTROL_PATH))
}

pub fn media_control_path() -> PathBuf {
    std::env::var(MEDIA_CONTROL_ENV)
        .map(PathBuf::from)
        .unwrap_or_else(|_| PathBuf::from(DEFAULT_MEDIA_CONTROL_PATH))
}

pub fn uplink_camera_preview_path() -> PathBuf {
    std::env::var(UPLINK_CAMERA_PREVIEW_ENV)
        .map(PathBuf::from)
        .unwrap_or_else(|_| PathBuf::from(DEFAULT_UPLINK_CAMERA_PREVIEW_PATH))
}

pub fn uplink_mic_level_path() -> PathBuf {
    std::env::var(UPLINK_MIC_LEVEL_ENV)
        .map(PathBuf::from)
        .unwrap_or_else(|_| PathBuf::from(DEFAULT_UPLINK_MIC_LEVEL_PATH))
}

pub fn uplink_telemetry_path() -> PathBuf {
    std::env::var(UPLINK_TELEMETRY_ENV)
        .map(PathBuf::from)
        .unwrap_or_else(|_| PathBuf::from(DEFAULT_UPLINK_TELEMETRY_PATH))
}

pub fn write_input_routing_request(path: &Path, routing: InputRouting) -> Result<()> {
    std::fs::write(
        path,
        format!("{} {}\n", routing_name(routing), control_request_nonce()),
    )?;
    Ok(())
}

pub fn write_audio_gain_request(path: &Path, gain_percent: u32) -> Result<()> {
    let gain = gain_percent.min(super::state::MAX_AUDIO_GAIN_PERCENT) as f64 / 100.0;
    std::fs::write(path, format!("{gain:.3} {}\n", control_request_nonce()))?;
    Ok(())
}

pub fn write_mic_gain_request(path: &Path, gain_percent: u32) -> Result<()> {
    let gain = gain_percent.min(super::state::MAX_MIC_GAIN_PERCENT) as f64 / 100.0;
    std::fs::write(path, format!("{gain:.3} {}\n", control_request_nonce()))?;
    Ok(())
}

/// Keeps `write_media_control_request` explicit because it sits on launcher state/UI wiring, where device choices should remain explainable across refreshes.
/// Inputs are the typed parameters; output is the return value or side effect.
pub fn write_media_control_request(path: &Path, state: &LauncherState) -> Result<()> {
    crate::live_media_control::write_media_control_request(
        path,
        crate::live_media_control::MediaControlState::with_devices_and_codecs(
            state.channels.camera,
            state.channels.microphone,
            state.channels.audio,
            state.devices.camera.clone(),
            state.camera_quality.map(|mode| mode.id()),
            state.devices.microphone.clone(),
            state.devices.speaker.clone(),
            Some(state.effective_webcam_transport().as_id().to_string()),
            state.upstream_audio_transport.as_common_codec(),
            state.mic_noise_suppression,
        ),
    )?;
    Ok(())
}

pub fn write_input_toggle_key_request(path: &Path, swap_key: &str) -> Result<()> {
    std::fs::write(
        path,
        format!("{} {}\n", swap_key.trim(), control_request_nonce()),
    )?;
    Ok(())
}

pub fn write_wake_control_request(path: &Path, minutes: Option<u64>) -> Result<()> {
    let value = minutes
        .map(|minutes| minutes.to_string())
        .unwrap_or_else(|| "off".to_string());
    std::fs::write(path, format!("{value} {}\n", control_request_nonce()))?;
    Ok(())
}

/// Keeps `read_input_routing_state` explicit because it sits on launcher state/UI wiring, where device choices should remain explainable across refreshes.
/// Inputs are the typed parameters; output is the return value or side effect.
pub fn read_input_routing_state(path: &Path) -> Option<InputRouting> {
    let raw = std::fs::read_to_string(path).ok()?;
    match raw
        .split_ascii_whitespace()
        .next()?
        .to_ascii_lowercase()
        .as_str()
    {
        "local" => Some(InputRouting::Local),
        "remote" => Some(InputRouting::Remote),
        _ => None,
    }
}

fn control_request_nonce() -> u128 {
    SystemTime::now()
        .duration_since(UNIX_EPOCH)
        .map(|duration| duration.as_nanos())
        .unwrap_or_default()
}

/// Keeps `routing_name` explicit because it sits on launcher state/UI wiring, where device choices should remain explainable across refreshes.
/// Inputs are the typed parameters; output is the return value or side effect.
pub fn routing_name(routing: InputRouting) -> &'static str {
    match routing {
        InputRouting::Local => "local",
        InputRouting::Remote => "remote",
    }
}

pub fn path_marker(path: &Path) -> u128 {
    std::fs::metadata(path)
        .ok()
        .and_then(|meta| meta.modified().ok())
        .and_then(|stamp| stamp.duration_since(std::time::UNIX_EPOCH).ok())
        .map(|duration| duration.as_millis())
        .unwrap_or_default()
}

/// Keeps `set_combo_active_text` explicit because it sits on launcher state/UI wiring, where device choices should remain explainable across refreshes.
/// Inputs are the typed parameters; output is the return value or side effect.
pub fn set_combo_active_text(combo: &gtk::ComboBoxText, wanted: Option<&str>) {
    let wanted = wanted.unwrap_or("auto");
    if combo.set_active_id(Some(wanted)) {
        return;
    }
    if combo.set_active_id(Some("auto")) {
        return;
    }
    let _ = combo.set_active_id(Some("all"));
}

/// Keeps `toggle_key_label` explicit because it sits on launcher state/UI wiring, where device choices should remain explainable across refreshes.
/// Inputs are the typed parameters; output is the return value or side effect.
pub fn toggle_key_label(raw: &str) -> String {
    match raw.trim().to_ascii_lowercase().as_str() {
        "" | "off" | "none" | "disabled" => "Disabled".to_string(),
        "scrolllock" | "scroll_lock" | "scroll-lock" => "Scroll Lock".to_string(),
        "sysrq" | "sysreq" | "prtsc" | "printscreen" | "print_screen" | "print-screen" => {
            "SysRq / PrtSc".to_string()
        }
        "pause" | "pausebreak" | "pause_break" | "pause-break" => "Pause".to_string(),
        "pageup" | "page_up" | "page-up" => "Page Up".to_string(),
        "pagedown" | "page_down" | "page-down" => "Page Down".to_string(),
        "capslock" | "caps_lock" | "caps-lock" => "Caps Lock".to_string(),
        "backspace" | "back_space" | "back-space" => "Backspace".to_string(),
        "space" | "spacebar" => "Space".to_string(),
        "escape" | "esc" => "Escape".to_string(),
        value
            if value.starts_with('f')
                && value.len() <= 3
                && value[1..].chars().all(|ch| ch.is_ascii_digit()) =>
        {
            value.to_ascii_uppercase()
        }
        value if value.len() == 1 => value.to_ascii_uppercase(),
        value => capitalize(&value.replace(['_', '-'], " ")),
    }
}

/// Keeps `capture_swap_key` explicit because it sits on launcher state/UI wiring, where device choices should remain explainable across refreshes.
/// Inputs are the typed parameters; output is the return value or side effect.
pub fn capture_swap_key(key: gtk::gdk::Key) -> Option<String> {
    let normalized_name = key.name()?.to_string().to_ascii_lowercase();
    match normalized_name.as_str() {
        "shift_l" | "shift_r" | "control_l" | "control_r" | "alt_l" | "alt_r" | "super_l"
        | "super_r" | "meta_l" | "meta_r" | "hyper_l" | "hyper_r" | "iso_level3_shift"
        | "multi_key" => None,
        "scroll_lock" => Some("scrolllock".to_string()),
        "sys_req" | "print" => Some("sysrq".to_string()),
        "pause" | "break" => Some("pause".to_string()),
        "page_up" => Some("pageup".to_string()),
        "page_down" => Some("pagedown".to_string()),
        "caps_lock" => Some("capslock".to_string()),
        "backspace" => Some("backspace".to_string()),
        "return" => Some("enter".to_string()),
        "space" => Some("space".to_string()),
        "escape" => Some("escape".to_string()),
        "kp_0" => Some("0".to_string()),
        "kp_1" => Some("1".to_string()),
        "kp_2" => Some("2".to_string()),
        "kp_3" => Some("3".to_string()),
        "kp_4" => Some("4".to_string()),
        "kp_5" => Some("5".to_string()),
        "kp_6" => Some("6".to_string()),
        "kp_7" => Some("7".to_string()),
        "kp_8" => Some("8".to_string()),
        "kp_9" => Some("9".to_string()),
        other
            if other.starts_with('f')
                && other.len() <= 3
                && other[1..].chars().all(|ch| ch.is_ascii_digit()) =>
        {
            Some(other.to_string())
        }
        other if other.len() == 1 => {
            let ch = other.chars().next()?;
            if ch.is_ascii_alphanumeric() {
                Some(ch.to_ascii_lowercase().to_string())
            } else {
                None
            }
        }
        "insert" | "delete" | "home" | "end" | "left" | "right" | "up" | "down" | "tab" => {
            Some(normalized_name)
        }
        _ => None,
    }
}
