//! SteamCMD update integration for process-managed game instances.
//!
//! Wraps the `steamcmd` binary to perform an `+app_update` for a given game
//! instance, streaming stdout lines to a caller-supplied progress callback so
//! the panel can display live update output.  The agent already runs a task per
//! command in a separate `tokio::spawn`, so the blocking-until-done semantics
//! here are intentional — the NATS reply is sent only when SteamCMD exits.
//!
//! Dune is Docker-image-based and explicitly has no SteamCMD integration — any
//! attempt to invoke `update` on a Dune instance returns a clear error rather
//! than a silent no-op.

use std::path::Path;
use tokio::io::{AsyncBufReadExt, BufReader};
use tokio::process::Command;

/// Return the Steam app ID for a given game name, or `None` for Dune (Docker).
///
/// Soulmask returns the Windows or Linux server app ID depending on the compile
/// target so this function is `#[cfg]`-gated at the platform level.
pub fn app_id_for_game(game: &str) -> Option<u32> {
    match game {
        "rust" => Some(258550),
        "conan" => Some(443030),
        "soulmask" => {
            #[cfg(windows)]
            {
                Some(3017310)
            }
            #[cfg(not(windows))]
            {
                Some(3017300)
            }
        }
        // Dune uses Docker images — SteamCMD has no role here.
        "dune" => None,
        _ => None,
    }
}

/// Configuration controlling SteamCMD behaviour for one instance.
/// Serialised as `[instance.steamcmd]` in agent.toml.
#[derive(Debug, Clone, serde::Deserialize, Default)]
pub struct SteamcmdConfig {
    /// Absolute or relative path to the `steamcmd` binary.
    /// Defaults to `"steamcmd"` (resolved via `PATH`) when absent.
    #[serde(default)]
    pub steamcmd_path: Option<std::path::PathBuf>,

    /// Whether to pass `validate` to `+app_update`.  Adds a file-hash check
    /// pass that catches corruption at the cost of a longer update time.
    #[serde(default)]
    pub validate: bool,
}

/// Run a SteamCMD update for `game` into `install_dir`.
///
/// - `steamcmd_path`: path to the binary (or `"steamcmd"` to use PATH).
/// - `validate`: appends `validate` to the `+app_update` call.
/// - `on_progress`: receives each stdout line as it arrives so callers can
///   forward progress to the panel in real time.
///
/// Returns `Ok(())` on a zero exit code, otherwise an error describing the
/// failure.  Dune is rejected before any process is spawned.
pub async fn update(
    game: &str,
    install_dir: &Path,
    steamcmd_path: &str,
    validate: bool,
    on_progress: impl Fn(&str),
) -> anyhow::Result<()> {
    use anyhow::Context;

    let app_id = app_id_for_game(game).ok_or_else(|| {
        anyhow::anyhow!(
            "dune uses Docker images, not SteamCMD — cannot run app_update for game '{game}'"
        )
    })?;

    let install_dir_str = install_dir
        .to_str()
        .with_context(|| format!("install_dir '{}' is not valid UTF-8", install_dir.display()))?;

    let mut args: Vec<String> = vec![
        "+force_install_dir".to_string(),
        install_dir_str.to_string(),
        "+login".to_string(),
        "anonymous".to_string(),
        "+app_update".to_string(),
        app_id.to_string(),
    ];
    if validate {
        args.push("validate".to_string());
    }
    args.push("+quit".to_string());

    tracing::info!(
        "steamcmd: starting update for game={game} app_id={app_id} install_dir={} validate={validate}",
        install_dir.display()
    );

    let mut child = Command::new(steamcmd_path)
        .args(&args)
        .stdout(std::process::Stdio::piped())
        .stderr(std::process::Stdio::null())
        .spawn()
        .with_context(|| format!("spawning steamcmd binary '{steamcmd_path}'"))?;

    let stdout = child.stdout.take().expect("stdout was piped");
    let mut lines = BufReader::new(stdout).lines();

    while let Some(line) = lines.next_line().await.context("reading steamcmd stdout")? {
        tracing::debug!("steamcmd: {line}");
        on_progress(&line);
    }

    let status = child.wait().await.context("waiting for steamcmd to exit")?;
    if status.success() {
        tracing::info!("steamcmd: update completed successfully for game={game}");
        Ok(())
    } else {
        let code = status.code().unwrap_or(-1);
        anyhow::bail!("steamcmd exited with non-zero status {code} for game={game}")
    }
}

