//! Aun — the docs source (markdown → nodes with receipts).
//!
//! Reads markdown files — skills, wikis, notes — and extracts [`Node`]s with provenance receipts.
//! Each doc becomes a `Doc` node; each heading-block becomes a `Section` node with a `file:line`
//! span. Wikilinks become `links_to` edges; unlinked mentions become `mentions` edges.
//!
//! This is the DOCS memory.
//!
//! [`Node`]: eidos_kernel::schema::Node

pub mod compile;

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

use crate::source::{PendingEdge, Source as SourceTrait, SourceOutput};

pub use compile::{
    AunFrontmatterProfile, CompileDiagnostics, DiagnosticSeverity, FieldProfile, FieldShape,
    Frontmatter, FrontmatterShape, LegacyValueProfile, NodeQualityScore, ProfileNodeKind,
    QualityFinding, QualityFindingKind, Repairability, aun_frontmatter_profile, build_graph,
    build_graph_with_diagnostics, build_graph_with_diagnostics_and_skill_roots,
    build_graph_with_diagnostics_and_vocab, build_graph_with_docs_roots_relation_fields,
    build_graph_with_relation_fields, build_graph_with_vocab, default_relation_fields,
    diagnostics_for, diagnostics_for_registry_source, diagnostics_for_with_relation_fields,
    parse_frontmatter, scan_docs, scan_docs_with_vocab, scan_skill_wikis,
    scan_skill_wikis_with_vocab, scan_skills, scan_skills_with_vocab,
};

/// The Aun source: compiles a docs/vault root into graph nodes and edges.
///
/// Aun supports the two-root model: a `vault_root` (skills, wikis) and an optional `docs_root`
/// (standalone docs). When `docs_root` is set, Aun compiles both in one pass — matching the
/// original `build_graph(vault, docs)` behavior exactly.
#[derive(Debug, Clone)]
pub struct Aun {
    docs_roots: Vec<std::path::PathBuf>,
    docs_partitions: Vec<Option<String>>,
    skill_roots: Vec<std::path::PathBuf>,
    skill_partitions: Vec<Option<String>>,
    relation_fields: BTreeMap<String, String>,
}

impl Aun {
    /// Create a new Aun source that compiles only `root` (the registry provides the root).
    pub fn new() -> Self {
        Self {
            docs_roots: Vec::new(),
            docs_partitions: Vec::new(),
            skill_roots: Vec::new(),
            skill_partitions: Vec::new(),
            relation_fields: default_relation_fields(),
        }
    }

    /// Create an Aun source with a separate docs/codex root.
    /// When registered, the registry provides the vault root; this provides the docs root.
    pub fn with_docs_root(docs_root: impl AsRef<std::path::Path>) -> Self {
        Self {
            docs_roots: vec![docs_root.as_ref().to_path_buf()],
            docs_partitions: vec![None],
            skill_roots: Vec::new(),
            skill_partitions: Vec::new(),
            relation_fields: default_relation_fields(),
        }
    }

    /// Create an Aun source with docs and skill roots carrying generated EKF partition provenance.
    pub fn with_docs_root_partitions_and_skill_roots(
        docs_roots: Vec<(std::path::PathBuf, Option<String>)>,
        skill_roots: Vec<(std::path::PathBuf, Option<String>)>,
    ) -> Self {
        let (docs_roots, docs_partitions): (Vec<_>, Vec<_>) = docs_roots.into_iter().unzip();
        let (skill_roots, skill_partitions): (Vec<_>, Vec<_>) = skill_roots.into_iter().unzip();
        Self {
            docs_roots,
            docs_partitions,
            skill_roots,
            skill_partitions,
            relation_fields: default_relation_fields(),
        }
    }

    pub fn with_relation_fields(mut self, relation_fields: BTreeMap<String, String>) -> Self {
        self.relation_fields = relation_fields;
        self
    }
}

impl Default for Aun {
    fn default() -> Self {
        Self::new()
    }
}

