//! Build-time generation of the **bundled stdlib catalog** (v0.4 E0).
//!
//! The flagship stdlib fingerprints ship as `fingerprint = r#"…"#` string
//! literals inside `#[antigen(…)]` attributes on Rust types in
//! `src/stdlib/*.rs`. There is no runtime function that yields them, and a
//! *consumer* crate that depends on the published `antigen` crate has **no
//! antigen source on disk** — so the only robust way to hand a consumer the
//! catalog is to **compile it in**.
//!
//! This build script parses antigen's *own* `src/stdlib/*.rs` at build time
//! (with `syn`, already a dependency of the crate) and emits a generated
//! `STDLIB_CATALOG: &[(&str, &str, &str)]` — `(name, fingerprint-string,
//! provenance-string)` — into `OUT_DIR/stdlib_catalog.rs`. The library
//! `include!`s it (see `src/stdlib/catalog.rs`) so the catalog lands in the
//! `antigen` rlib and travels to every consumer **without** antigen's source.
//!
//! This is the load-bearing E0 build-step the v0.4 Pioneers baton calls for:
//! the compile-in projection, NOT the fragile re-parse-source path (which
//! yields nothing on a consumer crate and would re-open the zero-hits-cliff).
//!
//! Drift-freedom: the catalog is *derived* from the same `#[antigen]`
//! declarations the rest of the crate uses, so it can never silently diverge
//! from the shipped stdlib (antigen's own `ParallelStateTrackersDiverge` class,
//! foreclosed by single-sourcing).

use std::fmt::Write as _;
use std::path::Path;

/// Stdlib module files whose `#[antigen(fingerprint = …)]` declarations are
/// source-walking flagships to bundle. We deliberately read each named module
/// rather than glob the directory so an added module is a conscious catalog
/// decision (recognition-not-design), and `dogfood.rs` (antigen-internal,
/// not consumer-facing flagships) is excluded by omission.
const STDLIB_FLAGSHIP_MODULES: &[&str] = &[
    "drop_panic.rs",
    "resource_lifecycle.rs",
    "panic_on_index.rs",
    "async_soundness.rs",
    "numeric_truncation.rs",
    "unsafe_soundness.rs",
    "deserialization.rs",
    "time_ordering.rs",
];

fn main() {
    let manifest_dir =
        std::env::var("CARGO_MANIFEST_DIR").expect("CARGO_MANIFEST_DIR set by cargo");
    let stdlib_dir = Path::new(&manifest_dir).join("src").join("stdlib");

    let mut entries: Vec<CatalogEntry> = Vec::new();
    for module in STDLIB_FLAGSHIP_MODULES {
        let path = stdlib_dir.join(module);
        // Re-run the build when any flagship module changes — the catalog is
        // derived from these files.
        println!("cargo:rerun-if-changed={}", path.display());
        let src = std::fs::read_to_string(&path)
            .unwrap_or_else(|e| panic!("build.rs: cannot read {}: {e}", path.display()));
        let file = syn::parse_file(&src)
            .unwrap_or_else(|e| panic!("build.rs: cannot parse {}: {e}", path.display()));
        collect_antigens(&file.items, &mut entries);
    }

    // Deterministic order (sorted by name) so the generated file is stable
    // across builds regardless of module read order.
    entries.sort_by(|a, b| a.name.cmp(&b.name));

    let mut generated = String::new();
    generated.push_str(
        "// @generated by antigen/build.rs — do not edit.\n\
         // The bundled stdlib catalog: (name, fingerprint-string, provenance-string).\n",
    );
    let _ = writeln!(
        generated,
        "pub(crate) const STDLIB_CATALOG: &[(&str, &str, &str)] = &[",
    );
    for e in &entries {
        // Each fingerprint string can contain anything; emit it as a raw string
        // with a guaranteed-unique hash delimiter so embedded `"#` never closes
        // it early.
        let _ = writeln!(
            generated,
            "    ({}, {}, {}),",
            raw_str(&e.name),
            raw_str(&e.fingerprint),
            raw_str(&e.provenance),
        );
    }
    generated.push_str("];\n");

    let out_dir = std::env::var("OUT_DIR").expect("OUT_DIR set by cargo");
    let out_path = Path::new(&out_dir).join("stdlib_catalog.rs");
    std::fs::write(&out_path, generated)
        .unwrap_or_else(|e| panic!("build.rs: cannot write {}: {e}", out_path.display()));
    println!("cargo:rerun-if-changed=build.rs");
}

/// One extracted catalog entry.
struct CatalogEntry {
    name: String,
    fingerprint: String,
    provenance: String,
}

/// Walk top-level items, pulling a catalog entry out of each item that carries
/// an `#[antigen(…)]` attribute with both a `name` and a `fingerprint`.
fn collect_antigens(items: &[syn::Item], out: &mut Vec<CatalogEntry>) {
    for item in items {
        let attrs = item_attrs(item);
        for attr in attrs {
            if !attr.path().is_ident("antigen") {
                continue;
            }
            if let Some(entry) = parse_antigen_attr(attr) {
                out.push(entry);
            }
        }
    }
}

