//! Cluster domain tools — list, show, create.
//!
//! Clusters are discovered via filesystem: each workspace environment may contain
//! a `clusters/<name>.ncl` or a `servers.ncl` with cluster-typed entries.
//! Creation is enqueued as a `ClusterWorkflow` via the orchestrator.

use crate::protocol::{ToolCategory, ToolError};
use crate::sources::{NclCache, get_workspace, list_infra_envs};
use crate::tool::{Context, Tool};
use async_trait::async_trait;
use serde_json::{Value, json};
use std::sync::Arc;

// ── Cluster discovery ─────────────────────────────────────────────────────────

fn is_cluster_entry(val: &Value) -> bool {
    val.get("cluster_type").is_some()
        || val.get("role").and_then(|r| r.as_str()) == Some("cluster")
}

async fn collect_from_servers_ncl(
    cache: &NclCache,
    env_path: &std::path::Path,
    ws_name: &str,
    env: &str,
    out: &mut Vec<Value>,
) {
    let path = env_path.join("servers.ncl");
    if !path.exists() {
        return;
    }
    if let Ok(v) = cache.get(&path).await {
        if let Some(obj) = v.as_object() {
            for (key, val) in obj {
                if is_cluster_entry(val) {
                    out.push(json!({
                        "name":      key,
                        "workspace": ws_name,
                        "env":       env,
                        "source":    "servers.ncl",
                        "config":    val,
                    }));
                }
            }
        }
    }
}

/// Walk `<env_path>/clusters/*.ncl` for cluster definitions.
/// Falls back to reading `servers.ncl` and extracting entries whose NCL type
/// indicates a k0s/cluster role (field `cluster_type` present).
async fn discover_clusters(
    cache: &NclCache,
    ws_name: &str,
    env: &str,
    env_path: &std::path::Path,
) -> Result<Vec<Value>, ToolError> {
    let clusters_dir = env_path.join("clusters");
    let mut out: Vec<Value> = Vec::new();

    if clusters_dir.is_dir() {
        let mut dir = tokio::fs::read_dir(&clusters_dir)
            .await
            .map_err(|e| ToolError::invocation(e.to_string()))?;
        while let Some(entry) = dir
            .next_entry()
            .await
            .map_err(|e| ToolError::invocation(e.to_string()))?
        {
            let path = entry.path();
            if path.extension().and_then(|e| e.to_str()) != Some("ncl") {
                continue;
            }
            let name = path
                .file_stem()
                .and_then(|s| s.to_str())
                .unwrap_or("")
                .to_owned();
            if name.starts_with('_') || name.starts_with('.') {
                continue;
            }
            let config = cache.get(&path).await.ok().map(|v| (*v).clone());
            out.push(json!({
                "name":      name,
                "workspace": ws_name,
                "env":       env,
                "path":      path.to_string_lossy(),
                "config":    config,
            }));
        }
    }

    // Also inspect servers.ncl for cluster_type entries when no clusters/ dir
    if out.is_empty() {
        collect_from_servers_ncl(cache, env_path, ws_name, env, &mut out).await;
    }

    out.sort_by(|a, b| {
        a["name"].as_str().cmp(&b["name"].as_str())
    });
    Ok(out)
}

// ── cluster_list ──────────────────────────────────────────────────────────────

pub struct ClusterList {
    cache: Arc<NclCache>,
}
impl ClusterList {
    pub fn new(cache: Arc<NclCache>) -> Self { Self { cache } }
}

#[async_trait]
impl Tool for ClusterList {
    fn name(&self) -> &'static str { "cluster_list" }
    fn description(&self) -> &'static str { "List clusters defined in a workspace environment" }
    fn schema(&self) -> Value {
        json!({
            "type": "object",
            "required": ["workspace"],
            "properties": {
                "workspace": { "type": "string" },
                "env": { "type": "string", "description": "Filter to a specific environment" }
            }
        })
    }
    fn category(&self) -> ToolCategory { ToolCategory::Read }

