//! `tatara-rust-docs` — the catalog **is** the documentation source of truth.
//!
//! The macro farm already treats every repeating impl as a typed Spec
//! and emits the proc-macro crate mechanically (★★ EMITTER SUBSTRATE).
//! This crate closes the loop on the *other* artifact every published
//! derive needs — its documentation — by rendering it from the same
//! [`MacroCatalogSpec`]. The fleet stops hand-maintaining a docs table
//! that drifts from the catalog (the 2026-05-28 `macro-farm.md` listed
//! 15 derives while the catalog had grown to 26).
//!
//! Three artifacts, all in pleme-io house style, all regenerated by one
//! command so they can never drift from the catalog:
//!
//! | Artifact | House style | Audience |
//! |---|---|---|
//! | [`DocBundle::reference_md`] | `./docs/` reference table | humans browsing the family |
//! | [`DocBundle::claude_fragment_md`] | `CLAUDE.md` ★★ section | agents loading repo context |
//! | [`DocBundle::skill_md`] | `skills/<name>/SKILL.md` | agents deciding *when* to reach for a derive |
//!
//! This is the typescape discipline applied to prose: a typed source
//! (`MacroCatalogSpec`) rendered to a target artifact (markdown), with a
//! reflection test (`reference lists every entry`) that makes drift a
//! compile-time-detectable error instead of a documentation rot.
//!
//! Pure rendering (`render_*`) is I/O-free; [`write_bundle`] is the only
//! function that touches disk.

use std::io;
use std::path::Path;

use tatara_rust_catalog::{CatalogEntry, MacroCatalogSpec};

/// The regeneration provenance stamped into every artifact so a reader
/// (human or agent) knows it is generated and how to refresh it.
const REGEN_HINT: &str =
    "tatara-rust-forge catalog-emit-docs catalogs/pleme-derives.lisp --out .";

/// A complete documentation bundle rendered from one catalog.
#[derive(Clone, Debug)]
pub struct DocBundle {
    /// `./docs/derives-reference.md` — the canonical family table.
    pub reference_md: String,
    /// A `CLAUDE.md` ★★ section — repo context for agents.
    pub claude_fragment_md: String,
    /// `skills/consume-pleme-derives/SKILL.md` — when-to-use guidance.
    pub skill_md: String,
}

/// Render every house-style doc artifact for a catalog. Pure — no I/O.
pub fn render_docs(catalog: &MacroCatalogSpec) -> DocBundle {
    DocBundle {
        reference_md: render_reference(catalog),
        claude_fragment_md: render_claude_fragment(catalog),
        skill_md: render_skill(catalog),
    }
}

/// Write a [`DocBundle`] to the standard pleme-io layout under `out`:
///
/// ```text
/// <out>/docs/derives-reference.md
/// <out>/docs/CLAUDE-derives-fragment.md
/// <out>/skills/consume-pleme-derives/SKILL.md
/// ```
pub fn write_bundle(bundle: &DocBundle, out: &Path) -> io::Result<Vec<std::path::PathBuf>> {
    let docs_dir = out.join("docs");
    let skill_dir = out.join("skills").join("consume-pleme-derives");
    std::fs::create_dir_all(&docs_dir)?;
    std::fs::create_dir_all(&skill_dir)?;

    let reference = docs_dir.join("derives-reference.md");
    let claude = docs_dir.join("CLAUDE-derives-fragment.md");
    let skill = skill_dir.join("SKILL.md");

    std::fs::write(&reference, &bundle.reference_md)?;
    std::fs::write(&claude, &bundle.claude_fragment_md)?;
    std::fs::write(&skill, &bundle.skill_md)?;

    Ok(vec![reference, claude, skill])
}

// ─────────────────────────────────────────────────────────────────────
// Renderers
// ─────────────────────────────────────────────────────────────────────

