//! Guidance tools — system status, doc search, next-action, troubleshooting, config validation.

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

// ── guidance_system_status ────────────────────────────────────────────────────

pub struct GuidanceSystemStatus {
    client: Arc<OrchestratorClient>,
}
impl GuidanceSystemStatus {
    pub fn new(client: Arc<OrchestratorClient>) -> Self { Self { client } }
}

#[async_trait]
impl Tool for GuidanceSystemStatus {
    fn name(&self) -> &'static str { "guidance_system_status" }
    fn description(&self) -> &'static str {
        "Check overall provisioning system health (orchestrator, vault, services)"
    }
    fn schema(&self) -> Value {
        json!({ "type": "object", "properties": {}, "additionalProperties": false })
    }
    fn category(&self) -> ToolCategory { ToolCategory::Read }

    async fn invoke(&self, _params: Value, _ctx: &Context) -> Result<Value, ToolError> {
        let orchestrator_ok = self.client.health().await.is_ok();
        let health = if orchestrator_ok {
            self.client.system_health().await.ok()
        } else {
            None
        };

        Ok(json!({
            "orchestrator": {
                "reachable": orchestrator_ok,
                "health": health,
            },
        }))
    }
}

// ── guidance_doc_find ─────────────────────────────────────────────────────────

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

#[async_trait]
impl Tool for GuidanceDocFind {
    fn name(&self) -> &'static str { "guidance_doc_find" }
    fn description(&self) -> &'static str {
        "Search documentation files by keyword under docs/ or .claude/"
    }
    fn schema(&self) -> Value {
        json!({
            "type": "object",
            "required": ["query"],
            "properties": {
                "query": { "type": "string" },
                "root": {
                    "type": "string",
                    "description": "Directory to search under (default: docs)",
                    "default": "docs"
                }
            }
        })
    }
    fn category(&self) -> ToolCategory { ToolCategory::Read }

    async fn invoke(&self, params: Value, ctx: &Context) -> Result<Value, ToolError> {
        let query = params["query"]
            .as_str()
            .ok_or_else(|| ToolError::invalid_param("query", "required string"))?
            .to_lowercase();
        let root_suffix = params["root"].as_str().unwrap_or("docs");

        // Anchor to workspaces_root's parent (project root)
        let project_root = ctx
            .env
            .workspaces_root
            .parent()
            .unwrap_or(&ctx.env.workspaces_root);
        let search_root = project_root.join(root_suffix);

        if !search_root.is_dir() {
            return Ok(json!({ "items": [], "total": 0, "query": query }));
        }

        let matches = search_markdown_files(&search_root, &query).await?;
        let total = matches.len();
        Ok(json!({ "items": matches, "total": total, "query": query }))
    }
}