    async fn invoke(&self, params: Value, ctx: &Context) -> Result<Value, ToolError> {
        let ws_name = params["workspace"]
            .as_str()
            .ok_or_else(|| ToolError::invalid_param("workspace", "required string"))?;
        let env_filter = params["env"].as_str();

        let ws = get_workspace(&ctx.env.workspaces_root, ws_name).await?;
        let envs = list_infra_envs(ws_name, &ws.path).await?;

        let mut items: Vec<Value> = Vec::new();
        for env in &envs {
            if let Some(f) = env_filter {
                if env.environment != f {
                    continue;
                }
            }
            let clusters =
                discover_clusters(&self.cache, ws_name, &env.environment, &env.path).await?;
            items.extend(clusters);
        }

        Ok(json!({ "items": items, "total": items.len() }))
    }
}

// ── cluster_show ──────────────────────────────────────────────────────────────

pub struct ClusterShow {
    cache: Arc<NclCache>,
}
impl ClusterShow {
    pub fn new(cache: Arc<NclCache>) -> Self { Self { cache } }
}

#[async_trait]
impl Tool for ClusterShow {
    fn name(&self) -> &'static str { "cluster_show" }
    fn description(&self) -> &'static str { "Show NCL config for a specific cluster" }
    fn schema(&self) -> Value {
        json!({
            "type": "object",
            "required": ["workspace", "env", "name"],
            "properties": {
                "workspace": { "type": "string" },
                "env":       { "type": "string" },
                "name":      { "type": "string" }
            }
        })
    }
    fn category(&self) -> ToolCategory { ToolCategory::Read }

    async fn invoke(&self, params: Value, ctx: &Context) -> Result<Value, ToolError> {
        let ws_name = params["workspace"]
            .as_str()
            .ok_or_else(|| ToolError::invalid_param("workspace", "required string"))?;
        let env_name = params["env"]
            .as_str()
            .ok_or_else(|| ToolError::invalid_param("env", "required string"))?;
        let cluster_name = params["name"]
            .as_str()
            .ok_or_else(|| ToolError::invalid_param("name", "required string"))?;

        let ws = get_workspace(&ctx.env.workspaces_root, ws_name).await?;
        let env_path = ws.path.join("infra").join(env_name);

        let ncl_path = env_path.join("clusters").join(format!("{cluster_name}.ncl"));
        if ncl_path.exists() {
            let config = self.cache.get(&ncl_path).await?;
            return Ok(json!({
                "name":      cluster_name,
                "workspace": ws_name,
                "env":       env_name,
                "config":    (*config).clone(),
            }));
        }

        // Fallback: check servers.ncl for a cluster_type entry
        let servers_path = env_path.join("servers.ncl");
        if servers_path.exists() {
            if let Ok(v) = self.cache.get(&servers_path).await {
                if let Some(entry) = v.as_object().and_then(|o| o.get(cluster_name)) {
                    if entry.get("cluster_type").is_some() {
                        return Ok(json!({
                            "name":      cluster_name,
                            "workspace": ws_name,
                            "env":       env_name,
                            "source":    "servers.ncl",
                            "config":    entry,
                        }));
                    }
                }
            }
        }

        Err(ToolError::not_found(format!("cluster '{cluster_name}' in {ws_name}/{env_name}")))
    }
}

// ── cluster_create ────────────────────────────────────────────────────────────

pub struct ClusterCreate;
impl ClusterCreate {
    pub fn new() -> Self { Self }
}
impl Default for ClusterCreate {
    fn default() -> Self { Self::new() }
}

