/// Convert api-catalog entries into Cytoscape graph nodes and edges.
///
/// Each catalog becomes a `Crate` node; each route becomes a `Route` node with
/// a `Contains` edge from its crate. Poles `"Code"` and `"Api"` are used so
/// the graph template can assign distinct colours without touching ontology
/// data.
pub(crate) fn api_catalog_graph_nodes(
    catalogs: &[(String, Vec<serde_json::Value>)],
) -> (Vec<serde_json::Value>, Vec<serde_json::Value>) {
    let mut nodes: Vec<serde_json::Value> = Vec::new();
    let mut edges: Vec<serde_json::Value> = Vec::new();

    for (crate_name, routes) in catalogs {
        let crate_id = format!("crate:{crate_name}");
        nodes.push(serde_json::json!({
            "id":            crate_id,
            "name":          crate_name,
            "level":         "Crate",
            "pole":          "Code",
            "description":   format!("API surface of {crate_name}"),
            "invariant":     false,
            "artifact_paths": [format!("artifacts/api-catalog-{crate_name}.ncl")],
            "adrs":          [],
        }));

        for route in routes {
            let method = route
                .get("method")
                .and_then(|v| v.as_str())
                .unwrap_or("GET");
            let path = route.get("path").and_then(|v| v.as_str()).unwrap_or("/");
            let desc = route
                .get("description")
                .and_then(|v| v.as_str())
                .unwrap_or("");
            let auth = route.get("auth").and_then(|v| v.as_str()).unwrap_or("none");
            let feature = route.get("feature").and_then(|v| v.as_str()).unwrap_or("");
            let source_file = route
                .get("source_file")
                .and_then(|v| v.as_str())
                .unwrap_or("");
            let actors = route
                .get("actors")
                .cloned()
                .unwrap_or(serde_json::json!([]));
            let tags = route.get("tags").cloned().unwrap_or(serde_json::json!([]));
            let params = route
                .get("params")
                .cloned()
                .unwrap_or(serde_json::json!([]));
            let route_id = format!(
                "route:{}:{}",
                method,
                path.replace('/', ":").trim_matches(':')
            );

            let mut artifact_paths = vec![format!("artifacts/api-catalog-{crate_name}.ncl")];
            if !source_file.is_empty() {
                artifact_paths.push(source_file.to_owned());
            }

            nodes.push(serde_json::json!({
                "id":             route_id,
                "name":           format!("{method} {path}"),
                "level":          "Route",
                "pole":           "Api",
                "description":    desc,
                "invariant":      false,
                "artifact_paths": artifact_paths,
                "adrs":           [],
                "method":         method,
                "auth":           auth,
                "actors":         actors,
                "tags":           tags,
                "params":         params,
                "feature":        feature,
            }));

            edges.push(serde_json::json!({
                "from": crate_id,
                "to":   route_id,
                "kind": "Contains",
                "note": "",
            }));
        }
    }

    (nodes, edges)
}

/// Scan `<project_root>/artifacts/` for all `api-catalog-*.ncl` files,
/// export each via the NCL cache, and return routes grouped by catalog name.
///
/// `api-catalog.ncl`          → name `"default"`
/// `api-catalog-foo-bar.ncl`  → name `"foo-bar"`
///
/// Results are sorted by name. Files that fail NCL export are skipped with a
/// warning; missing `artifacts/` directory is silently ignored.
pub(crate) async fn read_project_api_catalogs(
    project_root: &std::path::Path,
    cache: &std::sync::Arc<crate::cache::NclCache>,
    import_path: Option<&str>,
) -> Vec<(String, Vec<serde_json::Value>)> {
    let rd = match std::fs::read_dir(project_root.join("artifacts")) {
        Ok(r) => r,
        Err(_) => return vec![],
    };

    let entries: Vec<_> = rd
        .filter_map(|e| e.ok())
        .filter_map(|e| {
            let raw = e.file_name();
            let fname = raw.to_string_lossy().into_owned();
            if !fname.starts_with("api-catalog") || !fname.ends_with(".ncl") {
                return None;
            }
            let name = if fname == "api-catalog.ncl" {
                "default".to_string()
            } else {
                fname
                    .strip_prefix("api-catalog-")?
                    .strip_suffix(".ncl")
                    .map(str::to_owned)?
            };
            Some((name, e.path()))
        })
        .collect();

    let mut catalogs: Vec<(String, Vec<serde_json::Value>)> = Vec::new();
    for (name, path) in entries {
        match cache.export(&path, import_path).await {
            Ok((value, _)) => {
                let routes = value
                    .get("routes")
                    .and_then(|v| v.as_array())
                    .cloned()
                    .unwrap_or_default();
                catalogs.push((name, routes));
            }
            Err(err) => {
                tracing::warn!(
                    "api-catalog NCL export failed for {}: {err}",
                    path.display()
                );
            }
        }
    }
    catalogs.sort_by(|a, b| a.0.cmp(&b.0));
    catalogs
}

