//! Compile -- scan the vault (skills + docs) into a graph.
//!
//! Frontmatter is parsed by a small hand-rolled tolerant parser, NOT a YAML crate: the
//! Rust YAML ecosystem is in churn (serde_yaml deprecated, serde_yml unmaintained as of
//! 2026) and frontmatter is a simple `key: value` + list subset. A malformed file
//! degrades to empty frontmatter and is never allowed to panic the build.

mod diagnostics;
mod edges;
mod frontmatter;
mod helpers;
mod links;
mod mentions;
mod nodes;
mod ownership;
mod quality;
mod relations;
mod scan;
mod schema_profile;
mod search;
mod target;

use std::collections::BTreeMap;
use std::path::Path;

use eidos_kernel::schema::{Graph, Kind};

pub use diagnostics::{
    CompileDiagnostics, diagnostics_for, diagnostics_for_registry_source,
    diagnostics_for_with_relation_fields,
};
pub use frontmatter::{Frontmatter, FrontmatterShape, parse_frontmatter};
pub use quality::{
    DiagnosticSeverity, NodeQualityScore, QualityFinding, QualityFindingKind, Repairability,
};
pub use relations::{PendingFrontmatterEdge, default_relation_fields, frontmatter_pending_edges};
pub use scan::{
    scan_docs, scan_docs_with_base, scan_docs_with_vocab, scan_explicit_skill_docs,
    scan_explicit_skill_docs_with_vocab, scan_skill_wikis, scan_skill_wikis_with_vocab,
    scan_skills, scan_skills_with_vocab,
};
pub use schema_profile::{
    AunFrontmatterProfile, FieldProfile, FieldShape, LegacyValueProfile, ProfileNodeKind,
    aun_frontmatter_profile,
};

pub fn build_graph_with_diagnostics(root: &Path, codex_root: &Path) -> (Graph, CompileDiagnostics) {
    build_graph_with_diagnostics_and_vocab(root, codex_root, None)
}

/// Compile a graph using explicit EKF skill roots plus a docs root.
///
/// This preserves the legacy graph path when no explicit skill roots are configured, but lets EKF
/// packages move skills/wikis outside a hardcoded `skills/` directory.
pub fn build_graph_with_diagnostics_and_skill_roots(
    root: &Path,
    codex_root: &Path,
    skill_roots: &[std::path::PathBuf],
) -> (Graph, CompileDiagnostics) {
    let graph = build_graph_with_skill_roots(root, codex_root, skill_roots, None, None);
    // Diagnostics still use the legacy markdown inventory for now. The explicit-root compiler is
    // source-of-truth for nodes; missing-root and package-plane diagnostics come from EKF.
    let diagnostics = diagnostics_for(root, codex_root, &graph);
    (graph, diagnostics)
}

/// Like [`build_graph_with_diagnostics`], honoring an optional project-profile `[node_kinds]`
/// vocabulary so native docs/skills can carry a custom `type`/`kind` agent vocabulary.
/// `None` is byte-for-byte identical to [`build_graph_with_diagnostics`].
pub fn build_graph_with_diagnostics_and_vocab(
    root: &Path,
    codex_root: &Path,
    node_kind_overrides: Option<&BTreeMap<String, String>>,
) -> (Graph, CompileDiagnostics) {
    let graph = build_graph_with_vocab(root, codex_root, node_kind_overrides);
    let diagnostics = diagnostics_for(root, codex_root, &graph);
    (graph, diagnostics)
}

/// Compile the vault into a graph. Curated knowledge sources: skills + skill-wiki pages
/// under `root/skills`, plus docs under `codex_root`. Each source file yields one node;
/// edges come from skill->wiki structure, wikilinks, and frontmatter relations.
pub fn build_graph(root: &Path, codex_root: &Path) -> Graph {
    build_graph_with_vocab(root, codex_root, None)
}