/// The attribute list of any top-level item kind we model.
fn item_attrs(item: &syn::Item) -> &[syn::Attribute] {
    match item {
        syn::Item::Struct(i) => &i.attrs,
        syn::Item::Enum(i) => &i.attrs,
        syn::Item::Union(i) => &i.attrs,
        syn::Item::Fn(i) => &i.attrs,
        syn::Item::Trait(i) => &i.attrs,
        syn::Item::Type(i) => &i.attrs,
        syn::Item::Const(i) => &i.attrs,
        syn::Item::Static(i) => &i.attrs,
        syn::Item::Impl(i) => &i.attrs,
        syn::Item::Mod(i) => &i.attrs,
        _ => &[],
    }
}

/// Extract `(name, fingerprint, provenance)` from an `#[antigen(name = "…",
/// fingerprint = r#"…"#, provenance = Provenance::X, …)]` attribute. Returns
/// `None` if it lacks a `name` or a `fingerprint` (e.g. a CLI-discipline antigen
/// with no source-walking fingerprint — those are not bundled into the
/// source-scan catalog). Provenance defaults to `"Imagined"` (the honest floor,
/// mirroring `Provenance::DEFAULT`) when unauthored.
fn parse_antigen_attr(attr: &syn::Attribute) -> Option<CatalogEntry> {
    let mut name: Option<String> = None;
    let mut fingerprint: Option<String> = None;
    let mut provenance: Option<String> = None;

    // `parse_nested_meta` visits each `key = value` (and bare-path) entry.
    let parsed = attr.parse_nested_meta(|meta| {
        let key = meta
            .path
            .get_ident()
            .map(std::string::ToString::to_string)
            .unwrap_or_default();
        match key.as_str() {
            "name" => {
                let value = meta.value()?;
                let lit: syn::LitStr = value.parse()?;
                name = Some(lit.value());
            },
            "fingerprint" => {
                let value = meta.value()?;
                let lit: syn::LitStr = value.parse()?;
                fingerprint = Some(lit.value());
            },
            "provenance" => {
                // `provenance = Provenance::Constructable` — a path expr; take
                // its last segment as the variant name.
                let value = meta.value()?;
                let path: syn::Path = value.parse()?;
                if let Some(seg) = path.segments.last() {
                    provenance = Some(seg.ident.to_string());
                }
            },
            _ => {
                // Any other key (category, presentation, family, summary,
                // references, …): consume exactly ONE value expression so
                // `parse_nested_meta` can find the next `, key = …` entry. We
                // parse a `syn::Expr` (not a greedy `TokenStream`, which would
                // swallow every following key up to the closing paren and drop
                // the fingerprint). `references = [ … ]` parses as `ExprArray`.
                if meta.input.peek(syn::Token![=]) {
                    let value = meta.value()?;
                    let _: syn::Expr = value.parse()?;
                }
                // Bare-path entries (none expected today) need no value consume.
            },
        }
        Ok(())
    });

    // A malformed attribute is a hard build error — antigen's own stdlib must
    // parse. (parse_nested_meta surfaces the original span.)
    parsed.unwrap_or_else(|e| panic!("build.rs: malformed #[antigen] attribute: {e}"));

    let name = name?;
    let fingerprint = fingerprint?;
    let provenance = provenance.unwrap_or_else(|| "Imagined".to_string());
    Some(CatalogEntry {
        name,
        fingerprint,
        provenance,
    })
}

/// Emit a Rust string literal for `s`, using the minimal form so the generated
/// file stays clippy-clean (no unnecessary raw-string hashes):
/// - a plain `"…"` when `s` contains neither `"` nor `\`,
/// - otherwise a raw string `r#"…"#` with the **minimal** hash count that cannot
///   be terminated early by a `"#…` run inside `s`.
fn raw_str(s: &str) -> String {
    if !s.contains('"') && !s.contains('\\') {
        return format!("{s:?}"); // debug-format escapes nothing extra for a clean str
    }
    // Minimal hashes: the longest run of `#` that immediately FOLLOWS a `"` in
    // `s`, plus one. A raw string `r##"…"##` only terminates on `"##`, so only
    // `"`-preceded `#`-runs matter.
    let mut max_after_quote = 0usize;
    let bytes = s.as_bytes();
    let mut i = 0usize;
    while i < bytes.len() {
        if bytes[i] == b'"' {
            let mut run = 0usize;
            let mut j = i + 1;
            while j < bytes.len() && bytes[j] == b'#' {
                run += 1;
                j += 1;
            }
            max_after_quote = max_after_quote.max(run);
        }
        i += 1;
    }
    let hashes = "#".repeat(max_after_quote + 1);
    format!("r{hashes}\"{s}\"{hashes}")
}