/// Build graph nodes from `.ontology/manifest.ncl` — config sections and
/// requirements.
///
/// - `ConfigSection` nodes (pole `"Config"`) — one per
///   `config_surface.sections` entry.
/// - `Requirement` nodes (pole `"Env"`) — one per `requirements` entry.
pub(crate) fn manifest_graph_nodes(
    manifest_json: &serde_json::Value,
) -> (Vec<serde_json::Value>, Vec<serde_json::Value>) {
    let mut nodes: Vec<serde_json::Value> = Vec::new();

    // Config sections.
    if let Some(sections) = manifest_json
        .get("config_surface")
        .and_then(|cs| cs.get("sections"))
        .and_then(|v| v.as_array())
    {
        for sec in sections {
            let id = sec.get("id").and_then(|v| v.as_str()).unwrap_or_default();
            if id.is_empty() {
                continue;
            }
            let desc = sec
                .get("description")
                .and_then(|v| v.as_str())
                .unwrap_or("");
            let contract = sec.get("contract").and_then(|v| v.as_str()).unwrap_or("");
            let label = if contract.is_empty() {
                id.to_string()
            } else {
                format!("{id} ({contract})")
            };
            nodes.push(serde_json::json!({
                "id":            format!("cfg:{id}"),
                "name":          label,
                "level":         "ConfigSection",
                "pole":          "Config",
                "description":   desc,
                "invariant":     false,
                "artifact_paths": [".ontoref/config.ncl"],
                "adrs":          [],
                "contract":      contract,
            }));
        }
    }

    // Requirements (external deps: tools, services, infra).
    if let Some(reqs) = manifest_json.get("requirements").and_then(|v| v.as_array()) {
        for req in reqs {
            let id = req.get("id").and_then(|v| v.as_str()).unwrap_or_default();
            if id.is_empty() {
                continue;
            }
            let name = req.get("name").and_then(|v| v.as_str()).unwrap_or(id);
            let kind = req.get("kind").and_then(|v| v.as_str()).unwrap_or("");
            let env = req.get("env").and_then(|v| v.as_str()).unwrap_or("");
            let required = req
                .get("required")
                .and_then(|v| v.as_bool())
                .unwrap_or(false);
            let impact = req.get("impact").and_then(|v| v.as_str()).unwrap_or("");
            let provision = req.get("provision").and_then(|v| v.as_str()).unwrap_or("");
            let desc = format!(
                "[{kind}/{env}] {impact}{}",
                if provision.is_empty() {
                    String::new()
                } else {
                    format!(" — provision: {provision}")
                }
            );
            nodes.push(serde_json::json!({
                "id":            format!("req:{id}"),
                "name":          name,
                "level":         "Requirement",
                "pole":          "Env",
                "description":   desc,
                "invariant":     required,
                "artifact_paths": [".ontology/manifest.ncl"],
                "adrs":          [],
                "kind":          kind,
                "env":           env,
                "required":      required,
            }));
        }
    }

    (nodes, vec![])
}

