// SPDX-License-Identifier: GPL-3.0-or-later
// SPDX-FileCopyrightText: 2026 Mohamed Hammad

//! Build script for `loran-core`.
//!
//! Walks the in-crate `pages/` directory (sibling to `src/`), validates
//! every `*.md` file via [`loran_pages::Page::parse`] at compile time
//! (Spec §6.1 fail-loud policy: a malformed bundled page must fail
//! the build, never silently land in a shipped binary), and emits a
//! generated `bundled_pages.rs` into `OUT_DIR` that the runtime
//! [`crate::BundledPagesIngestor`] reads via `include_str!`.
//!
//! Pages live inside `crates/loran-core/pages/` rather than at the
//! workspace root so they ship in the `loran-core` crate tarball when
//! `cargo publish` packages this crate. (Workspace-root assets are not
//! included by `cargo package` since they live outside the crate dir.)
//!
//! [`crate::BundledPagesIngestor`]: loran_core::BundledPagesIngestor

use std::fmt::Write as _;
use std::fs;
use std::path::{Path, PathBuf};

fn main() {
    let pages_dir = manifest_dir().join("pages");
    println!("cargo:rerun-if-changed={}", pages_dir.display());

    let entries = collect_md_files(&pages_dir);

    // Compile-time validation: every page must parse cleanly.
    for path in &entries {
        let body =
            fs::read_to_string(path).unwrap_or_else(|e| panic!("reading {}: {e}", path.display()));
        if let Err(err) = loran_pages::Page::parse(&body) {
            panic!(
                "bundled page `{}` failed Spec §6.1 validation: {err}",
                path.display()
            );
        }
    }

    // Emit the generated module.
    let mut generated = String::new();
    generated.push_str("// auto-generated by crates/loran-core/build.rs — do not edit\n");
    generated.push_str("pub(crate) const BUNDLED_PAGES: &[(&str, &str)] = &[\n");
    for path in &entries {
        let rel = path
            .strip_prefix(&pages_dir)
            .unwrap_or(path)
            .to_string_lossy()
            .replace('\\', "/");
        let abs = path.to_string_lossy().replace('\\', "/");
        writeln!(generated, "    ({rel:?}, include_str!({abs:?})),")
            .expect("formatting into a String never fails");
    }
    generated.push_str("];\n\n");

    // Bundle categories.toml as a literal string so the runtime can
    // parse it without doing its own filesystem walk.
    let categories_path = pages_dir.join("categories.toml");
    if categories_path.is_file() {
        let abs = categories_path.to_string_lossy().replace('\\', "/");
        writeln!(
            generated,
            "pub(crate) const BUNDLED_CATEGORIES: &str = include_str!({abs:?});"
        )
        .expect("formatting into a String never fails");
    } else {
        generated.push_str("pub(crate) const BUNDLED_CATEGORIES: &str = \"\";\n");
    }

    let out_dir = std::env::var_os("OUT_DIR").expect("OUT_DIR set by cargo");
    let out_path = Path::new(&out_dir).join("bundled_pages.rs");
    fs::write(&out_path, generated)
        .unwrap_or_else(|e| panic!("writing {}: {e}", out_path.display()));
}

fn manifest_dir() -> PathBuf {
    let manifest_dir =
        std::env::var_os("CARGO_MANIFEST_DIR").expect("CARGO_MANIFEST_DIR set by cargo");
    PathBuf::from(manifest_dir)
}

fn collect_md_files(root: &Path) -> Vec<PathBuf> {
    let mut out = Vec::new();
    walk(root, &mut out);
    out.sort();
    out
}

fn walk(dir: &Path, out: &mut Vec<PathBuf>) {
    let Ok(entries) = fs::read_dir(dir) else {
        // Pages directory may not exist in CI scratch checkouts; that's
        // fine — emit an empty BUNDLED_PAGES.
        return;
    };
    for entry in entries.flatten() {
        let path = entry.path();
        if let Some(name) = path.file_name().and_then(|n| n.to_str())
            && name.starts_with('.')
        {
            continue;
        }
        if path.is_dir() {
            walk(&path, out);
        } else if path.extension().and_then(|s| s.to_str()) == Some("md") {
            out.push(path);
        }
    }
}
