//! Extension domain tools — list, show, search, capabilities.
//!
//! Extensions are discovered from `Environment::extensions_root/components/`
//! and `extensions_root/taskservs/` by reading `metadata.ncl` files.
//! The extension-registry HTTP service (port 8084) is intentionally NOT used
//! here — filesystem discovery is authoritative for the local repo state.

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

// ── Shared discovery ──────────────────────────────────────────────────────────

/// Walk `root` (non-recursively) for subdirs that contain a `metadata.ncl`.
/// Returns `(name, metadata_value)` pairs sorted by name.
async fn discover_metadata(
    cache: &NclCache,
    root: &std::path::Path,
) -> Result<Vec<(String, Value)>, ToolError> {
    if !root.is_dir() {
        return Ok(Vec::new());
    }
    let mut dir = tokio::fs::read_dir(root)
        .await
        .map_err(|e| ToolError::invocation(e.to_string()))?;
    let mut results = Vec::new();
    while let Some(entry) = dir
        .next_entry()
        .await
        .map_err(|e| ToolError::invocation(e.to_string()))?
    {
        let path = entry.path();
        if !path.is_dir() {
            continue;
        }
        let name = path
            .file_name()
            .and_then(|n| n.to_str())
            .unwrap_or("")
            .to_owned();
        if name.starts_with('_') || name.starts_with('.') {
            continue;
        }
        let meta_path = path.join("metadata.ncl");
        if !meta_path.exists() {
            continue;
        }
        if let Ok(v) = cache.get(&meta_path).await {
            results.push((name, (*v).clone()));
        }
    }
    results.sort_by(|a, b| a.0.cmp(&b.0));
    Ok(results)
}

fn matches_query(meta: &Value, query: &str) -> bool {
    let q = query.to_lowercase();
    let name = meta["name"].as_str().unwrap_or("").to_lowercase();
    let desc = meta["description"].as_str().unwrap_or("").to_lowercase();
    let tags: Vec<&str> = meta["tags"]
        .as_array()
        .map(|a| a.iter().filter_map(|v| v.as_str()).collect())
        .unwrap_or_default();
    name.contains(&q) || desc.contains(&q) || tags.iter().any(|t| t.contains(&q))
}

// ── extension_list ────────────────────────────────────────────────────────────

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

#[async_trait]
impl Tool for ExtensionList {
    fn name(&self) -> &'static str { "extension_list" }
    fn description(&self) -> &'static str { "List all available extensions (components and taskservs)" }
    fn schema(&self) -> Value {
        json!({
            "type": "object",
            "properties": {
                "kind": { "type": "string", "enum": ["component", "taskserv", "all"], "default": "all" }
            }
        })
    }
    fn category(&self) -> ToolCategory { ToolCategory::Read }

