//! Stub-the-Design output mode — generates code skeletons from BDR atoms.
//!
//! Given decomposed atoms (especially `TechnicalSpec` and `Constraint` kinds),
//! emits a minimal Rust stub file into the target repo. The stub file contains
//! function signatures and doc comments but no logic (`todo!()`).
//!
//! Reviewing the stub PR is reviewing the design before any implementation.
//! This mitigates AI anchoring on premature implementations.

use bdr::atom::{Atom, AtomKind};

/// Generate a Rust stub file from decomposed atoms.
///
/// Only `TechnicalSpec` and `Constraint` atoms produce stubs — other kinds
/// are design context, not API contracts.
///
/// Returns the generated file content as a string. Returns `None` when no
/// stub-worthy atoms are found (nothing to emit).
pub fn generate_stubs(decade_title: &str, atoms: &[Atom]) -> Option<String> {
    let stubs: Vec<String> = atoms
        .iter()
        .filter(|a| matches!(a.kind, AtomKind::TechnicalSpec | AtomKind::Constraint))
        .map(atom_to_stub)
        .collect();

    if stubs.is_empty() {
        return None;
    }

    let slug = slugify(decade_title);
    let header = format!(
        "//! Stubs generated from: {decade_title}\n\
         //! Review these type signatures and function stubs before implementing.\n\
         //! This file is auto-generated by `rsry decompose --stub-output`.\n\
         //! Delete or replace once implementation is complete.\n\
         #![allow(unused, dead_code)]\n\
         \n\
         /// Stubs for: {decade_title}\n\
         pub mod {slug} {{\n"
    );

    let body = stubs.join("\n");

    Some(format!("{header}{body}\n}}\n"))
}

fn atom_to_stub(atom: &Atom) -> String {
    let fn_name = slugify(&atom.title);
    let doc_title = atom.title.replace('"', "'");
    let kind_label = match atom.kind {
        AtomKind::TechnicalSpec => "spec",
        AtomKind::Constraint => "constraint",
        _ => "stub",
    };

    // Wrap body text into doc comment lines (≤80 chars each)
    let body_lines: String = wrap_doc(&atom.body)
        .into_iter()
        .map(|line| format!("    /// {line}\n"))
        .collect();

    let refs: String = if atom.references.is_empty() {
        String::new()
    } else {
        let ref_list = atom
            .references
            .iter()
            .map(|r| format!("    ///   - {r}"))
            .collect::<Vec<_>>()
            .join("\n");
        format!("    ///\n    /// References:\n{ref_list}\n")
    };

    format!(
        "    /// [{kind_label}] {doc_title}\n\
         ///\n\
         {body_lines}\
         {refs}\
         \
         #[allow(dead_code)]\n\
         fn {fn_name}() {{\n\
         \
             todo!(\"{doc_title}\")\n\
         }}\n"
    )
}

/// Slug: lowercase, alphanumeric + underscore, starts with letter.
fn slugify(s: &str) -> String {
    let slug: String = s
        .chars()
        .map(|c| {
            if c.is_alphanumeric() {
                c.to_lowercase().next().unwrap()
            } else {
                '_'
            }
        })
        .collect::<String>()
        .split('_')
        .filter(|p| !p.is_empty())
        .collect::<Vec<_>>()
        .join("_");

    // Ensure it starts with a letter
    if slug.starts_with(|c: char| c.is_ascii_digit()) {
        format!("s_{slug}")
    } else if slug.is_empty() {
        "stub".to_string()
    } else {
        slug
    }
}

/// Wrap text at word boundaries to at most `width` chars per line.
fn wrap_doc(text: &str) -> Vec<String> {
    const WIDTH: usize = 76;
    let mut lines = Vec::new();
    for paragraph in text.split("\n\n") {
        let words: Vec<&str> = paragraph.split_whitespace().collect();
        let mut current = String::new();
        for word in words {
            if current.is_empty() {
                current.push_str(word);
            } else if current.len() + 1 + word.len() <= WIDTH {
                current.push(' ');
                current.push_str(word);
            } else {
                lines.push(current.clone());
                current = word.to_string();
            }
        }
        if !current.is_empty() {
            lines.push(current);
        }
        lines.push(String::new()); // paragraph break
    }
    // Remove trailing blank line
    while lines.last().is_some_and(|l| l.is_empty()) {
        lines.pop();
    }
    lines
}

/// Write stub file(s) into the target repo under `.rsry-stubs/`.
///
/// Returns the path to the written stub file, or `None` if no stubs were generated.
pub fn write_stubs(
    target_repo: &std::path::Path,
    decade_title: &str,
    atoms: &[Atom],
) -> anyhow::Result<Option<std::path::PathBuf>> {
    let Some(content) = generate_stubs(decade_title, atoms) else {
        return Ok(None);
    };

    let stub_dir = target_repo.join(".rsry-stubs");
    std::fs::create_dir_all(&stub_dir)?;

    let file_name = format!("{}.rs", slugify(decade_title));
    let path = stub_dir.join(&file_name);
    std::fs::write(&path, &content)?;

    Ok(Some(path))
}

#[cfg(test)]
mod stub_output {
    use super::*;
    use bdr::atom::{Atom, AtomKind};

    fn make_atom(kind: AtomKind, title: &str, body: &str) -> Atom {
        Atom {
            kind,
            title: title.to_string(),
            body: body.to_string(),
            source_line: 1,
            source_section: "Test".to_string(),
            references: vec![],
        }
    }

