use std::fs;
use std::path::{Path, PathBuf};

pub(super) fn slugify(s: &str) -> String {
    s.to_lowercase()
        .chars()
        .map(|c| if c.is_alphanumeric() { c } else { '-' })
        .collect::<String>()
        .split('-')
        .filter(|x| !x.is_empty())
        .collect::<Vec<_>>()
        .join("-")
}

pub(super) fn read(path: &Path) -> String {
    fs::read_to_string(path).unwrap_or_default()
}

pub(super) fn first_body_line(body: &str) -> String {
    body.lines()
        .map(str::trim)
        .find(|l| !l.is_empty() && !l.starts_with('#'))
        .unwrap_or("")
        .to_string()
}

/// Build/vendor/cache directories never worth indexing as docs. Mirrors Glyph's code-walk
/// ignore list (eidos-glyph `is_ignored_dir_name`) so the Aun markdown scan doesn't flood a
/// real vault with vendored `node_modules` docs and generated artifacts.
pub(super) fn is_ignored_dir_name(name: &str) -> bool {
    matches!(
        name,
        "target" | "node_modules" | "dist" | "build" | "coverage" | "vendor"
    ) || matches!(name, ".git" | ".next" | ".turbo")
        || name == ".eidos"
        || name.starts_with("graph.tmp-")
        || (name.starts_with('.') && name != ".")
}

/// Project-root `.eidosignore` directory names/path-prefixes (one per line, `#` comments),
/// e.g. `_archive` or `backups` — for user content the default list can't know about.
fn read_eidosignore(root: &Path) -> Vec<String> {
    let Ok(raw) = fs::read_to_string(root.join(".eidosignore")) else {
        return Vec::new();
    };
    raw.lines()
        .map(str::trim)
        .filter(|l| !l.is_empty() && !l.starts_with('#'))
        .map(|l| l.trim_end_matches('/').to_string())
        .collect()
}

pub(super) fn walk_md(dir: &Path, out: &mut Vec<PathBuf>) {
    let ignore = read_eidosignore(dir);
    walk_md_inner(dir, dir, &ignore, out);
}

fn walk_md_inner(dir: &Path, root: &Path, ignore: &[String], out: &mut Vec<PathBuf>) {
    if let Ok(entries) = fs::read_dir(dir) {
        for e in entries.flatten() {
            let p = e.path();
            if p.is_dir() {
                let name = p.file_name().and_then(|n| n.to_str()).unwrap_or("");
                if is_ignored_dir_name(name) {
                    continue;
                }
                let rel = p.strip_prefix(root).unwrap_or(&p).to_string_lossy();
                if ignore
                    .iter()
                    .any(|pat| name == pat || rel == *pat || rel.starts_with(&format!("{pat}/")))
                {
                    continue;
                }
                walk_md_inner(&p, root, ignore, out);
            } else if p.extension().is_some_and(|x| x == "md") {
                out.push(p);
            }
        }
    }
}

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

    #[test]
    fn walk_md_skips_vendored_and_eidosignored_dirs() {
        let dir = tempfile::tempdir().unwrap();
        let root = dir.path();
        fs::write(root.join("top.md"), "# top").unwrap();
        fs::create_dir_all(root.join("node_modules/pkg")).unwrap();
        fs::write(root.join("node_modules/pkg/dep.md"), "# dep").unwrap();
        fs::create_dir_all(root.join("sub")).unwrap();
        fs::write(root.join("sub/inner.md"), "# inner").unwrap();
        fs::create_dir_all(root.join("_archive")).unwrap();
        fs::write(root.join("_archive/old.md"), "# old").unwrap();
        fs::write(root.join(".eidosignore"), "_archive\n").unwrap();

        let mut out = Vec::new();
        walk_md(root, &mut out);
        let names: Vec<String> = out
            .iter()
            .filter_map(|p| p.file_name().map(|n| n.to_string_lossy().into_owned()))
            .collect();
        assert!(names.contains(&"top.md".to_string()));
        assert!(names.contains(&"inner.md".to_string()));
        assert!(
            !names.contains(&"dep.md".to_string()),
            "node_modules must be skipped"
        );
        assert!(
            !names.contains(&"old.md".to_string()),
            ".eidosignore _archive must be skipped"
        );
    }
}
