pub mod devices;
pub mod diagnostics;
pub mod state;

#[cfg(not(coverage))]
mod calibration;
mod clipboard;
#[cfg(not(coverage))]
mod device_test;
#[cfg(not(coverage))]
mod power;
#[cfg(not(coverage))]
mod preview;
mod ui;
#[cfg(not(coverage))]
mod ui_components;
#[cfg(not(coverage))]
mod ui_runtime;

use std::{collections::BTreeMap, path::PathBuf};

use crate::app_support::DEFAULT_SERVER_ADDR;
use anyhow::Result;

pub use diagnostics::{DiagnosticsLog, PerformanceSample, SnapshotReport, quality_probe_command};
pub use state::{CapturePowerStatus, DeviceSelection, InputRouting, LauncherState, ViewMode};

pub const LAUNCHER_FOCUS_SIGNAL_ENV: &str = "LESAVKA_LAUNCHER_FOCUS_SIGNAL";
pub const DEFAULT_LAUNCHER_FOCUS_SIGNAL_PATH: &str = "/tmp/lesavka-launcher-focus.signal";
pub const LAUNCHER_CLIPBOARD_CONTROL_ENV: &str = "LESAVKA_LAUNCHER_CLIPBOARD_CONTROL";
pub const DEFAULT_LAUNCHER_CLIPBOARD_CONTROL_PATH: &str = "/tmp/lesavka-launcher-clipboard.control";
pub const LAUNCHER_PARENT_PID_ENV: &str = "LESAVKA_LAUNCHER_PARENT_PID";
pub const LAUNCHER_PARENT_START_TICKS_ENV: &str = "LESAVKA_LAUNCHER_PARENT_START_TICKS";
pub const REMOTE_INPUT_FAILSAFE_SECONDS_ENV: &str = "LESAVKA_INPUT_REMOTE_FAILSAFE_SECS";
pub const DEFAULT_REMOTE_INPUT_FAILSAFE_SECONDS: &str = "0";

#[derive(Clone, Debug, Eq, PartialEq)]
struct LauncherParentProcess {
    pid: u32,
    start_ticks: Option<String>,
}

/// Start a safe watchdog in relay children so launcher crashes do not leak streams.
#[cfg(not(coverage))]
pub fn start_launcher_child_parent_watchdog_from_env() {
    let Some(parent) = launcher_parent_process_from_env() else {
        return;
    };

    if !launcher_parent_process_matches(&parent) {
        eprintln!("[launcher-watchdog] launcher parent is already gone; exiting relay child");
        std::process::exit(0);
    }

    let spawn_result = std::thread::Builder::new()
        .name("launcher-parent-watchdog".to_string())
        .spawn(move || {
            loop {
                std::thread::sleep(std::time::Duration::from_millis(500));
                if !launcher_parent_process_matches(&parent) {
                    eprintln!(
                        "[launcher-watchdog] launcher parent disappeared; exiting relay child"
                    );
                    std::process::exit(0);
                }
            }
        });

    if let Err(err) = spawn_result {
        eprintln!("[launcher-watchdog] failed to start parent watchdog: {err}");
    }
}

/// Coverage-safe watchdog entrypoint; the live version can call `process::exit`.
#[cfg(coverage)]
pub fn start_launcher_child_parent_watchdog_from_env() {
    let _ = launcher_parent_process_from_env();
}

#[must_use]
pub(crate) fn launcher_parent_start_ticks() -> Option<String> {
    process_start_ticks(std::process::id())
}

#[must_use]
fn launcher_parent_process_from_env() -> Option<LauncherParentProcess> {
    let pid = std::env::var(LAUNCHER_PARENT_PID_ENV)
        .ok()?
        .trim()
        .parse::<u32>()
        .ok()?;
    let start_ticks = std::env::var(LAUNCHER_PARENT_START_TICKS_ENV)
        .ok()
        .map(|value| value.trim().to_string())
        .filter(|value| !value.is_empty());
    Some(LauncherParentProcess { pid, start_ticks })
}

#[must_use]
fn launcher_parent_process_matches(parent: &LauncherParentProcess) -> bool {
    let Some(current_start_ticks) = process_start_ticks(parent.pid) else {
        return false;
    };
    parent
        .start_ticks
        .as_deref()
        .is_none_or(|expected| expected == current_start_ticks)
}

#[must_use]
fn process_start_ticks(pid: u32) -> Option<String> {
    let stat = std::fs::read_to_string(format!("/proc/{pid}/stat")).ok()?;
    proc_stat_start_ticks(&stat)
}

#[must_use]
fn proc_stat_start_ticks(stat: &str) -> Option<String> {
    let after_name = stat.rsplit_once(") ")?.1;
    after_name
        .split_whitespace()
        .nth(19)
        .map(ToString::to_string)
}

pub fn maybe_run_launcher(args: &[String]) -> Result<bool> {
    if should_run_launcher(args) {
        let server_addr = resolve_server_addr(args);
        ui::run_gui_launcher(server_addr)?;
        return Ok(true);
    }
    Ok(false)
}

/// Decides when to present the GUI launcher instead of direct session startup.
fn should_run_launcher(args: &[String]) -> bool {
    !args.iter().any(|arg| arg == "--no-launcher")
}

