//! Per-instance command channel + state-change events.
//!
//! Each process-managed instance gets a request-reply subscriber on
//! `corrosion.{license}.{instance_id}.cmd` (funcs: start/stop/restart/status/rcon)
//! and a publisher task that pushes every supervisor state change to
//! `corrosion.{license}.{instance_id}.status` — the panel sees crashes when
//! they happen, not when the next heartbeat ambles in.

use chrono::{SecondsFormat, Utc};
use futures::StreamExt;
use serde::Deserialize;
use serde_json::json;
use std::sync::Arc;

use crate::agent::Agent;
use crate::subjects;
use crate::steamcmd;
use crate::supervisor::Supervisor;
use crate::wipe;

#[derive(Debug, Deserialize)]
struct InstanceCommand {
    func: String,
    /// Payload for funcs that carry a text argument (e.g. rcon).
    #[serde(default)]
    command: Option<String>,
    /// Wipe type: "map" | "blueprint" | "full" — required for func="wipe".
    #[serde(default)]
    wipe_type: Option<wipe::WipeType>,
    /// Whether to back up wipe targets before deleting (func="wipe").
    #[serde(default)]
    backup: bool,
    /// Label for the backup subdirectory (func="wipe"). Defaults to "wipe-backup".
    #[serde(default = "default_backup_label")]
    backup_label: String,
}

fn default_backup_label() -> String {
    "wipe-backup".to_string()
}

/// Forward every supervisor state change as a status event.
pub async fn publish_state_changes(agent: Arc<Agent>, sup: Arc<dyn Supervisor>) {
    let subject = subjects::instance_status(&agent.cfg.license_id, sup.instance_id());
    let mut rx = sup.watch_state();
    let cancel = agent.shutdown.clone();

    loop {
        tokio::select! {
            changed = rx.changed() => {
                if changed.is_err() {
                    break;
                }
                let state = rx.borrow().clone();
                let event = json!({
                    "timestamp": Utc::now().to_rfc3339_opts(SecondsFormat::Secs, true),
                    "instance_id": sup.instance_id(),
                    "event": state,
                });
                match serde_json::to_vec(&event) {
                    Ok(bytes) => {
                        if let Err(e) = agent.nats.publish(subject.clone(), bytes.into()).await {
                            tracing::warn!("status publish failed for '{}': {e}", sup.instance_id());
                        }
                    }
                    Err(e) => tracing::error!("status serialize failed: {e}"),
                }
            }
            _ = cancel.cancelled() => break,
        }
    }
}

/// Request-reply command handler for one instance.
pub async fn run(agent: Arc<Agent>, sup: Arc<dyn Supervisor>) -> anyhow::Result<()> {
    let subject = subjects::instance_cmd(&agent.cfg.license_id, sup.instance_id());
    let mut sub = agent.nats.subscribe(subject.clone()).await?;
    tracing::info!("instance command handler listening on {subject}");

    let cancel = agent.shutdown.clone();
    loop {
        tokio::select! {
            msg = sub.next() => {
                match msg {
                    Some(msg) => {
                        let agent = agent.clone();
                        let sup = sup.clone();
                        tokio::spawn(async move { handle(agent, sup, msg).await });
                    }
                    None => {
                        tracing::warn!("instance command subscription ended for '{}'", sup.instance_id());
                        break;
                    }
                }
            }
            _ = cancel.cancelled() => {
                tracing::info!("instance command handler stopping for '{}'", sup.instance_id());
                break;
            }
        }
    }
    Ok(())
}

async fn handle(agent: Arc<Agent>, sup: Arc<dyn Supervisor>, msg: async_nats::Message) {
    let Some(reply) = msg.reply.clone() else {
        tracing::warn!("instance command without reply subject ignored");
        return;
    };

    let response = match serde_json::from_slice::<InstanceCommand>(&msg.payload) {
        Ok(cmd) => dispatch(&agent, &sup, &cmd).await,
        Err(e) => json!({ "status": "error", "message": format!("invalid command payload: {e}") }),
    };

    let bytes = match serde_json::to_vec(&response) {
        Ok(b) => b,
        Err(e) => {
            tracing::error!("response serialize failed: {e}");
            return;
        }
    };
    if let Err(e) = agent.nats.publish(reply, bytes.into()).await {
        tracing::warn!("response publish failed: {e}");
    }
}