#[async_trait]
impl Tool for ClusterCreate {
    fn name(&self) -> &'static str { "cluster_create" }
    fn description(&self) -> &'static str {
        "Enqueue a cluster creation workflow via the orchestrator"
    }
    fn schema(&self) -> Value {
        json!({
            "type": "object",
            "required": ["infra", "settings", "cluster_type"],
            "properties": {
                "infra":        { "type": "string", "description": "Infrastructure name" },
                "settings":     { "type": "string", "description": "Settings NCL path" },
                "cluster_type": {
                    "type": "string",
                    "enum": ["k0s", "k3s", "rke2"],
                    "description": "Cluster distribution to provision"
                },
                "operation":  { "type": "string", "enum": ["create", "delete"], "default": "create" },
                "check_mode": { "type": "boolean", "default": false },
                "wait":       { "type": "boolean", "default": true }
            }
        })
    }
    fn category(&self) -> ToolCategory { ToolCategory::Mutation }

    async fn invoke(&self, params: Value, ctx: &Context) -> Result<Value, ToolError> {
        let infra = params["infra"]
            .as_str()
            .ok_or_else(|| ToolError::invalid_param("infra", "required string"))?;
        let settings = params["settings"]
            .as_str()
            .ok_or_else(|| ToolError::invalid_param("settings", "required string"))?;
        let cluster_type = params["cluster_type"]
            .as_str()
            .ok_or_else(|| ToolError::invalid_param("cluster_type", "required string"))?;
        let operation = params["operation"].as_str().unwrap_or("create");
        let check_mode = params["check_mode"].as_bool().unwrap_or(false);
        let wait = params["wait"].as_bool().unwrap_or(true);

        let body = json!({
            "infra":        infra,
            "settings":     settings,
            "cluster_type": cluster_type,
            "operation":    operation,
            "check_mode":   check_mode,
            "wait":         wait,
        });

        let client = reqwest::Client::builder()
            .timeout(std::time::Duration::from_secs(10))
            .build()
            .map_err(|e| ToolError::invocation(format!("http client: {e}")))?;

        let url = format!("{}/workflows/cluster/create", ctx.env.orchestrator_url);
        let resp = client
            .post(&url)
            .json(&body)
            .send()
            .await
            .map_err(|e| ToolError::invocation(format!("orchestrator unreachable: {e}")))?;

        let status = resp.status();
        if !status.is_success() {
            return Err(ToolError::invocation(format!(
                "orchestrator returned {status} for cluster {operation}"
            )));
        }

        let result: Value = resp
            .json()
            .await
            .map_err(|e| ToolError::invocation(format!("parse response: {e}")))?;

        Ok(json!({
            "infra":        infra,
            "cluster_type": cluster_type,
            "operation":    operation,
            "task_id":      result["data"],
            "status":       "queued",
        }))
    }
}

// ── cluster_pods ──────────────────────────────────────────────────────────────

pub struct ClusterPods;
impl ClusterPods {
    pub fn new() -> Self { Self }
}
impl Default for ClusterPods {
    fn default() -> Self { Self::new() }
}

