use anyhow::{Context as _, Result};
use serde_json::{json, Value};
use std::path::Path;

use crate::AppState;
use super::error::UiError;

// ── Tool invocation ───────────────────────────────────────────────────────────

pub async fn invoke_tool(app: &AppState, name: &str, params: Value) -> Result<Value, UiError> {
    let ctx = provisioning_core::tool::Context::new(app.env.clone());
    app.registry
        .invoke(name, params, &ctx)
        .await
        .map_err(|e| UiError::Tool(format!("{name}: {e}")))
}

// ── Nickel export ─────────────────────────────────────────────────────────────

pub async fn ncl_export(path: &Path, import_path: Option<&str>) -> Value {
    let mut cmd = tokio::process::Command::new("nickel");
    cmd.arg("export").arg(path);
    if let Some(ip) = import_path {
        cmd.env("NICKEL_IMPORT_PATH", ip);
    }
    match cmd.output().await {
        Ok(out) if out.status.success() => {
            serde_json::from_slice(&out.stdout)
                .unwrap_or_else(|_| json!({"nodes": [], "edges": []}))
        }
        Ok(out) => {
            let err = String::from_utf8_lossy(&out.stderr);
            tracing::warn!(path = %path.display(), %err, "nickel export failed");
            json!({"nodes": [], "edges": []})
        }
        Err(e) => {
            tracing::warn!(path = %path.display(), error = %e, "nickel export spawn failed");
            json!({"nodes": [], "edges": []})
        }
    }
}

// ── Reflection modes ──────────────────────────────────────────────────────────

#[derive(Debug, serde::Serialize)]
pub struct ModeEntry {
    pub id: String,
    pub path: String,
}

pub fn list_modes(project_root: &Path) -> Vec<ModeEntry> {
    let modes_dir = project_root.join("reflection").join("modes");
    let Ok(entries) = std::fs::read_dir(&modes_dir) else {
        return vec![];
    };
    let mut modes: Vec<ModeEntry> = entries
        .filter_map(|e| e.ok())
        .filter(|e| e.path().extension().map(|x| x == "ncl").unwrap_or(false))
        .map(|e| {
            let path = e.path();
            let id = path
                .file_stem()
                .unwrap_or_default()
                .to_string_lossy()
                .into_owned();
            ModeEntry { id, path: path.display().to_string() }
        })
        .collect();
    modes.sort_by(|a, b| a.id.cmp(&b.id));
    modes
}

// ── Backlog ───────────────────────────────────────────────────────────────────

pub async fn list_backlog(backlog_ncl: &Path, import_path: Option<&str>) -> Value {
    if !backlog_ncl.exists() {
        return json!({"items": []});
    }
    let v = ncl_export(backlog_ncl, import_path).await;
    if v["items"].is_array() { v } else { json!({"items": []}) }
}

// ── Mode runner ───────────────────────────────────────────────────────────────

pub async fn run_mode(project_root: &Path, mode_id: &str, provisioning_bin: &str) -> Result<()> {
    tokio::process::Command::new(provisioning_bin)
        .args(["run", mode_id])
        .current_dir(project_root)
        .spawn()
        .with_context(|| format!("spawn {provisioning_bin} run {mode_id}"))?;
    Ok(())
}