async fn search_markdown_files(
    root: &std::path::Path,
    query: &str,
) -> Result<Vec<Value>, ToolError> {
    let mut results: Vec<Value> = Vec::new();
    let mut stack = vec![root.to_path_buf()];

    while let Some(dir) = stack.pop() {
        let mut rd = tokio::fs::read_dir(&dir)
            .await
            .map_err(|e| ToolError::invocation(e.to_string()))?;
        while let Some(entry) = rd
            .next_entry()
            .await
            .map_err(|e| ToolError::invocation(e.to_string()))?
        {
            let path = entry.path();
            if path.is_dir() {
                stack.push(path);
                continue;
            }
            if path.extension().and_then(|e| e.to_str()) != Some("md") {
                continue;
            }
            let name = path
                .file_stem()
                .and_then(|s| s.to_str())
                .unwrap_or("")
                .to_lowercase();
            if name.contains(query) || path.to_string_lossy().to_lowercase().contains(query) {
                results.push(json!({ "path": path.to_string_lossy() }));
            }
        }
    }

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

// ── guidance_next_action ──────────────────────────────────────────────────────

pub struct GuidanceNextAction {
    client: Arc<OrchestratorClient>,
}
impl GuidanceNextAction {
    pub fn new(client: Arc<OrchestratorClient>) -> Self { Self { client } }
}

#[async_trait]
impl Tool for GuidanceNextAction {
    fn name(&self) -> &'static str { "guidance_next_action" }
    fn description(&self) -> &'static str {
        "Suggest the next provisioning action based on current system state"
    }
    fn schema(&self) -> Value {
        json!({
            "type": "object",
            "properties": {
                "context": {
                    "type": "string",
                    "description": "Optional context hint (e.g. 'deploying cluster', 'onboarding')"
                }
            }
        })
    }
    fn category(&self) -> ToolCategory { ToolCategory::Read }

    async fn invoke(&self, params: Value, _ctx: &Context) -> Result<Value, ToolError> {
        let context_hint = params["context"].as_str().unwrap_or("");
        let orchestrator_ok = self.client.health().await.is_ok();

        let mut suggestions: Vec<Value> = Vec::new();

        if !orchestrator_ok {
            suggestions.push(json!({
                "action":   "start_orchestrator",
                "priority": "high",
                "reason":   "orchestrator is not reachable",
                "command":  "just start-orchestrator",
            }));
        } else {
            let tasks = self.client.list_tasks().await.unwrap_or_default();
            let running = tasks.iter().filter(|t| t.status.to_string() == "Running").count();
            let failed = tasks.iter().filter(|t| t.status.to_string() == "Failed").count();

            if failed > 0 {
                suggestions.push(json!({
                    "action":   "review_failed_jobs",
                    "priority": "high",
                    "reason":   format!("{failed} job(s) have failed"),
                    "command":  "provisioning orchestrator jobs --status Failed",
                }));
            }
            if running > 0 {
                suggestions.push(json!({
                    "action":   "monitor_running_jobs",
                    "priority": "medium",
                    "reason":   format!("{running} job(s) in progress"),
                    "command":  "provisioning orchestrator jobs --status Running",
                }));
            }
        }

        if context_hint.contains("cluster") {
            suggestions.push(json!({
                "action":   "validate_cluster_config",
                "priority": "medium",
                "reason":   "cluster deployment in progress",
                "command":  "provisioning workspace validate",
            }));
        }

        if suggestions.is_empty() {
            suggestions.push(json!({
                "action":   "check_workspace",
                "priority": "low",
                "reason":   "system appears healthy",
                "command":  "provisioning workspace list",
            }));
        }

        Ok(json!({
            "suggestions": suggestions,
            "orchestrator_healthy": orchestrator_ok,
        }))
    }
}

// ── guidance_troubleshoot ─────────────────────────────────────────────────────

pub struct GuidanceTroubleshoot {
    client: Arc<OrchestratorClient>,
    vault: Option<Arc<crate::sources::VaultSource>>,
}
impl GuidanceTroubleshoot {
    pub fn new(client: Arc<OrchestratorClient>) -> Self {
        Self { client, vault: None }
    }
    pub fn with_vault(mut self, vault: Arc<crate::sources::VaultSource>) -> Self {
        self.vault = Some(vault);
        self
    }
}