/// `#[derive(Trait)]` usage string for an entry, or a `macro!` / `#[attr]`
/// form for non-derive kinds. Routed through the catalog's own
/// `trait_name_for_verifier()` so the doc label and the verifier label
/// are the same identifier — they cannot disagree.
fn usage_token(entry: &CatalogEntry) -> String {
    // `CatalogSpec::trait_name_for_verifier` is `Option` only to satisfy
    // a uniform accessor signature — every catalog spec kind has one.
    let name = entry.spec.trait_name_for_verifier().unwrap_or("?");
    match entry.spec.kind_label() {
        "proc-attr" => format!("#[{name}]"),
        "proc-fn" | "macro-rules" => format!("{name}!"),
        _ => format!("#[derive({name})]"),
    }
}

/// Stable kind ordering for the family table — groups the reader sees
/// top-to-bottom. Unknown kinds sort last, alphabetically.
fn kind_rank(kind: &str) -> usize {
    match kind {
        "per-field" => 0,
        "per-variant" => 1,
        "newtype" => 2,
        "enum-fold" => 3,
        "derive" => 4,
        "proc-attr" => 5,
        "proc-fn" => 6,
        "macro-rules" => 7,
        "composite" => 8,
        _ => 99,
    }
}

/// Entries sorted by (kind-rank, crate-name) — the canonical doc order.
fn sorted_entries(catalog: &MacroCatalogSpec) -> Vec<&CatalogEntry> {
    let mut v: Vec<&CatalogEntry> = catalog.entries.iter().collect();
    v.sort_by(|a, b| {
        kind_rank(a.spec.kind_label())
            .cmp(&kind_rank(b.spec.kind_label()))
            .then_with(|| a.spec.kind_label().cmp(b.spec.kind_label()))
            .then_with(|| a.crate_name.cmp(&b.crate_name))
    });
    v
}

/// `| Crate | Usage | Kind | Since | Description |` table over every entry.
fn family_table(catalog: &MacroCatalogSpec) -> String {
    let mut s = String::new();
    s.push_str("| Crate | Usage | Kind | Since | Description |\n");
    s.push_str("|---|---|---|---|---|\n");
    for e in sorted_entries(catalog) {
        s.push_str(&format!(
            "| `{}` | `{}` | {} | {} | {} |\n",
            e.crate_name,
            usage_token(e),
            e.spec.kind_label(),
            e.since,
            e.description.replace('|', "\\|"),
        ));
    }
    s
}

fn render_reference(catalog: &MacroCatalogSpec) -> String {
    let n = catalog.entries.len();
    format!(
        "<!-- GENERATED by tatara-rust-docs from the macro-farm catalog. DO NOT EDIT BY HAND. -->\n\
         <!-- Regenerate: {REGEN_HINT} -->\n\n\
         # {title} — derive reference\n\n\
         The pleme-io macro farm publishes **{n}** proc-macro derive crates, each\n\
         emitted mechanically from one catalog entry (★★ EMITTER SUBSTRATE —\n\
         *every repeating impl is a spec; emit, don't author*). This table is a\n\
         generated mirror of the catalog: it is regenerated, never edited, so it\n\
         can never drift from what is actually published.\n\n\
         {table}\n\
         ## Adopting these across a crate\n\n\
         Don't hand-write `#[derive(...)]` site-by-site — let the survey find and\n\
         apply them:\n\n\
         ```bash\n\
         # what could this crate adopt?\n\
         tatara-rust-forge survey <crate-src-path>\n\
         # apply every candidate atomically (rolls back if the cargo gate goes red)\n\
         tatara-rust-forge survey-apply-all <crate-root> --write\n\
         ```\n\n\
         ## Is more farming worth it?\n\n\
         The diminishing-returns metric scores the whole fleet and tells you\n\
         whether to keep adopting or to stop:\n\n\
         ```bash\n\
         tatara-rust-forge survey-fleet-returns <org-root>\n\
         ```\n\n\
         A `Plateau` verdict means the mechanical-impl toil has been harvested —\n\
         stop refactoring and let new code consume the derives as it is written.\n",
        title = catalog.title,
        table = family_table(catalog),
    )
}