/// Like [`build_graph`], honoring an optional project-profile `[node_kinds]` vocabulary applied to
/// native docs/skills frontmatter `type`/`kind`. `None` is byte-for-byte identical to [`build_graph`].
pub fn build_graph_with_vocab(
    root: &Path,
    codex_root: &Path,
    node_kind_overrides: Option<&BTreeMap<String, String>>,
) -> Graph {
    build_graph_with_skill_roots(root, codex_root, &[], node_kind_overrides, None)
}

pub fn build_graph_with_relation_fields(
    root: &Path,
    codex_root: &Path,
    skill_roots: &[std::path::PathBuf],
    relation_fields: &BTreeMap<String, String>,
) -> Graph {
    build_graph_with_skill_roots(root, codex_root, skill_roots, None, Some(relation_fields))
}

pub fn build_graph_with_docs_roots_relation_fields(
    root: &Path,
    codex_roots: &[std::path::PathBuf],
    skill_roots: &[std::path::PathBuf],
    relation_fields: &BTreeMap<String, String>,
) -> Graph {
    build_graph_with_docs_roots(root, codex_roots, skill_roots, None, Some(relation_fields))
}

fn build_graph_with_skill_roots(
    root: &Path,
    codex_root: &Path,
    skill_roots: &[std::path::PathBuf],
    node_kind_overrides: Option<&BTreeMap<String, String>>,
    relation_fields: Option<&BTreeMap<String, String>>,
) -> Graph {
    let codex_roots = [codex_root.to_path_buf()];
    build_graph_with_docs_roots(
        root,
        &codex_roots,
        skill_roots,
        node_kind_overrides,
        relation_fields,
    )
}

fn build_graph_with_docs_roots(
    root: &Path,
    codex_roots: &[std::path::PathBuf],
    skill_roots: &[std::path::PathBuf],
    node_kind_overrides: Option<&BTreeMap<String, String>>,
    relation_fields: Option<&BTreeMap<String, String>>,
) -> Graph {
    let mut nodes = scan_skills_with_vocab(root, node_kind_overrides);
    if skill_roots.is_empty() {
        nodes.extend(scan_skill_wikis_with_vocab(root, node_kind_overrides));
    } else {
        nodes.clear();
        for skill_root in skill_roots {
            nodes.extend(scan_skills_with_vocab(skill_root, node_kind_overrides));
            nodes.extend(scan_explicit_skill_docs_with_vocab(
                skill_root,
                node_kind_overrides,
            ));
        }
    }

    match codex_roots {
        [] => nodes.extend(scan_docs_with_vocab(root, node_kind_overrides)),
        [only] => nodes.extend(scan_docs_with_vocab(only, node_kind_overrides)),
        many => {
            for codex_root in many {
                let base = if codex_root.starts_with(root) {
                    root
                } else {
                    codex_root
                };
                nodes.extend(scan_docs_with_base(codex_root, base, node_kind_overrides));
            }
        }
    }

    // one node per source file (a file scanned twice keeps the first) — EXCEPT Section nodes, which
    // legitimately share their parent doc's file, so they dedup by id instead of by source file.
    let mut seen_files = std::collections::HashSet::new();
    let mut seen_sections = std::collections::HashSet::new();
    nodes.retain(|n| {
        if n.kind == Kind::Section {
            seen_sections.insert(n.id.clone())
        } else {
            n.source_files
                .first()
                .is_none_or(|s| seen_files.insert(s.clone()))
        }
    });
    nodes.sort_by(|a, b| a.id.cmp(&b.id));
    let default_relation_fields;
    let relation_fields = match relation_fields {
        Some(relation_fields) => relation_fields,
        None => {
            default_relation_fields = relations::default_relation_fields();
            &default_relation_fields
        }
    };
    let edges = edges::build_edges(&nodes, relation_fields);
    Graph {
        nodes,
        edges,
        ..Default::default()
    }
}
