use crate::command::{self, Spec, run_capture};
use crate::nix_config::{NIX_BUILD_EXECUTABLE, SPINDLE_RUN_DIR};
use crate::protocol::{self, Message, v1};
use anyhow::{Context, Result};
use serde::Deserialize;
use serde_json::Value;
use std::collections::BTreeMap;
use std::fs;
use std::time::Duration;
use tokio::sync::mpsc::Sender;
use tracing::info;

const USER_CONFIG_DIR: &str = "/run/spindle/user-config";

pub async fn run(id: String, req: v1::ActivateConfig, out: Sender<Message>) {
    let config_key = req.config_key.clone();
    let result = activate(&req).await;
    let msg = Message {
        id,
        activate_config_result: Some(v1::ActivateConfigResult {
            config_key,
            toplevel: result.as_ref().cloned().unwrap_or_default(),
            error: protocol::error_or_empty(result.err().map(|e| format!("{e:#}"))),
        }),
        ..Default::default()
    };
    let _ = out.send(msg).await;
}

async fn activate(req: &v1::ActivateConfig) -> Result<String> {
    let need_build = req.toplevel.is_empty();
    let timeout = if req.timeout_seconds > 0 {
        Duration::from_secs(u64::from(req.timeout_seconds))
    } else if need_build {
        Duration::from_secs(10 * 60)
    } else {
        Duration::from_secs(2 * 60)
    };

    let toplevel = if need_build {
        build_toplevel(req, timeout).await?
    } else {
        realise_toplevel(&req.toplevel, timeout).await?;
        req.toplevel.clone()
    };

    if !toplevel.starts_with("/nix/store/") {
        anyhow::bail!("config toplevel {toplevel:?} is not a nix store path");
    }

    switch_to_configuration(&toplevel, timeout).await?;
    info!(
        config_key = %req.config_key,
        base_config_hash = %req.base_config_hash,
        %toplevel,
        "activated NixOS config"
    );
    Ok(toplevel)
}

// mirrors the shape consumed by nix/microvm/user-config.nix; parsed here only
// to reject malformed configs before handing them to a root nix-build
#[derive(Debug, Deserialize)]
#[allow(dead_code)]
struct UserConfig {
    #[serde(default)]
    services: BTreeMap<String, Value>,
    #[serde(default)]
    virtualisation: BTreeMap<String, Value>,
    #[serde(default)]
    dependencies: Vec<String>,
    #[serde(default)]
    registry: BTreeMap<String, Value>,
}

async fn build_toplevel(req: &v1::ActivateConfig, timeout: Duration) -> Result<String> {
    let user_config = if req.user_config.is_empty() {
        "{}"
    } else {
        let _: UserConfig = serde_json::from_str(&req.user_config).context("parse user config")?;
        req.user_config.as_str()
    };

    info!("writing user config to {USER_CONFIG_DIR}/config.json");
    write_user_config(user_config).context("write user config")?;

    info!("running nix-build command for user config toplevel...");
    let output = run_capture(
        Spec::new(NIX_BUILD_EXECUTABLE)
            .args([
                "--no-link",
                "--show-trace",
                "-A",
                "system",
                "/etc/spindle/nixos/default.nix",
            ])
            .cwd(SPINDLE_RUN_DIR)
            .timeout(timeout),
    )
    .await?;
    if !output.success() {
        anyhow::bail!(
            "nix config build failed: exit={} error={:?} output={}",
            output.exit.exit_code,
            output.exit.error,
            output.combined_lossy(),
        );
    }

    let toplevel = String::from_utf8_lossy(&output.stdout)
        .lines()
        .last()
        .unwrap_or_default()
        .trim()
        .to_owned();
    if toplevel.is_empty() {
        anyhow::bail!("nix config build did not print a toplevel path");
    }
    Ok(toplevel)
}

fn write_user_config(user_config: &str) -> Result<()> {
    fs::create_dir_all(USER_CONFIG_DIR).with_context(|| format!("create {USER_CONFIG_DIR}"))?;

    let config_path = format!("{USER_CONFIG_DIR}/config.json");
    fs::write(&config_path, user_config).with_context(|| format!("write {config_path}"))?;
    Ok(())
}

async fn realise_toplevel(toplevel: &str, timeout: Duration) -> Result<()> {
    if !toplevel.starts_with("/nix/store/") {
        anyhow::bail!("cached config toplevel {toplevel:?} is not a nix store path");
    }
    let output = command::run_capture(
        Spec::new(crate::nix_config::NIX_STORE_EXECUTABLE)
            .args(["--realise", toplevel])
            .timeout(timeout),
    )
    .await?;
    if !output.success() {
        anyhow::bail!(
            "realise cached config failed: exit={} error={:?} output={}",
            output.exit.exit_code,
            output.exit.error,
            output.combined_lossy(),
        );
    }
    Ok(())
}

async fn switch_to_configuration(toplevel: &str, timeout: Duration) -> Result<()> {
    info!("switching to new configuration: {}", toplevel);
    let switch = format!("{toplevel}/bin/switch-to-configuration");
    let output = run_capture(Spec::new(switch).args(["test"]).timeout(timeout)).await?;
    if !output.success() {
        anyhow::bail!(
            "switch-to-configuration test failed: exit={} error={:?} output={}",
            output.exit.exit_code,
            output.exit.error,
            output.combined_lossy(),
        );
    }
    Ok(())
}