fn render_claude_fragment(catalog: &MacroCatalogSpec) -> String {
    let n = catalog.entries.len();
    format!(
        "<!-- GENERATED by tatara-rust-docs from the macro-farm catalog. DO NOT EDIT BY HAND. -->\n\
         <!-- Regenerate: {REGEN_HINT} -->\n\n\
         ## Derive family (★★ EMITTER SUBSTRATE)\n\n\
         > **★★ EMITTER SUBSTRATE.** Every repeating impl is a spec; emit, don't\n\
         > author. The {n} derives below are published proc-macro crates emitted\n\
         > from `catalogs/pleme-derives.lisp`. Need a new mechanical impl shape?\n\
         > Add a catalog entry and run `catalog-instantiate` — do not hand-write\n\
         > the proc-macro, and do not hand-write the impl blocks it would replace.\n\n\
         {table}\n\
         **Consuming a derive:** add the crate as a dependency and write the\n\
         `Usage` token on the target type. **Adopting fleet-wide:** run\n\
         `tatara-rust-forge survey-apply-all <crate-root> --write` (atomic,\n\
         gate-checked, rolls back on red). **Deciding whether to keep farming:**\n\
         `tatara-rust-forge survey-fleet-returns <org-root>` emits the\n\
         diminishing-returns verdict (ContinueFarming vs Plateau).\n",
        table = family_table(catalog),
    )
}