#[async_trait]
impl Tool for ClusterPods {
    fn name(&self) -> &'static str { "cluster_pods" }
    fn description(&self) -> &'static str {
        "Fetch pod JSON from a k0s/k8s cluster controller via SSH (`k0s kubectl get pods -A -o json`)"
    }
    fn schema(&self) -> Value {
        json!({
            "type": "object",
            "required": ["host"],
            "properties": {
                "host": { "type": "string", "description": "SSH target IP or hostname" },
                "user": { "type": "string", "description": "SSH user (default: root)" },
                "key":  { "type": "string", "description": "Path to SSH private key (expanded)" }
            }
        })
    }
    fn category(&self) -> ToolCategory { ToolCategory::Read }

    async fn invoke(&self, params: Value, _ctx: &Context) -> Result<Value, ToolError> {
        let host = params["host"]
            .as_str()
            .ok_or_else(|| ToolError::invalid_param("host", "required string"))?;
        let user = params["user"].as_str().unwrap_or("root");
        let key  = params["key"].as_str();

        let mut ssh_args: Vec<String> = vec![
            "-o".into(), "BatchMode=yes".into(),
            "-o".into(), "ConnectTimeout=10".into(),
            "-o".into(), "StrictHostKeyChecking=no".into(),
            "-o".into(), "UserKnownHostsFile=/dev/null".into(),
        ];
        if let Some(k) = key {
            ssh_args.extend(["-i".into(), k.into()]);
        }
        ssh_args.push(format!("{user}@{host}"));
        ssh_args.push("k0s kubectl get pods --all-namespaces -o json".into());

        let output = tokio::process::Command::new("ssh")
            .args(&ssh_args)
            .output()
            .await
            .map_err(|e| ToolError::invocation(format!("ssh exec: {e}")))?;

        if !output.status.success() {
            let stderr = String::from_utf8_lossy(&output.stderr);
            return Err(ToolError::invocation(format!("kubectl exit {}: {stderr}", output.status)));
        }

        let stdout = String::from_utf8_lossy(&output.stdout);
        let pods: Value = serde_json::from_str(&stdout)
            .map_err(|e| ToolError::invocation(format!("parse pods JSON: {e}")))?;

        let items = pods["items"].as_array().cloned().unwrap_or_default();
        let total = items.len();
        Ok(json!({ "items": items, "total": total }))
    }
}

// ── cluster_describe_pod ──────────────────────────────────────────────────────

pub struct ClusterDescribePod;
impl ClusterDescribePod {
    pub fn new() -> Self { Self }
}
impl Default for ClusterDescribePod {
    fn default() -> Self { Self::new() }
}

#[async_trait]
impl Tool for ClusterDescribePod {
    fn name(&self) -> &'static str { "cluster_describe_pod" }
    fn description(&self) -> &'static str {
        "Run `k0s kubectl describe pod -n <ns> <pod>` on a cluster controller via SSH"
    }
    fn schema(&self) -> Value {
        json!({
            "type": "object",
            "required": ["host", "namespace", "pod"],
            "properties": {
                "host":      { "type": "string" },
                "namespace": { "type": "string" },
                "pod":       { "type": "string" },
                "user":      { "type": "string" },
                "key":       { "type": "string", "description": "Path to SSH private key (expanded)" }
            }
        })
    }
    fn category(&self) -> ToolCategory { ToolCategory::Read }

    async fn invoke(&self, params: Value, _ctx: &Context) -> Result<Value, ToolError> {
        let host = params["host"].as_str()
            .ok_or_else(|| ToolError::invalid_param("host", "required string"))?;
        let ns = params["namespace"].as_str()
            .ok_or_else(|| ToolError::invalid_param("namespace", "required string"))?;
        let pod = params["pod"].as_str()
            .ok_or_else(|| ToolError::invalid_param("pod", "required string"))?;
        let user = params["user"].as_str().unwrap_or("root");
        let key  = params["key"].as_str();

        let cmd = format!("k0s kubectl describe pod -n {ns} {pod}");
        let mut ssh_args: Vec<String> = vec![
            "-o".into(), "BatchMode=yes".into(),
            "-o".into(), "ConnectTimeout=10".into(),
            "-o".into(), "StrictHostKeyChecking=no".into(),
            "-o".into(), "UserKnownHostsFile=/dev/null".into(),
        ];
        if let Some(k) = key {
            ssh_args.extend(["-i".into(), k.into()]);
        }
        ssh_args.push(format!("{user}@{host}"));
        ssh_args.push(cmd);

        let output = tokio::process::Command::new("ssh")
            .args(&ssh_args)
            .output()
            .await
            .map_err(|e| ToolError::invocation(format!("ssh exec: {e}")))?;

        let text = if output.status.success() {
            String::from_utf8_lossy(&output.stdout).into_owned()
        } else {
            let stderr = String::from_utf8_lossy(&output.stderr).into_owned();
            return Err(ToolError::invocation(format!("describe failed: {stderr}")));
        };
        Ok(json!({ "output": text, "pod": pod, "namespace": ns }))
    }
}