async fn dispatch(
    agent: &Arc<Agent>,
    sup: &Arc<dyn Supervisor>,
    cmd: &InstanceCommand,
) -> serde_json::Value {
    let func = cmd.func.as_str();

    // start/stop/restart take `self: Arc<Self>` (they may hand a clone to a
    // monitor task), so clone the Arc before the consuming call.
    let outcome = match func {
        "start" => sup.clone().start().await.map(|_| "starting"),
        "stop" => sup.clone().stop().await.map(|_| "stopped"),
        "restart" => sup.clone().restart().await.map(|_| "restarted"),
        "status" => {
            return json!({
                "status": "success",
                "func": "status",
                "instance_id": sup.instance_id(),
                "state": sup.state(),
                "uptime_seconds": sup.uptime_seconds().await,
            });
        }
        "rcon" => {
            // Look up the InstanceConfig for this supervisor so we can access
            // rcon settings and the game name without changing the supervisor's
            // data model.
            let inst_cfg = agent
                .cfg
                .instances
                .iter()
                .find(|i| i.id == sup.instance_id());

            let rcon_cfg = inst_cfg.and_then(|i| i.rcon.as_ref());
            let Some(rcon_cfg) = rcon_cfg else {
                return json!({
                    "status": "error",
                    "func": "rcon",
                    "instance_id": sup.instance_id(),
                    "message": format!("instance '{}' has no rcon configured", sup.instance_id()),
                });
            };

            let Some(command) = cmd.command.as_deref() else {
                return json!({
                    "status": "error",
                    "func": "rcon",
                    "instance_id": sup.instance_id(),
                    "message": "rcon func requires a 'command' field",
                });
            };

            let game = inst_cfg.map(|i| i.game.as_str()).unwrap_or("rust");
            return match crate::rcon::send_command(rcon_cfg, game, command).await {
                Ok(output) => json!({
                    "status": "success",
                    "func": "rcon",
                    "instance_id": sup.instance_id(),
                    "output": output,
                }),
                Err(e) => json!({
                    "status": "error",
                    "func": "rcon",
                    "instance_id": sup.instance_id(),
                    "message": format!("{e:#}"),
                }),
            };
        }
        "steam_update" => {
            // Look up instance config for game name, root, and optional steamcmd
            // settings.  The supervisor only carries process-control state, not
            // the full config, so we reach into agent.cfg.instances here as the
            // rcon dispatch does.
            let inst_cfg = agent.cfg.instances.iter().find(|i| i.id == sup.instance_id());

            let Some(inst_cfg) = inst_cfg else {
                return json!({
                    "status": "error",
                    "func": "steam_update",
                    "instance_id": sup.instance_id(),
                    "message": format!("no config found for instance '{}'", sup.instance_id()),
                });
            };

            let game = inst_cfg.game.as_str();
            let root = inst_cfg.root.clone();

            // Resolve steamcmd path and validate flag from config or use defaults.
            let (steamcmd_path, validate) = match inst_cfg.steamcmd.as_ref() {
                Some(s) => {
                    let path = s
                        .steamcmd_path
                        .as_ref()
                        .and_then(|p| p.to_str().map(|s| s.to_string()))
                        .unwrap_or_else(|| "steamcmd".to_string());
                    (path, s.validate)
                }
                None => ("steamcmd".to_string(), false),
            };

            let license = agent.cfg.license_id.clone();
            let instance_id = sup.instance_id().to_string();
            let nats = agent.nats.clone();

            // Publish each progress line to the steam_status subject.
            let on_progress = move |line: &str| {
                let subject = subjects::instance_steam_status(&license, &instance_id);
                let event = json!({
                    "timestamp": Utc::now().to_rfc3339_opts(SecondsFormat::Secs, true),
                    "instance_id": instance_id,
                    "line": line,
                });
                match serde_json::to_vec(&event) {
                    Ok(bytes) => {
                        // Fire-and-forget; the async publish is non-blocking on
                        // the caller side.  We create a mini-runtime task via
                        // a oneshot since on_progress is Fn (not async).
                        let nats = nats.clone();
                        tokio::spawn(async move {
                            if let Err(e) = nats.publish(subject, bytes.into()).await {
                                tracing::warn!("steam_status publish failed: {e}");
                            }
                        });
                    }
                    Err(e) => tracing::error!("steam_status serialize failed: {e}"),
                }
            };

            return match steamcmd::update(game, &root, &steamcmd_path, validate, on_progress).await {
                Ok(()) => json!({
                    "status": "success",
                    "func": "steam_update",
                    "instance_id": sup.instance_id(),
                }),
                Err(e) => json!({
                    "status": "error",
                    "func": "steam_update",
                    "instance_id": sup.instance_id(),
                    "message": format!("{e:#}"),
                }),
            };
        }
        "wipe" => {
            let inst_cfg = agent.cfg.instances.iter().find(|i| i.id == sup.instance_id());

            let Some(inst_cfg) = inst_cfg else {
                return json!({
                    "status": "error",
                    "func": "wipe",
                    "instance_id": sup.instance_id(),
                    "message": format!("no config found for instance '{}'", sup.instance_id()),
                });
            };

            let Some(wipe_type) = cmd.wipe_type.clone() else {
                return json!({
                    "status": "error",
                    "func": "wipe",
                    "instance_id": sup.instance_id(),
                    "message": "wipe func requires a 'wipe_type' field (\"map\", \"blueprint\", or \"full\")",
                });
            };

            let root = inst_cfg.root.clone();
            let instance_id = sup.instance_id().to_string();

            let wipe_req = wipe::WipeRequest {
                wipe_type,
                backup: cmd.backup,
                backup_label: cmd.backup_label.clone(),
            };

            // Stop the server best-effort before wiping; proceed even if stop fails
            // (the server may already be down).
            if let Err(e) = sup.clone().stop().await {
                tracing::warn!("wipe: stop instance '{}' failed (proceeding anyway): {e:#}", instance_id);
            }

            // Run the blocking I/O on the blocking thread pool.
            let result = tokio::task::spawn_blocking(move || wipe::execute(&root, &wipe_req)).await;

            // Restart best-effort regardless of wipe outcome.
            if let Err(e) = sup.clone().start().await {
                tracing::warn!("wipe: restart instance '{}' failed: {e:#}", instance_id);
            }

            return match result {
                Ok(Ok(wr)) => {
                    let wipe_type_str = format!("{:?}", wr.wipe_type).to_lowercase();
                    json!({
                        "status": "success",
                        "func": "wipe",
                        "instance_id": sup.instance_id(),
                        "wipe_type": wipe_type_str,
                        "deleted_count": wr.deleted_count,
                    })
                }
                Ok(Err(e)) => json!({
                    "status": "error",
                    "func": "wipe",
                    "instance_id": sup.instance_id(),
                    "message": format!("{e:#}"),
                }),
                Err(e) => json!({
                    "status": "error",
                    "func": "wipe",
                    "instance_id": sup.instance_id(),
                    "message": format!("internal error: {e}"),
                }),
            };
        }
        other => {
            return json!({
                "status": "error",
                "message": format!("unknown func '{other}' (supported: start, stop, restart, status, rcon, steam_update, wipe)"),
            });
        }
    };

    match outcome {
        Ok(result) => json!({
            "status": "success",
            "func": func,
            "instance_id": sup.instance_id(),
            "result": result,
            "state": sup.state(),
        }),
        Err(e) => json!({
            "status": "error",
            "func": func,
            "instance_id": sup.instance_id(),
            "message": format!("{e:#}"),
        }),
    }
}
