const CLIENT_BIN_ENV: &str = "LESAVKA_CLIENT_BIN";
const INSTALLED_CLIENT_BIN: &str = "/usr/local/bin/lesavka-client";

pub fn spawn_client_process(
    server_addr: &str,
    state: &LauncherState,
    input_toggle_key: &str,
    input_control_path: &Path,
    input_state_path: &Path,
    input_toggle_control_path: &Path,
) -> Result<RelayChild> {
    let exe = resolve_relay_child_exe()?;
    let mut command = Command::new(&exe);
    command.arg("--no-launcher");
    command.stdout(Stdio::piped());
    command.stderr(Stdio::piped());
    command.env("LESAVKA_LAUNCHER_CHILD", "1");
    command.env(
        "LESAVKA_LAUNCHER_PARENT_PID",
        std::process::id().to_string(),
    );
    if let Some(start_ticks) = super::launcher_parent_start_ticks() {
        command.env("LESAVKA_LAUNCHER_PARENT_START_TICKS", start_ticks);
    }
    command.env("LESAVKA_SERVER_ADDR", server_addr);
    command.env("LESAVKA_INPUT_TOGGLE_KEY", input_toggle_key);
    command.env("LESAVKA_LAUNCHER_WINDOW_TITLE", "Lesavka");
    command.env("LESAVKA_FOCUS_LAUNCHER_ON_LOCAL", "1");
    command.env(LAUNCHER_FOCUS_SIGNAL_ENV, launcher_focus_signal_path());
    command.env(
        LAUNCHER_CLIPBOARD_CONTROL_ENV,
        launcher_clipboard_control_path(),
    );
    command.env(INPUT_CONTROL_ENV, input_control_path);
    command.env(INPUT_STATE_ENV, input_state_path);
    command.env(TOGGLE_KEY_CONTROL_ENV, input_toggle_control_path);
    let wake_path = wake_control_path();
    command.env(WAKE_CONTROL_ENV, wake_path);
    command.env("LESAVKA_DISABLE_VIDEO_RENDER", "1");
    command.env("LESAVKA_CLIPBOARD_PASTE", "1");
    let audio_gain_path = audio_gain_control_path();
    let _ = write_audio_gain_request(&audio_gain_path, state.audio_gain_percent);
    command.env(AUDIO_GAIN_CONTROL_ENV, audio_gain_path);
    let mic_gain_path = mic_gain_control_path();
    let _ = write_mic_gain_request(&mic_gain_path, state.mic_gain_percent);
    command.env(MIC_GAIN_CONTROL_ENV, mic_gain_path);
    let media_control_path = media_control_path();
    let _ = write_media_control_request(&media_control_path, state);
    command.env(MEDIA_CONTROL_ENV, media_control_path);
    let camera_preview_path = uplink_camera_preview_path();
    let _ = std::fs::remove_file(&camera_preview_path);
    command.env(UPLINK_CAMERA_PREVIEW_ENV, camera_preview_path);
    let mic_level_path = uplink_mic_level_path();
    let _ = std::fs::remove_file(&mic_level_path);
    command.env(UPLINK_MIC_LEVEL_ENV, mic_level_path);
    let uplink_telemetry_path = uplink_telemetry_path();
    let _ = std::fs::remove_file(&uplink_telemetry_path);
    command.env(UPLINK_TELEMETRY_ENV, uplink_telemetry_path);
    for (key, value) in runtime_env_vars(state) {
        command.env(key, value);
    }
    let child = command.spawn().map_err(|err| {
        anyhow::anyhow!("starting relay child {} failed: {err}", exe.display())
    })?;
    Ok(child)
}

fn resolve_relay_child_exe() -> Result<PathBuf> {
    let env_override = std::env::var_os(CLIENT_BIN_ENV).map(PathBuf::from);
    let current_exe = std::env::current_exe().map_err(|err| {
        anyhow::anyhow!("resolving launcher executable for relay child failed: {err}")
    })?;
    resolve_relay_child_exe_from(
        env_override.as_deref(),
        &current_exe,
        Path::new(INSTALLED_CLIENT_BIN),
    )
}

fn resolve_relay_child_exe_from(
    env_override: Option<&Path>,
    current_exe: &Path,
    installed_client: &Path,
) -> Result<PathBuf> {
    if let Some(override_path) = env_override
        && !override_path.as_os_str().is_empty()
    {
        if override_path.is_file() {
            return Ok(override_path.to_path_buf());
        }
        return Err(anyhow::anyhow!(
            "{CLIENT_BIN_ENV} points at {}, but that file is not runnable",
            override_path.display()
        ));
    }

    if relay_exe_path_is_runnable(current_exe) {
        return Ok(current_exe.to_path_buf());
    }

    if installed_client.is_file() {
        return Ok(installed_client.to_path_buf());
    }

    Err(anyhow::anyhow!(
        "launcher executable {} is no longer runnable and fallback {} is missing; restart Lesavka or reinstall the client",
        current_exe.display(),
        installed_client.display()
    ))
}

fn relay_exe_path_is_runnable(path: &Path) -> bool {
    path.is_file()
        && !path
            .as_os_str()
            .to_string_lossy()
            .ends_with(" (deleted)")
}

pub fn attach_child_log_streams(child: &mut RelayChild, tx: Sender<String>) {
    if let Some(stdout) = child.stdout.take() {
        spawn_log_reader(stdout, "[relay] ", tx.clone());
    }
    if let Some(stderr) = child.stderr.take() {
        spawn_log_reader(stderr, "[relay:stderr] ", tx);
    }
}