#[async_trait]
impl Tool for GuidanceTroubleshoot {
    fn name(&self) -> &'static str { "guidance_troubleshoot" }
    fn description(&self) -> &'static str {
        "Diagnose common provisioning issues (orchestrator, vault, config)"
    }
    fn schema(&self) -> Value {
        json!({ "type": "object", "properties": {}, "additionalProperties": false })
    }
    fn category(&self) -> ToolCategory { ToolCategory::Read }

    async fn invoke(&self, _params: Value, _ctx: &Context) -> Result<Value, ToolError> {
        let mut checks: Vec<Value> = Vec::new();

        // Orchestrator
        let orch_ok = self.client.health().await.is_ok();
        checks.push(json!({
            "service": "orchestrator",
            "ok":      orch_ok,
            "hint":    if orch_ok { null_value() } else { json!("run: just start-orchestrator") },
        }));

        // Vault
        if let Some(vault) = &self.vault {
            let vault_ok = vault.health().await.unwrap_or(false);
            checks.push(json!({
                "service": "vault",
                "ok":      vault_ok,
                "backend": vault.backend_name(),
                "hint":    if vault_ok { null_value() } else { json!("check vault credentials and backend config") },
            }));
        }

        // hcloud binary
        let hcloud_present = which_binary("hcloud");
        checks.push(json!({
            "service": "hcloud_cli",
            "ok":      hcloud_present,
            "hint":    if hcloud_present { null_value() } else { json!("install hcloud CLI: https://github.com/hetznercloud/cli") },
        }));

        // nickel binary
        let nickel_present = which_binary("nickel");
        checks.push(json!({
            "service": "nickel",
            "ok":      nickel_present,
            "hint":    if nickel_present { null_value() } else { json!("install nickel: https://nickel-lang.org") },
        }));

        let all_ok = checks.iter().all(|c| c["ok"].as_bool().unwrap_or(false));
        Ok(json!({ "all_ok": all_ok, "checks": checks }))
    }
}

fn null_value() -> Value { Value::Null }

fn which_binary(name: &str) -> bool {
    std::process::Command::new("which")
        .arg(name)
        .output()
        .map(|o| o.status.success())
        .unwrap_or(false)
}

// ── guidance_config_validate ──────────────────────────────────────────────────

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

#[async_trait]
impl Tool for GuidanceConfigValidate {
    fn name(&self) -> &'static str { "guidance_config_validate" }
    fn description(&self) -> &'static str {
        "Validate a Nickel config file or directory by running nickel export"
    }
    fn schema(&self) -> Value {
        json!({
            "type": "object",
            "required": ["path"],
            "properties": {
                "path": { "type": "string", "description": "Absolute path to .ncl file or directory" }
            }
        })
    }
    fn category(&self) -> ToolCategory { ToolCategory::Read }

    async fn invoke(&self, params: Value, _ctx: &Context) -> Result<Value, ToolError> {
        let raw_path = params["path"]
            .as_str()
            .ok_or_else(|| ToolError::invalid_param("path", "required string"))?;
        let path = std::path::PathBuf::from(raw_path);

        if path.is_file() {
            return validate_single(&self.cache, &path).await;
        }
        if path.is_dir() {
            return validate_directory(&self.cache, &path).await;
        }
        Err(ToolError::not_found(format!("path '{}' does not exist", path.display())))
    }
}

async fn validate_single(cache: &NclCache, path: &std::path::Path) -> Result<Value, ToolError> {
    match cache.get(path).await {
        Ok(_) => Ok(json!({ "valid": true, "path": path.to_string_lossy() })),
        Err(e) => Ok(json!({ "valid": false, "path": path.to_string_lossy(), "error": e.to_string() })),
    }
}

async fn validate_directory(cache: &NclCache, dir: &std::path::Path) -> Result<Value, ToolError> {
    let mut rd = tokio::fs::read_dir(dir)
        .await
        .map_err(|e| ToolError::invocation(e.to_string()))?;
    let mut results: Vec<Value> = Vec::new();
    while let Some(entry) = rd
        .next_entry()
        .await
        .map_err(|e| ToolError::invocation(e.to_string()))?
    {
        let p = entry.path();
        if p.extension().and_then(|e| e.to_str()) == Some("ncl") {
            results.push(validate_single(cache, &p).await?);
        }
    }
    let valid_count = results.iter().filter(|r| r["valid"].as_bool().unwrap_or(false)).count();
    Ok(json!({
        "valid":       valid_count == results.len(),
        "total":       results.len(),
        "valid_count": valid_count,
        "results":     results,
    }))
}