impl SourceTrait for Aun {
    fn name(&self) -> &str {
        "aun"
    }

    /// Aun's separate docs/codex root (when set) is a second filesystem root it reads — declare it
    /// so the freshness signature walks it too and detects changes to standalone docs.
    fn extra_roots(&self) -> Vec<std::path::PathBuf> {
        self.docs_roots
            .iter()
            .cloned()
            .chain(self.skill_roots.iter().cloned())
            .collect()
    }

    fn compile(&self, root: &Path) -> SourceOutput {
        let mut graph = if self.docs_roots.len() <= 1 {
            let docs = self.docs_roots.first().map_or(root, |path| path.as_path());
            build_graph_with_relation_fields(root, docs, &self.skill_roots, &self.relation_fields)
        } else {
            build_graph_with_docs_roots_relation_fields(
                root,
                &self.docs_roots,
                &self.skill_roots,
                &self.relation_fields,
            )
        };
        stamp_root_partitions(
            &mut graph.nodes,
            self.docs_roots
                .iter()
                .zip(self.docs_partitions.iter())
                .chain(self.skill_roots.iter().zip(self.skill_partitions.iter())),
        );
        let diagnostic_docs_root = self.docs_roots.first().map_or(root, |path| path.as_path());
        let diagnostics = diagnostics_for_registry_source(
            root,
            diagnostic_docs_root,
            &graph,
            &self.relation_fields,
        );
        SourceOutput {
            pending_edges: compile::frontmatter_pending_edges(&graph.nodes, &self.relation_fields)
                .into_iter()
                .map(|edge| PendingEdge {
                    from: edge.from,
                    target: edge.target,
                    relation: edge.relation,
                    evidence: edge.evidence,
                })
                .collect(),
            nodes: graph.nodes,
            edges: graph.edges,
            diagnostics: diagnostics
                .findings
                .into_iter()
                .map(|f| crate::source::SourceDiagnostic {
                    message: f.message,
                    severity: match f.severity {
                        compile::DiagnosticSeverity::Info => "info".into(),
                        compile::DiagnosticSeverity::Warning => "warning".into(),
                    },
                    kind: serde_json::to_value(&f.kind)
                        .ok()
                        .and_then(|value| value.as_str().map(str::to_string))
                        .unwrap_or_else(|| format!("{:?}", f.kind)),
                    source: f.source,
                    node_id: f.node_id,
                    suggested_action: f.suggested_action,
                    repair_tool: f.repair_tool,
                })
                .collect(),
        }
    }
}

fn stamp_root_partitions<'a>(
    nodes: &mut [eidos_kernel::schema::Node],
    roots: impl Iterator<Item = (&'a std::path::PathBuf, &'a Option<String>)>,
) {
    let mut roots = roots
        .filter_map(|(root, partition)| partition.as_ref().map(|partition| (root, partition)))
        .collect::<Vec<_>>();
    roots.sort_by_key(|(root, _)| std::cmp::Reverse(root.components().count()));
    for node in nodes {
        let Some(source) = node.source_files.first() else {
            continue;
        };
        let source = std::path::Path::new(source);
        for (root, partition) in &roots {
            if source_is_under_root(source, root) {
                node.partition = Some((*partition).clone());
                break;
            }
        }
    }
}

fn source_is_under_root(source: &std::path::Path, root: &std::path::Path) -> bool {
    if source.starts_with(root) {
        return true;
    }
    let cwd = std::env::current_dir().ok();
    let source_abs = if source.is_absolute() {
        source.to_path_buf()
    } else if let Some(cwd) = &cwd {
        cwd.join(source)
    } else {
        source.to_path_buf()
    };
    let root_abs = if root.is_absolute() {
        root.to_path_buf()
    } else if let Some(cwd) = &cwd {
        cwd.join(root)
    } else {
        root.to_path_buf()
    };
    source_abs.starts_with(root_abs)
}