/// Build graph nodes and edges from `.ontology/state.ncl` dimensions.
///
/// Each dimension becomes a `Dimension` node (pole `"State"`). `coupled_with`
/// entries produce `CoupledWith` edges between dimension nodes.
pub(crate) fn state_graph_nodes(
    state_json: &serde_json::Value,
) -> (Vec<serde_json::Value>, Vec<serde_json::Value>) {
    let Some(dimensions) = state_json.get("dimensions").and_then(|v| v.as_array()) else {
        return (vec![], vec![]);
    };

    let mut nodes: Vec<serde_json::Value> = Vec::new();
    let mut edges: Vec<serde_json::Value> = Vec::new();

    for dim in dimensions {
        let id = dim.get("id").and_then(|v| v.as_str()).unwrap_or_default();
        if id.is_empty() {
            continue;
        }
        let node_id = format!("dim:{id}");
        let name = dim.get("name").and_then(|v| v.as_str()).unwrap_or(id);
        let desc = dim
            .get("description")
            .and_then(|v| v.as_str())
            .unwrap_or("");
        let current = dim
            .get("current_state")
            .and_then(|v| v.as_str())
            .unwrap_or("");
        let desired = dim
            .get("desired_state")
            .and_then(|v| v.as_str())
            .unwrap_or("");
        let horizon = dim.get("horizon").and_then(|v| v.as_str()).unwrap_or("");

        let reached_desired = !current.is_empty() && current == desired;
        nodes.push(serde_json::json!({
            "id":            node_id,
            "name":          name,
            "level":         "Dimension",
            "pole":          "State",
            "description":   format!("{desc} [current: {current} → desired: {desired}]"),
            "invariant":     reached_desired,
            "artifact_paths": [".ontology/state.ncl"],
            "adrs":          [],
            "current_state": current,
            "desired_state": desired,
            "horizon":       horizon,
        }));

        if let Some(coupled) = dim.get("coupled_with").and_then(|v| v.as_array()) {
            for c in coupled {
                if let Some(other_id) = c.as_str() {
                    edges.push(serde_json::json!({
                        "from": node_id,
                        "to":   format!("dim:{other_id}"),
                        "kind": "CoupledWith",
                        "note": "",
                    }));
                }
            }
        }
    }

    (nodes, edges)
}

/// Build graph nodes and edges from workspace `Cargo.toml` files.
///
/// Scans `<project_root>/crates/` for member crates. Each crate becomes a
/// `WorkspaceCrate` node (pole `"Code"`). Only inter-workspace `[dependencies]`
/// produce `DependsOn` edges — external crates are ignored.
pub(crate) fn workspace_crate_nodes(
    project_root: &std::path::Path,
) -> (Vec<serde_json::Value>, Vec<serde_json::Value>) {
    let crates_dir = project_root.join("crates");
    let Ok(rd) = std::fs::read_dir(&crates_dir) else {
        return (vec![], vec![]);
    };

    let mut crates: Vec<(String, std::path::PathBuf)> = rd
        .filter_map(|e| e.ok())
        .filter_map(|e| {
            let manifest_path = e.path().join("Cargo.toml");
            let content = std::fs::read_to_string(&manifest_path).ok()?;
            let table: toml::Table = toml::from_str(&content).ok()?;
            let name = table
                .get("package")
                .and_then(|p| p.get("name"))
                .and_then(|v| v.as_str())?
                .to_string();
            Some((name, e.path()))
        })
        .collect();
    crates.sort_by(|a, b| a.0.cmp(&b.0));

    let crate_names: std::collections::HashSet<String> =
        crates.iter().map(|(n, _)| n.clone()).collect();

    let mut nodes: Vec<serde_json::Value> = Vec::new();
    let mut edges: Vec<serde_json::Value> = Vec::new();

    for (name, path) in &crates {
        let node_id = format!("ws:{name}");
        let dir_name = path
            .file_name()
            .map(|n| n.to_string_lossy().into_owned())
            .unwrap_or_default();

        let manifest_path = path.join("Cargo.toml");
        let (desc, ws_deps, crate_features) =
            if let Ok(content) = std::fs::read_to_string(&manifest_path) {
                let table: toml::Table = toml::from_str(&content).unwrap_or_default();
                let desc = table
                    .get("package")
                    .and_then(|p| p.get("description"))
                    .and_then(|v| v.as_str())
                    .unwrap_or("")
                    .to_string();
                let deps: Vec<String> = table
                    .get("dependencies")
                    .and_then(|d| d.as_table())
                    .map(|t| {
                        t.keys()
                            .filter(|k| crate_names.contains(*k))
                            .cloned()
                            .collect()
                    })
                    .unwrap_or_default();
                let feats: Vec<String> = table
                    .get("features")
                    .and_then(|f| f.as_table())
                    .map(|t| t.keys().cloned().collect())
                    .unwrap_or_default();
                (desc, deps, feats)
            } else {
                (String::new(), vec![], vec![])
            };

        nodes.push(serde_json::json!({
            "id":            node_id,
            "name":          name,
            "level":         "WorkspaceCrate",
            "pole":          "Code",
            "description":   desc,
            "invariant":     false,
            "artifact_paths": [format!("crates/{dir_name}/Cargo.toml")],
            "adrs":          [],
            "features":      crate_features,
            "ws_deps":       ws_deps.iter().map(|d| format!("ws:{d}")).collect::<Vec<_>>(),
        }));

        for dep in ws_deps {
            edges.push(serde_json::json!({
                "from": node_id,
                "to":   format!("ws:{dep}"),
                "kind": "DependsOn",
                "note": "",
            }));
        }
    }

    (nodes, edges)
}