    async fn invoke(&self, params: Value, ctx: &Context) -> Result<Value, ToolError> {
        let kind = params["kind"].as_str().unwrap_or("all");
        let root = &ctx.env.extensions_root;
        let mut items: Vec<Value> = Vec::new();

        if kind == "component" || kind == "all" {
            for (name, meta) in discover_metadata(&self.cache, &root.join("components")).await? {
                let mut m = meta;
                m["_kind"] = json!("component");
                m["_name"] = json!(name);
                items.push(m);
            }
        }
        if kind == "taskserv" || kind == "all" {
            // taskservs are organised in subdirs by category; walk two levels
            let ts_root = root.join("taskservs");
            if ts_root.is_dir() {
                let mut cat_dir = tokio::fs::read_dir(&ts_root)
                    .await
                    .map_err(|e| ToolError::invocation(e.to_string()))?;
                while let Some(cat) = cat_dir
                    .next_entry()
                    .await
                    .map_err(|e| ToolError::invocation(e.to_string()))?
                {
                    if !cat.path().is_dir() { continue; }
                    for (name, mut meta) in
                        discover_metadata(&self.cache, &cat.path()).await?
                    {
                        meta["_kind"] = json!("taskserv");
                        meta["_name"] = json!(name);
                        items.push(meta);
                    }
                }
                items.sort_by(|a, b| {
                    a["_name"].as_str().cmp(&b["_name"].as_str())
                });
            }
        }

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

// ── extension_show ────────────────────────────────────────────────────────────

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

#[async_trait]
impl Tool for ExtensionShow {
    fn name(&self) -> &'static str { "extension_show" }
    fn description(&self) -> &'static str { "Show metadata for a specific extension" }
    fn schema(&self) -> Value {
        json!({
            "type": "object",
            "required": ["name"],
            "properties": {
                "name": { "type": "string" },
                "kind": { "type": "string", "enum": ["component", "taskserv"], "default": "component" }
            }
        })
    }
    fn category(&self) -> ToolCategory { ToolCategory::Read }

    async fn invoke(&self, params: Value, ctx: &Context) -> Result<Value, ToolError> {
        let name = params["name"]
            .as_str()
            .ok_or_else(|| ToolError::invalid_param("name", "required string"))?;
        let kind = params["kind"].as_str().unwrap_or("component");
        let root = &ctx.env.extensions_root;

        let meta_path = match kind {
            "taskserv" => {
                // search in all taskserv category subdirs
                find_taskserv_metadata(root, name).await?
            }
            _ => root.join("components").join(name).join("metadata.ncl"),
        };

        if !meta_path.exists() {
            return Err(ToolError::not_found(format!("extension '{name}'")));
        }
        let meta = self.cache.get(&meta_path).await?;
        Ok((*meta).clone())
    }
}

async fn find_taskserv_metadata(
    root: &std::path::Path,
    name: &str,
) -> Result<std::path::PathBuf, ToolError> {
    let ts_root = root.join("taskservs");
    if !ts_root.is_dir() {
        return Err(ToolError::not_found(format!("taskserv '{name}'")));
    }
    let mut cat_dir = tokio::fs::read_dir(&ts_root)
        .await
        .map_err(|e| ToolError::invocation(e.to_string()))?;
    while let Some(cat) = cat_dir
        .next_entry()
        .await
        .map_err(|e| ToolError::invocation(e.to_string()))?
    {
        let candidate = cat.path().join(name).join("metadata.ncl");
        if candidate.exists() {
            return Ok(candidate);
        }
    }
    Err(ToolError::not_found(format!("taskserv '{name}'")))
}

// ── extension_search ──────────────────────────────────────────────────────────

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

#[async_trait]
impl Tool for ExtensionSearch {
    fn name(&self) -> &'static str { "extension_search" }
    fn description(&self) -> &'static str { "Search extensions by name, description, or tag" }
    fn schema(&self) -> Value {
        json!({
            "type": "object",
            "required": ["query"],
            "properties": {
                "query": { "type": "string" }
            }
        })
    }
    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"))?;
        let root = &ctx.env.extensions_root;

        let all = discover_metadata(&self.cache, &root.join("components")).await?;
        let matched: Vec<Value> = all
            .into_iter()
            .filter(|(_, meta)| matches_query(meta, query))
            .map(|(_, meta)| meta)
            .collect();

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

// ── extension_capabilities ────────────────────────────────────────────────────

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

#[async_trait]
impl Tool for ExtensionCapabilities {
    fn name(&self) -> &'static str { "extension_capabilities" }
    fn description(&self) -> &'static str { "Aggregate provides/requires/conflicts across all components" }
    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 root = &ctx.env.extensions_root;
        let all = discover_metadata(&self.cache, &root.join("components")).await?;

        let mut provides: Vec<Value> = Vec::new();
        let mut requires: Vec<Value> = Vec::new();
        let mut conflicts: Vec<Value> = Vec::new();

        for (name, meta) in &all {
            let wrap = |v: &Value| json!({ "extension": name, "entry": v });
            if let Some(arr) = meta["provides"].as_array() {
                provides.extend(arr.iter().map(wrap));
            }
            if let Some(arr) = meta["requires"].as_array() {
                requires.extend(arr.iter().map(wrap));
            }
            if let Some(arr) = meta["conflicts_with"].as_array() {
                conflicts.extend(arr.iter().map(wrap));
            }
        }

        Ok(json!({
            "total_extensions": all.len(),
            "provides": provides,
            "requires": requires,
            "conflicts_with": conflicts,
        }))
    }
}