fn spawn_log_reader<R>(reader: R, prefix: &'static str, tx: Sender<String>)
where
    R: std::io::Read + Send + 'static,
{
    std::thread::spawn(move || {
        for line in BufReader::new(reader)
            .lines()
            .map_while(std::result::Result::ok)
        {
            let trimmed = line.trim();
            if !trimmed.is_empty() {
                let _ = tx.send(format!("{prefix}{trimmed}"));
            }
        }
    });
}

pub fn append_session_log(buffer: &gtk::TextBuffer, message: &str) {
    let cleaned = strip_ansi_sequences(message);
    let trimmed = cleaned.trim();
    if trimmed.is_empty() {
        return;
    }
    append_clean_session_log(buffer, trimmed);
}

/// Appends a session log line only when it passes the selected severity filter.
pub fn append_session_log_for_level(
    buffer: &gtk::TextBuffer,
    message: &str,
    level: ConsoleLogLevel,
) -> bool {
    let cleaned = strip_ansi_sequences(message);
    let trimmed = cleaned.trim();
    if trimmed.is_empty() || !should_show_clean_session_log_line(trimmed, level) {
        return false;
    }
    append_clean_session_log(buffer, trimmed);
    true
}

#[cfg(test)]
fn should_show_session_log_line(message: &str, level: ConsoleLogLevel) -> bool {
    let cleaned = strip_ansi_sequences(message);
    let trimmed = cleaned.trim();
    !trimmed.is_empty() && should_show_clean_session_log_line(trimmed, level)
}

/// Writes a cleaned line with the GTK text tags that match its source/severity.
fn append_clean_session_log(buffer: &gtk::TextBuffer, trimmed: &str) {
    let mut end = buffer.end_iter();
    let tags = classify_log_tags(trimmed);
    if tags.is_empty() {
        buffer.insert(&mut end, &format!("{trimmed}\n"));
    } else {
        buffer.insert_with_tags_by_name(&mut end, &format!("{trimmed}\n"), &tags);
    }
}

pub fn copy_session_log(buffer: &gtk::TextBuffer) -> Result<()> {
    let text = buffer
        .text(&buffer.start_iter(), &buffer.end_iter(), false)
        .to_string();
    copy_plain_text(&text)
}

pub fn copy_plain_text(text: &str) -> Result<()> {
    let display = gtk::gdk::Display::default()
        .ok_or_else(|| anyhow::anyhow!("no desktop clipboard is available in this session"))?;
    display.clipboard().set_text(text);
    Ok(())
}

pub fn refresh_diagnostics_report(
    widgets: &LauncherWidgets,
    state: &LauncherState,
    child_running: bool,
) {
    let mut snapshot = SnapshotReport::from_state(
        state,
        &widgets.diagnostics_log.borrow(),
        quality_probe_command().to_string(),
    );
    if child_running && !snapshot.remote_active {
        snapshot.recommendations.insert(
            0,
            "The relay child is still alive while launcher state says inactive; give it a moment or reconnect before trusting throughput feel.".to_string(),
        );
    }
    let rendered = snapshot.to_pretty_text();
    if *widgets.diagnostics_rendered_text.borrow() == rendered {
        return;
    }

    let diagnostics_adjustment = widgets.diagnostics_scroll.vadjustment();
    let previous_value = diagnostics_adjustment.value();
    let previous_max =
        (diagnostics_adjustment.upper() - diagnostics_adjustment.page_size()).max(0.0);
    let was_at_bottom = previous_max <= 0.0 || previous_value >= (previous_max - 4.0);
    let popout_adjustment = widgets
        .diagnostics_popout_scroll
        .borrow()
        .as_ref()
        .map(|scroll| scroll.vadjustment());
    let popout_state = popout_adjustment.as_ref().map(|adjustment| {
        let previous_value = adjustment.value();
        let previous_max = (adjustment.upper() - adjustment.page_size()).max(0.0);
        let was_at_bottom = previous_max <= 0.0 || previous_value >= (previous_max - 4.0);
        (adjustment.clone(), previous_value, was_at_bottom)
    });
    let restore_adjustment =
        |adjustment: &gtk::Adjustment, previous_value: f64, was_at_bottom: bool| {
            let max = (adjustment.upper() - adjustment.page_size()).max(0.0);
            let target = if was_at_bottom {
                max
            } else {
                previous_value.min(max)
            };
            if (adjustment.value() - target).abs() > 1.0 {
                adjustment.set_value(target);
            }
        };
    *widgets.diagnostics_rendered_text.borrow_mut() = rendered.clone();
    let update_docked = was_at_bottom || widgets.diagnostics_label.text().is_empty();
    if update_docked {
        widgets.diagnostics_label.set_text(&rendered);
        restore_adjustment(&diagnostics_adjustment, previous_value, was_at_bottom);
    }
    let update_popout = popout_state
        .as_ref()
        .map(|(_, _, was_at_bottom)| *was_at_bottom)
        .unwrap_or(false);
    if let Some(label) = widgets.diagnostics_popout_label.borrow().as_ref()
        && (update_popout || label.text().is_empty())
    {
        label.set_text(&rendered);
    }
    if update_popout
        && let Some((adjustment, previous_value, was_at_bottom)) = popout_state.as_ref()
    {
        restore_adjustment(adjustment, *previous_value, *was_at_bottom);
    }
    glib::idle_add_local_once(move || {
        if update_docked {
            restore_adjustment(&diagnostics_adjustment, previous_value, was_at_bottom);
        }
        if update_popout && let Some((adjustment, previous_value, was_at_bottom)) = popout_state {
            restore_adjustment(&adjustment, previous_value, was_at_bottom);
        }
    });
}
