//! Infrastructure tools — status, technology detection, config validation, reconcile.

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

// ── infra_status ──────────────────────────────────────────────────────────────

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

#[async_trait]
impl Tool for InfraStatus {
    fn name(&self) -> &'static str { "infra_status" }
    fn description(&self) -> &'static str {
        "Overall infrastructure status: orchestrator health + workspace count"
    }
    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 metrics = if orchestrator_ok {
            self.client.system_metrics().await.ok()
        } else {
            None
        };
        let workspaces = list_workspaces(&ctx.env.workspaces_root)
            .await
            .unwrap_or_default();

        Ok(json!({
            "orchestrator": { "reachable": orchestrator_ok, "metrics": metrics },
            "workspaces":   { "count": workspaces.len(), "names": workspaces.iter().map(|w| &w.name).collect::<Vec<_>>() },
        }))
    }
}

// ── infra_detect ──────────────────────────────────────────────────────────────

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

/// Technology indicators: (file/dir pattern, technology name, confidence)
const TECH_INDICATORS: &[(&str, &str, f32)] = &[
    ("Cargo.toml", "rust", 0.95),
    ("go.mod", "go", 0.95),
    ("package.json", "nodejs", 0.90),
    ("requirements.txt", "python", 0.85),
    ("pyproject.toml", "python", 0.90),
    ("Dockerfile", "docker", 0.90),
    ("docker-compose.yml", "docker-compose", 0.90),
    ("docker-compose.yaml", "docker-compose", 0.90),
    ("*.ncl", "nickel-iac", 0.80),
    ("*.nu", "nushell", 0.80),
    ("justfile", "just", 0.75),
    ("flake.nix", "nix", 0.90),
    ("Chart.yaml", "helm", 0.90),
    ("kustomization.yaml", "kustomize", 0.90),
    (".terraform", "terraform", 0.90),
];

#[async_trait]
impl Tool for InfraDetect {
    fn name(&self) -> &'static str { "infra_detect" }
    fn description(&self) -> &'static str {
        "Detect technologies and IaC tooling present in a directory"
    }
    fn schema(&self) -> Value {
        json!({
            "type": "object",
            "properties": {
                "path": {
                    "type": "string",
                    "description": "Directory to inspect (defaults to workspaces_root parent)"
                }
            }
        })
    }
    fn category(&self) -> ToolCategory { ToolCategory::Read }

    async fn invoke(&self, params: Value, ctx: &Context) -> Result<Value, ToolError> {
        let root = if let Some(p) = params["path"].as_str() {
            std::path::PathBuf::from(p)
        } else {
            ctx.env
                .workspaces_root
                .parent()
                .unwrap_or(&ctx.env.workspaces_root)
                .to_path_buf()
        };

        if !root.is_dir() {
            return Err(ToolError::not_found(format!("directory '{}'", root.display())));
        }

        let mut detections: Vec<Value> = Vec::new();
        for (pattern, tech, confidence) in TECH_INDICATORS {
            let found = if pattern.contains('*') {
                // Glob-style: check if any matching file exists
                let ext = pattern.trim_start_matches("*.");
                dir_contains_extension(&root, ext).await
            } else {
                root.join(pattern).exists()
            };
            if found {
                detections.push(json!({
                    "technology": tech,
                    "confidence": confidence,
                    "indicator":  pattern,
                }));
            }
        }

        Ok(json!({
            "root":       root.to_string_lossy(),
            "detections": detections,
            "total":      detections.len(),
        }))
    }
}

async fn dir_contains_extension(dir: &std::path::Path, ext: &str) -> bool {
    let Ok(mut rd) = tokio::fs::read_dir(dir).await else { return false };
    while let Ok(Some(entry)) = rd.next_entry().await {
        if entry.path().extension().and_then(|e| e.to_str()) == Some(ext) {
            return true;
        }
    }
    false
}

// ── infra_validate ────────────────────────────────────────────────────────────

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

#[async_trait]
impl Tool for InfraValidate {
    fn name(&self) -> &'static str { "infra_validate" }
    fn description(&self) -> &'static str {
        "Validate all NCL config files in a workspace environment"
    }
    fn schema(&self) -> Value {
        json!({
            "type": "object",
            "required": ["workspace", "env"],
            "properties": {
                "workspace": { "type": "string" },
                "env":       { "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 env_path = ctx.env.workspaces_root.join(ws_name).join("infra").join(env_name);
        if !env_path.is_dir() {
            return Err(ToolError::not_found(format!(
                "workspace env '{ws_name}/{env_name}'"
            )));
        }

        let mut results: Vec<Value> = Vec::new();
        let mut rd = tokio::fs::read_dir(&env_path)
            .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.extension().and_then(|e| e.to_str()) != Some("ncl") {
                continue;
            }
            let r = match self.cache.get(&path).await {
                Ok(_) => json!({ "path": path.to_string_lossy(), "valid": true }),
                Err(e) => json!({ "path": path.to_string_lossy(), "valid": false, "error": e.to_string() }),
            };
            results.push(r);
        }

        let valid = results.iter().filter(|r| r["valid"].as_bool().unwrap_or(false)).count();
        Ok(json!({
            "workspace":   ws_name,
            "env":         env_name,
            "valid":       valid == results.len(),
            "valid_count": valid,
            "total":       results.len(),
            "results":     results,
        }))
    }
}

// ── infra_reconcile ───────────────────────────────────────────────────────────

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

#[async_trait]
impl Tool for InfraReconcile {
    fn name(&self) -> &'static str { "infra_reconcile" }
    fn description(&self) -> &'static str {
        "Trigger infrastructure reconciliation via POST /api/v1/infra/reconcile"
    }
    fn schema(&self) -> Value {
        json!({
            "type": "object",
            "required": ["workspace"],
            "properties": {
                "workspace": { "type": "string" },
                "dry_run": { "type": "boolean", "default": false }
            }
        })
    }
    fn category(&self) -> ToolCategory { ToolCategory::Mutation }

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

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

        let body = json!({ "workspace": workspace, "dry_run": dry_run });
        let url = format!("{}/api/v1/infra/reconcile", 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 infra reconcile"
            )));
        }

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

        Ok(json!({
            "workspace": workspace,
            "dry_run":   dry_run,
            "result":    result,
        }))
    }
}
