use std::collections::{BTreeMap, BTreeSet};
use std::path::{Path, PathBuf};

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

use super::frontmatter::parse_frontmatter;
use super::helpers::{first_body_line, read, slugify, walk_md};
use super::nodes::doc_and_sections_from;
use super::search::query_surfaces_from_frontmatter_and_body;

pub(super) fn aun_source_markdown(root: &Path, codex_root: &Path) -> Vec<PathBuf> {
    let mut files = Vec::new();
    walk_md(&root.join("skills"), &mut files);

    let mut docs = Vec::new();
    walk_md(codex_root, &mut docs);
    files.extend(
        docs.into_iter()
            .filter(|md| md.file_name().is_none_or(|n| n != "SKILL.md")),
    );

    let mut seen = BTreeSet::new();
    files.retain(|p| seen.insert(p.to_string_lossy().into_owned()));
    files.sort();
    files
}

/// Scan `root/skills/*/SKILL.md` into skill nodes (native kinds; `type` maps legacy).
pub fn scan_skills(root: &Path) -> Vec<Node> {
    scan_skills_with_vocab(root, None)
}

/// Like [`scan_skills`], honoring an optional project-profile `[node_kinds]` vocabulary for the
/// frontmatter `type`/`kind`. `None` is byte-for-byte identical to [`scan_skills`].
pub fn scan_skills_with_vocab(
    root: &Path,
    node_kind_overrides: Option<&BTreeMap<String, String>>,
) -> Vec<Node> {
    let mut nodes = Vec::new();
    // Discover SKILL.md ANYWHERE under the vault root — not just `root/skills/<name>/SKILL.md`.
    // The old depth-1 `read_dir(root/skills)` assumed every skill lives one level under a `skills/`
    // dir. Hermes (and others) lay skills out as `<namespace>/<skill>/SKILL.md` with NO `skills/`
    // segment, so the scanner found nothing and the skill was invisible (0 nodes). Walking the whole
    // root catches both layouts. `scan_md_dir`'s SKILL.md filter still keeps the wiki/doc scans from
    // double-indexing these, so each SKILL.md → exactly one Skill node.
    let mut skill_files = Vec::new();
    walk_md(root, &mut skill_files);
    skill_files.retain(|p| p.file_name().is_some_and(|n| n == "SKILL.md"));
    skill_files.sort();
    for skill_md in skill_files {
        let text = read(&skill_md);
        let (fm, body) = parse_frontmatter(&text);
        let name = fm.get("name").map_or_else(
            || {
                skill_md
                    .parent()
                    .and_then(|p| p.file_name())
                    .map(|n| n.to_string_lossy().into_owned())
                    .unwrap_or_default()
            },
            str::to_string,
        );
        let kind = fm
            .get("type")
            .or(fm.get("kind"))
            .map_or(Kind::Skill, |raw| Kind::resolve(raw, node_kind_overrides));
        // A SKILL.md is definitionally a skill — subkind defaults to "skill" unless the author
        // declared a more specific domain type. (Raw frontmatter type, lowercased.)
        let subkind = Some(fm.get("type").or(fm.get("kind")).map_or_else(
            || "skill".to_string(),
            |raw| raw.trim().to_ascii_lowercase(),
        ));
        // Read whatever vocabulary the author declared -- aliases, triggers, and the common
        // `keywords:`/`tags:` fields (previously ignored, leaving such skills near-invisible).
        let mut aliases = fm.get_canonical_list("aliases");
        aliases.extend(fm.get_canonical_list("triggers"));
        aliases.extend(fm.get_canonical_list("keywords"));
        aliases.extend(fm.get_canonical_list("tags"));
        let mut seen_aliases = BTreeSet::new();
        aliases.retain(|alias| seen_aliases.insert(alias.clone()));
        // Fall back to the first body line when no summary/description is given (mirrors docs),
        // so a skill authored as `name:` + prose body still has a searchable surface.
        let summary = fm
            .get("summary")
            .or(fm.get("description"))
            .map_or_else(|| first_body_line(body), str::to_string);
        nodes.push(Node {
            id: format!("skill.{}", slugify(&name)),
            kind,
            subkind,
            title: name,
            summary,
            aliases,
            tags: fm.get_canonical_list("tags"),
            query_examples: query_surfaces_from_frontmatter_and_body(
                fm.get_canonical_list("query_examples"),
                body,
            ),
            source_files: vec![skill_md.to_string_lossy().into_owned()],
            span: None,
            partition: None,
        });
    }
    nodes
}

/// Scan markdown under `dir` into doc nodes (skipping SKILL.md). Ids are relative to `base`.
fn scan_md_dir(
    dir: &Path,
    base: &Path,
    node_kind_overrides: Option<&BTreeMap<String, String>>,
) -> Vec<Node> {
    let mut files = Vec::new();
    walk_md(dir, &mut files);
    files.sort();
    files
        .into_iter()
        .filter(|md| md.file_name().is_none_or(|n| n != "SKILL.md"))
        .flat_map(|md| doc_and_sections_from(&md, base, node_kind_overrides))
        .collect()
}

