//! Ontology domain tools — search, node, graph.
//!
//! Reads the project-level ontology from `Environment::ontology_root`
//! (`core.ncl`, `state.ncl`, `manifest.ncl`). Returns provisioning's own
//! domain ontology — not ontoref-daemon's cross-project graph.

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 helpers ────────────────────────────────────────────────────────────

async fn load_core(cache: &NclCache, ctx: &Context) -> Result<Value, ToolError> {
    let path = ctx.env.ontology_root.join("core.ncl");
    if !path.exists() {
        return Err(ToolError::not_found(format!(
            "ontology core.ncl not found at {}",
            ctx.env.ontology_root.display()
        )));
    }
    let v = cache.get(&path).await?;
    Ok((*v).clone())
}

fn collect_nodes(core: &Value) -> Vec<Value> {
    // Expected shape: { nodes: [ { id, kind, label, description, ... }, ... ] }
    // Also handles flat record shape from ontoref: { "NodeId": { ... }, ... }
    if let Some(arr) = core["nodes"].as_array() {
        return arr.clone();
    }
    // Flat record shape: top-level keys are node IDs
    if let Some(obj) = core.as_object() {
        return obj
            .iter()
            .filter(|(k, _)| !k.starts_with('_'))
            .map(|(k, v)| {
                let mut node = v.clone();
                if node["id"].is_null() {
                    node["id"] = json!(k);
                }
                node
            })
            .collect();
    }
    Vec::new()
}

// ── ontology_search ───────────────────────────────────────────────────────────

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

#[async_trait]
impl Tool for OntologySearch {
    fn name(&self) -> &'static str { "ontology_search" }
    fn description(&self) -> &'static str {
        "Search ontology nodes by term (id, label, description, tags)"
    }
    fn schema(&self) -> Value {
        json!({
            "type": "object",
            "required": ["query"],
            "properties": {
                "query": { "type": "string" },
                "kind":  { "type": "string", "description": "Filter by node kind" }
            }
        })
    }
    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 kind_filter = params["kind"].as_str();

        let core = load_core(&self.cache, ctx).await?;
        let nodes = collect_nodes(&core);

        let matched: Vec<&Value> = nodes
            .iter()
            .filter(|n| {
                if let Some(k) = kind_filter {
                    if n["kind"].as_str().unwrap_or("") != k {
                        return false;
                    }
                }
                let id = n["id"].as_str().unwrap_or("").to_lowercase();
                let label = n["label"].as_str().unwrap_or("").to_lowercase();
                let desc = n["description"].as_str().unwrap_or("").to_lowercase();
                id.contains(&query) || label.contains(&query) || desc.contains(&query)
            })
            .collect();

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

// ── ontology_node ─────────────────────────────────────────────────────────────

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

#[async_trait]
impl Tool for OntologyNode {
    fn name(&self) -> &'static str { "ontology_node" }
    fn description(&self) -> &'static str { "Get a single ontology node by id" }
    fn schema(&self) -> Value {
        json!({
            "type": "object",
            "required": ["id"],
            "properties": {
                "id": { "type": "string" }
            }
        })
    }
    fn category(&self) -> ToolCategory { ToolCategory::Read }

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

        let core = load_core(&self.cache, ctx).await?;

        // Try flat record first (key = id), then nodes array
        if let Some(obj) = core.as_object() {
            if let Some(node) = obj.get(id) {
                let mut n = node.clone();
                n["id"] = json!(id);
                return Ok(n);
            }
        }
        if let Some(arr) = core["nodes"].as_array() {
            if let Some(n) = arr.iter().find(|n| n["id"].as_str() == Some(id)) {
                return Ok(n.clone());
            }
        }

        Err(ToolError::not_found(format!("ontology node '{id}'")))
    }
}

// ── ontology_graph ────────────────────────────────────────────────────────────

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

#[async_trait]
impl Tool for OntologyGraph {
    fn name(&self) -> &'static str { "ontology_graph" }
    fn description(&self) -> &'static str {
        "Return the ontology as a graph (nodes + edges) for visualisation"
    }
    fn schema(&self) -> Value {
        json!({
            "type": "object",
            "properties": {
                "format": { "type": "string", "enum": ["json", "dot"], "default": "json" }
            }
        })
    }
    fn category(&self) -> ToolCategory { ToolCategory::Read }

    async fn invoke(&self, params: Value, ctx: &Context) -> Result<Value, ToolError> {
        let format = params["format"].as_str().unwrap_or("json");
        let core = load_core(&self.cache, ctx).await?;

        match format {
            "dot" => {
                let dot = ontology_to_dot(&core);
                Ok(json!({ "format": "dot", "content": dot }))
            }
            _ => {
                let nodes = collect_nodes(&core);
                let edges = core["edges"].as_array().cloned().unwrap_or_default();
                Ok(json!({ "nodes": nodes, "edges": edges, "total_nodes": nodes.len() }))
            }
        }
    }
}

fn ontology_to_dot(core: &Value) -> String {
    let nodes = collect_nodes(core);
    let edges = core["edges"].as_array().cloned().unwrap_or_default();

    let mut lines = vec!["digraph ontology {".to_owned(), "  rankdir=LR;".to_owned()];

    for node in &nodes {
        let id = node["id"].as_str().unwrap_or("?");
        let label = node["label"].as_str().or(node["description"].as_str()).unwrap_or(id);
        let safe_id = id.replace(['-', '.', ' '], "_");
        lines.push(format!(r#"  {safe_id} [label="{label}"];"#));
    }
    for edge in &edges {
        let from = edge["from"].as_str().or(edge["source"].as_str()).unwrap_or("?");
        let to = edge["to"].as_str().or(edge["target"].as_str()).unwrap_or("?");
        let rel = edge["relation"].as_str().or(edge["label"].as_str()).unwrap_or("");
        let f = from.replace(['-', '.', ' '], "_");
        let t = to.replace(['-', '.', ' '], "_");
        lines.push(format!(r#"  {f} -> {t} [label="{rel}"];"#));
    }
    lines.push("}".to_owned());
    lines.join("\n")
}

#[cfg(test)]
mod tests {
    use super::*;

    #[test]
    fn collect_nodes_from_array() {
        let core = json!({ "nodes": [{ "id": "a", "label": "A" }] });
        let nodes = collect_nodes(&core);
        assert_eq!(nodes.len(), 1);
        assert_eq!(nodes[0]["id"], "a");
    }

    #[test]
    fn collect_nodes_from_flat_record() {
        let core = json!({ "practice_a": { "label": "Practice A" } });
        let nodes = collect_nodes(&core);
        assert_eq!(nodes.len(), 1);
        assert_eq!(nodes[0]["id"], "practice_a");
    }

    #[test]
    fn ontology_to_dot_produces_digraph() {
        let core = json!({
            "nodes": [
                { "id": "n1", "label": "Node 1" },
                { "id": "n2", "label": "Node 2" }
            ],
            "edges": [
                { "from": "n1", "to": "n2", "relation": "uses" }
            ]
        });
        let dot = ontology_to_dot(&core);
        assert!(dot.contains("digraph"));
        assert!(dot.contains("n1 -> n2"));
        assert!(dot.contains("uses"));
    }
}