    fn make_atom_with_refs(kind: AtomKind, title: &str, body: &str, refs: Vec<&str>) -> Atom {
        Atom {
            kind,
            title: title.to_string(),
            body: body.to_string(),
            source_line: 1,
            source_section: "Test".to_string(),
            references: refs.into_iter().map(str::to_string).collect(),
        }
    }

    // --- generate_stubs ---

    #[test]
    fn technical_spec_generates_stub() {
        let atoms = vec![make_atom(
            AtomKind::TechnicalSpec,
            "Add plugin_kind field",
            "Discriminator for hook vs mcp vs dispatch.",
        )];
        let output = generate_stubs("plugin substrate", &atoms).unwrap();
        assert!(
            output.contains("fn add_plugin_kind_field()"),
            "expected fn: {output}"
        );
        assert!(output.contains("todo!("), "expected todo: {output}");
        assert!(output.contains("[spec]"), "expected spec label: {output}");
    }

    #[test]
    fn constraint_generates_stub() {
        let atoms = vec![make_atom(
            AtomKind::Constraint,
            "Plugin names must be unique",
            "Registry rejects duplicate plugin names.",
        )];
        let output = generate_stubs("plugin substrate", &atoms).unwrap();
        assert!(
            output.contains("[constraint]"),
            "expected constraint label: {output}"
        );
        assert!(output.contains("fn plugin_names_must_be_unique()"));
    }

    #[test]
    fn non_spec_atoms_excluded() {
        let atoms = vec![
            make_atom(AtomKind::Decision, "Use async runtime", "Tokio."),
            make_atom(AtomKind::FrictionPoint, "Slow tests", "Need faster CI."),
            make_atom(AtomKind::OpenQuestion, "Which model?", "TBD."),
        ];
        let output = generate_stubs("infra", &atoms);
        assert!(output.is_none(), "non-spec atoms should produce no stubs");
    }

    #[test]
    fn mixed_atoms_only_spec_and_constraint_stubbed() {
        let atoms = vec![
            make_atom(AtomKind::Decision, "Use Dolt", "Version-controlled SQL."),
            make_atom(
                AtomKind::TechnicalSpec,
                "Bead ID format",
                "8 hex chars, url-safe.",
            ),
            make_atom(AtomKind::Constraint, "Max 64 plugins", "Registry limit."),
        ];
        let output = generate_stubs("storage", &atoms).unwrap();
        assert!(
            output.contains("fn bead_id_format()"),
            "spec stub missing: {output}"
        );
        assert!(
            output.contains("fn max_64_plugins()"),
            "constraint stub missing: {output}"
        );
        assert!(
            !output.contains("fn use_dolt()"),
            "decision should not appear: {output}"
        );
    }

    #[test]
    fn stub_file_wrapped_in_module() {
        let atoms = vec![make_atom(
            AtomKind::TechnicalSpec,
            "Wire GitHub endpoint",
            "POST /webhook/github.",
        )];
        let output = generate_stubs("GitHub Integration", &atoms).unwrap();
        assert!(
            output.contains("pub mod github_integration {"),
            "missing module: {output}"
        );
        assert!(output.ends_with("}\n"), "module not closed: {output}");
    }

    #[test]
    fn references_appear_in_stub_doc() {
        let atoms = vec![make_atom_with_refs(
            AtomKind::TechnicalSpec,
            "Connect MCP client",
            "Connect outbound to child MCP servers.",
            vec!["rosary-5410b6", "rmcp-docs"],
        )];
        let output = generate_stubs("mcp substrate", &atoms).unwrap();
        assert!(
            output.contains("rosary-5410b6"),
            "ref should appear in doc: {output}"
        );
    }

    #[test]
    fn empty_atoms_returns_none() {
        assert!(generate_stubs("empty", &[]).is_none());
    }

    // --- slugify ---

    #[test]
    fn slugify_basic() {
        assert_eq!(slugify("Add plugin kind"), "add_plugin_kind");
    }

    #[test]
    fn slugify_strips_special_chars() {
        assert_eq!(slugify("plugin: kind/axis"), "plugin_kind_axis");
    }

    #[test]
    fn slugify_leading_digit() {
        assert_eq!(slugify("4th generation"), "s_4th_generation");
    }

    #[test]
    fn slugify_empty() {
        assert_eq!(slugify(""), "stub");
    }

    // --- write_stubs ---

    #[test]
    fn write_stubs_creates_file() {
        let dir = tempfile::tempdir().unwrap();
        let atoms = vec![make_atom(
            AtomKind::TechnicalSpec,
            "Write stub test",
            "Verify file creation.",
        )];
        let path = write_stubs(dir.path(), "test decade", &atoms)
            .unwrap()
            .unwrap();
        assert!(path.exists(), "stub file should exist: {}", path.display());
        let content = std::fs::read_to_string(&path).unwrap();
        assert!(
            content.contains("fn write_stub_test()"),
            "missing fn: {content}"
        );
    }

    #[test]
    fn write_stubs_returns_none_for_no_stubs() {
        let dir = tempfile::tempdir().unwrap();
        let atoms = vec![make_atom(AtomKind::Decision, "Use Dolt", "Because.")];
        let result = write_stubs(dir.path(), "test", &atoms).unwrap();
        assert!(result.is_none());
    }

    #[test]
    fn write_stubs_creates_rsry_stubs_dir() {
        let dir = tempfile::tempdir().unwrap();
        let atoms = vec![make_atom(
            AtomKind::Constraint,
            "Max 64 plugins",
            "Registry limit.",
        )];
        write_stubs(dir.path(), "limits", &atoms).unwrap();
        assert!(dir.path().join(".rsry-stubs").is_dir());
    }
}