/// Doc nodes from the codex vault (ids relative to the codex root).
pub fn scan_docs(codex_root: &Path) -> Vec<Node> {
    scan_docs_with_vocab(codex_root, None)
}

/// Like [`scan_docs`], honoring an optional project-profile `[node_kinds]` vocabulary.
pub fn scan_docs_with_vocab(
    codex_root: &Path,
    node_kind_overrides: Option<&BTreeMap<String, String>>,
) -> Vec<Node> {
    scan_md_dir(codex_root, codex_root, node_kind_overrides)
}

/// Doc nodes from `codex_root`, with ids relative to an explicit `base`.
///
/// Single-root packages keep using [`scan_docs_with_vocab`] so existing IDs stay stable
/// (`doc.retrieval`). Multi-root EKF packages use this with the package root as `base`, so
/// `recipes/overview.md` and `furniture/overview.md` do not collide.
pub fn scan_docs_with_base(
    codex_root: &Path,
    base: &Path,
    node_kind_overrides: Option<&BTreeMap<String, String>>,
) -> Vec<Node> {
    scan_md_dir(codex_root, base, node_kind_overrides)
}

/// Doc nodes from skill WIKI pages (`root/skills/**/*.md`, excluding SKILL.md). Ids are
/// relative to `root` (so they carry the `skills-<name>-wiki-...` path -> always unique).
pub fn scan_skill_wikis(root: &Path) -> Vec<Node> {
    scan_skill_wikis_with_vocab(root, None)
}

/// Like [`scan_skill_wikis`], honoring an optional project-profile `[node_kinds]` vocabulary.
pub fn scan_skill_wikis_with_vocab(
    root: &Path,
    node_kind_overrides: Option<&BTreeMap<String, String>>,
) -> Vec<Node> {
    scan_md_dir(&root.join("skills"), root, node_kind_overrides)
}

/// Docs/wiki pages from an explicit EKF skill root. Unlike [`scan_skill_wikis`], this does not
/// require the root directory to be named `skills`; every markdown file under `root` except
/// `SKILL.md` becomes a doc/section node relative to that declared root.
pub fn scan_explicit_skill_docs(root: &Path) -> Vec<Node> {
    scan_explicit_skill_docs_with_vocab(root, None)
}

/// Like [`scan_explicit_skill_docs`], honoring an optional project-profile vocabulary.
pub fn scan_explicit_skill_docs_with_vocab(
    root: &Path,
    node_kind_overrides: Option<&BTreeMap<String, String>>,
) -> Vec<Node> {
    scan_md_dir(root, root, node_kind_overrides)
}

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

    fn write(path: &Path, body: &str) {
        std::fs::create_dir_all(path.parent().unwrap()).unwrap();
        std::fs::write(path, body).unwrap();
    }

    /// Regression for the SKILL.md discovery bug: Hermes-style `<namespace>/<skill>/SKILL.md`
    /// (no `skills/` directory) was invisible — the old depth-1 `read_dir(root/skills)` found
    /// nothing, so the skill produced 0 nodes and was ungroundable.
    #[test]
    fn discovers_skill_md_in_nested_namespace_layout() {
        let tmp = tempfile::tempdir().unwrap();
        write(
            &tmp.path().join("autonomous-ai/baking/SKILL.md"),
            "---\nname: baking\ntype: skill\nsummary: \"How to bake bread.\"\n---\n# Baking\nProofing.\n",
        );

        let skills = scan_skills(tmp.path());
        assert_eq!(skills.len(), 1, "nested SKILL.md must be discovered");
        assert_eq!(skills[0].kind, Kind::Skill);
        assert_eq!(skills[0].title, "baking");
        assert_eq!(skills[0].id, "skill.baking");
    }

    /// The flat `skills/<name>/SKILL.md` layout (the eval-vault layout) must keep working — the
    /// gate's byte-identity depends on it.
    #[test]
    fn still_discovers_flat_skills_layout() {
        let tmp = tempfile::tempdir().unwrap();
        write(
            &tmp.path().join("skills/knife/SKILL.md"),
            "---\nname: knife\ntype: skill\n---\nGrip.\n",
        );
        let skills = scan_skills(tmp.path());
        assert_eq!(skills.len(), 1);
        assert_eq!(skills[0].id, "skill.knife");
    }

    #[test]
    fn explicit_skill_docs_do_not_require_skills_dir_name() {
        let tmp = tempfile::tempdir().unwrap();
        write(
            &tmp.path().join("skillpacks/retry/SKILL.md"),
            "---\nname: retry-policy\nsummary: Retries.\n---\nRetry skill.\n",
        );
        write(
            &tmp.path().join("skillpacks/retry/wiki/budget.md"),
            "---\ntitle: Retry Budget\nsummary: Budget guidance.\n---\nRetry amplification guidance.\n",
        );

        let docs = scan_explicit_skill_docs(&tmp.path().join("skillpacks"));

        assert!(
            docs.iter().any(|node| node.title == "Retry Budget"),
            "explicit skill roots should include adjacent wiki/docs even without root/skills"
        );
    }
}