pub fn runtime_env_vars(state: &LauncherState) -> BTreeMap<String, String> {
    let mut envs = BTreeMap::new();
    envs.insert(
        "LESAVKA_CAPTURE_REMOTE".to_string(),
        state.routing.as_env().to_string(),
    );
    envs.insert(
        "LESAVKA_VIEW_MODE".to_string(),
        state.view_mode.as_env().to_string(),
    );
    envs.insert("LESAVKA_CLIPBOARD_DELAY_MS".to_string(), "18".to_string());
    envs.insert(
        REMOTE_INPUT_FAILSAFE_SECONDS_ENV.to_string(),
        std::env::var(REMOTE_INPUT_FAILSAFE_SECONDS_ENV)
            .ok()
            .filter(|value| value.trim().parse::<u64>().is_ok())
            .unwrap_or_else(|| DEFAULT_REMOTE_INPUT_FAILSAFE_SECONDS.to_string()),
    );
    envs.insert(
        "LESAVKA_AUDIO_GAIN".to_string(),
        state.audio_gain_env_value(),
    );
    envs.insert("LESAVKA_MIC_GAIN".to_string(), state.mic_gain_env_value());
    envs.insert(
        "LESAVKA_CAM_CODEC".to_string(),
        state.effective_webcam_transport().env_value().to_string(),
    );
    envs.insert(
        "LESAVKA_UPLINK_AUDIO_CODEC".to_string(),
        state.upstream_audio_transport.env_value().to_string(),
    );
    envs.insert(
        "LESAVKA_MIC_NOISE_SUPPRESSION".to_string(),
        if state.mic_noise_suppression {
            "1"
        } else {
            "0"
        }
        .to_string(),
    );
    if matches!(state.view_mode, ViewMode::Unified) {
        envs.insert("LESAVKA_DISABLE_VIDEO_RENDER".to_string(), "1".to_string());
    }
    if let Some(camera) = state.devices.camera.as_ref() {
        envs.insert("LESAVKA_CAM_SOURCE".to_string(), camera.clone());
        if let Some(mode) = state.camera_quality {
            envs.insert("LESAVKA_CAM_WIDTH".to_string(), mode.width.to_string());
            envs.insert("LESAVKA_CAM_HEIGHT".to_string(), mode.height.to_string());
            envs.insert("LESAVKA_CAM_FPS".to_string(), mode.fps.to_string());
            envs.insert(
                "LESAVKA_CAM_H264_KBIT".to_string(),
                mode.h264_bitrate_kbit().to_string(),
            );
        }
    } else {
        envs.insert("LESAVKA_CAM_DISABLE".to_string(), "1".to_string());
    }
    if let Some(microphone) = state.devices.microphone.as_ref() {
        envs.insert("LESAVKA_MIC_SOURCE".to_string(), microphone.clone());
    } else {
        envs.insert("LESAVKA_MIC_DISABLE".to_string(), "1".to_string());
    }
    if let Some(speaker) = state.devices.speaker.as_ref() {
        envs.insert("LESAVKA_AUDIO_SINK".to_string(), speaker.clone());
    } else {
        envs.insert("LESAVKA_AUDIO_DISABLE".to_string(), "1".to_string());
    }
    if let Some(keyboard) = state.devices.keyboard.as_ref() {
        envs.insert("LESAVKA_KEYBOARD_DEVICE".to_string(), keyboard.clone());
    }
    if let Some(mouse) = state.devices.mouse.as_ref() {
        envs.insert("LESAVKA_MOUSE_DEVICE".to_string(), mouse.clone());
    }
    for key in [
        "LESAVKA_PASTE_KEY",
        "LESAVKA_PASTE_KEY_FILE",
        "LESAVKA_PASTE_RPC",
        "LESAVKA_PASTE_MAX",
        "LESAVKA_PASTE_DELAY_MS",
        "LESAVKA_CLIPBOARD_CMD",
        "LESAVKA_CLIPBOARD_TIMEOUT_MS",
    ] {
        if let Ok(value) = std::env::var(key)
            && !value.trim().is_empty()
        {
            envs.insert(key.to_string(), value);
        }
    }
    envs
}

pub fn launcher_focus_signal_path() -> PathBuf {
    std::env::var(LAUNCHER_FOCUS_SIGNAL_ENV)
        .map(PathBuf::from)
        .unwrap_or_else(|_| PathBuf::from(DEFAULT_LAUNCHER_FOCUS_SIGNAL_PATH))
}

pub fn launcher_clipboard_control_path() -> PathBuf {
    std::env::var(LAUNCHER_CLIPBOARD_CONTROL_ENV)
        .map(PathBuf::from)
        .unwrap_or_else(|_| PathBuf::from(DEFAULT_LAUNCHER_CLIPBOARD_CONTROL_PATH))
}

fn resolve_server_addr(args: &[String]) -> String {
    for window in args.windows(2) {
        if window[0] == "--server" {
            return window[1].clone();
        }
    }
    args.iter()
        .find(|arg| !arg.starts_with("--"))
        .cloned()
        .or_else(|| std::env::var("LESAVKA_SERVER_ADDR").ok())
        .unwrap_or_else(|| DEFAULT_SERVER_ADDR.to_string())
}

#[cfg(test)]
#[path = "tests/mod.rs"]
mod tests;