fn render_skill(catalog: &MacroCatalogSpec) -> String {
    let n = catalog.entries.len();
    format!(
        "---\n\
         name: consume-pleme-derives\n\
         description: >-\n\
         \x20 Reach for a published pleme-io derive instead of hand-writing a mechanical\n\
         \x20 impl block (per-field getter/setter/builder, per-variant predicate, newtype\n\
         \x20 delegation, enum-fold constant table). Use when adding such a method to a\n\
         \x20 struct or enum, when reviewing a diff full of boilerplate impls, or when\n\
         \x20 deciding whether a crate should adopt the derives. Backed by the macro farm\n\
         \x20 ({n} derives) and the survey/returns tooling in tatara-rust-forge.\n\
         ---\n\n\
         <!-- GENERATED by tatara-rust-docs from the macro-farm catalog. Regenerate: {REGEN_HINT} -->\n\n\
         # consume-pleme-derives\n\n\
         The macro farm has already turned the fleet's recurring impl shapes into\n\
         published derives. Before hand-writing one of the shapes below, **derive\n\
         it**. Before merging a diff full of them, **survey it**.\n\n\
         ## The published derives\n\n\
         {table}\n\
         ## Workflow\n\n\
         1. **Adding a mechanical method by hand?** Find its row above and write the\n\
         \x20  `Usage` token on the type instead.\n\
         2. **Reviewing / cleaning a crate?** Let the survey find every adoptable\n\
         \x20  site and apply them atomically:\n\
         \x20  ```bash\n\
         \x20  tatara-rust-forge survey <crate-src-path>            # discover\n\
         \x20  tatara-rust-forge survey-apply-all <crate-root> --write  # apply + gate + rollback\n\
         \x20  ```\n\
         3. **Deciding whether more adoption pays?** Run the diminishing-returns\n\
         \x20  metric. `ContinueFarming` → harvest; `Plateau` → stop refactoring and\n\
         \x20  let new code consume the derives as written:\n\
         \x20  ```bash\n\
         \x20  tatara-rust-forge survey-fleet-returns <org-root>\n\
         \x20  ```\n\
         4. **A shape you need isn't published?** That's a catalog gap, not a\n\
         \x20  hand-impl excuse — add an entry to `catalogs/pleme-derives.lisp` and run\n\
         \x20  `tatara-rust-forge catalog-instantiate` (see the `macro-farm-author` skill).\n\n\
         ## Why this is safe\n\n\
         `survey-apply-all` is atomic per crate: it applies the rewrite, injects the\n\
         dependency, runs `cargo build && cargo test`, and **restores every file if\n\
         the gate goes red**. An optimistic candidate that doesn't actually compile\n\
         (e.g. an enum embedded in trait machinery) is rolled back, never landed —\n\
         so adopting is a no-risk operation.\n",
        table = family_table(catalog),
    )
}

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

    /// A tiny two-entry catalog built through serde so the test doesn't
    /// depend on the spec constructors' exact shape.
    fn sample_catalog() -> MacroCatalogSpec {
        let json = r#"{
            "title": "test-derives",
            "entries": [
                {
                    "crate_name": "pleme-getter-derive",
                    "description": "Per-field inherent getter.",
                    "since": "0.1.0",
                    "owner": "pleme-io",
                    "kind": "per-field",
                    "spec": {
                        "trait_name": "GetterAll",
                        "target": "named-struct",
                        "per_field_template": "pub fn #field_name(&self) -> &#field_ty { &self.#field_name }"
                    }
                },
                {
                    "crate_name": "pleme-asref-derive",
                    "description": "Newtype AsRef delegation.",
                    "since": "0.1.0",
                    "owner": "pleme-io",
                    "kind": "newtype",
                    "spec": {
                        "trait_name": "AsRefNewtype",
                        "target": "tuple",
                        "impl_template": "impl AsRef<#inner_ty> for #self_name { fn as_ref(&self) -> &#inner_ty { &self.0 } }"
                    }
                }
            ]
        }"#;
        serde_json::from_str(json).expect("sample catalog parses")
    }

    /// ★★ CATALOG REFLECTION: the reference must mention EVERY catalog
    /// entry's crate name and usage token. This is the test that makes
    /// doc-drift impossible — a new catalog entry that the renderer
    /// forgets trips here.
    #[test]
    fn reference_lists_every_entry() {
        let catalog = sample_catalog();
        let md = render_reference(&catalog);
        for e in &catalog.entries {
            assert!(
                md.contains(&e.crate_name),
                "reference missing crate {}",
                e.crate_name
            );
            let tn = e.spec.trait_name_for_verifier().unwrap_or("");
            assert!(md.contains(tn), "reference missing trait {tn}");
        }
    }

    #[test]
    fn skill_has_frontmatter_and_every_entry() {
        let catalog = sample_catalog();
        let md = render_skill(&catalog);
        assert!(md.starts_with("---\n"), "skill needs YAML frontmatter");
        assert!(md.contains("name: consume-pleme-derives"));
        for e in &catalog.entries {
            assert!(md.contains(&e.crate_name), "skill missing {}", e.crate_name);
        }
    }

    #[test]
    fn claude_fragment_carries_emitter_substrate_marker() {
        let md = render_claude_fragment(&sample_catalog());
        assert!(md.contains("★★ EMITTER SUBSTRATE"));
        assert!(md.contains("survey-fleet-returns"));
    }

    #[test]
    fn usage_token_forms_per_kind() {
        let catalog = sample_catalog();
        let getter = &catalog.entries[0];
        assert_eq!(usage_token(getter), "#[derive(GetterAll)]");
    }

    /// The whole bundle round-trips to disk in the house layout.
    #[test]
    fn write_bundle_lays_out_house_paths() {
        let catalog = sample_catalog();
        let bundle = render_docs(&catalog);
        let out = std::env::temp_dir().join(format!("trd-{}", std::process::id()));
        let _ = std::fs::remove_dir_all(&out);
        let written = write_bundle(&bundle, &out).unwrap();
        assert_eq!(written.len(), 3);
        assert!(out.join("docs/derives-reference.md").exists());
        assert!(out.join("skills/consume-pleme-derives/SKILL.md").exists());
    }
}
