use anyhow::{Context, Result};
use clap::CommandFactory;
use std::fmt::Write as _;
use std::fs;
use std::path::Path;

#[path = "../cli.rs"]
mod cli;
#[path = "../config/mod.rs"]
mod config;

const GENERATED_BANNER: &str =
    "<!-- Generated by scripts/generate-docs.sh. Do not edit directly. -->";

fn main() -> Result<()> {
    let output_dir = Path::new("docs").join("reference");
    fs::create_dir_all(&output_dir).context("failed to create docs/reference directory")?;

    let cli_path = output_dir.join("cli.md");
    let cli_doc = render_cli_reference()?;
    fs::write(&cli_path, cli_doc)
        .with_context(|| format!("failed to write {}", cli_path.display()))?;

    let config_path = output_dir.join("config.md");
    let config_doc = render_config_reference();
    fs::write(&config_path, config_doc)
        .with_context(|| format!("failed to write {}", config_path.display()))?;

    Ok(())
}

fn render_cli_reference() -> Result<String> {
    let mut root = cli::Cli::command();
    root.build();

    let mut out = String::new();
    writeln!(&mut out, "# CLI Reference")?;
    writeln!(&mut out)?;
    writeln!(&mut out, "{}", GENERATED_BANNER)?;
    writeln!(&mut out)?;
    writeln!(
        &mut out,
        "This reference is generated from Batty's clap command tree."
    )?;
    writeln!(&mut out)?;

    let root_name = root.get_name().to_string();
    render_command_markdown(&root, &[root_name], &mut out)?;

    Ok(format!("{}\n", out.trim_end()))
}

fn render_command_markdown(
    command: &clap::Command,
    path: &[String],
    out: &mut String,
) -> Result<()> {
    let path_str = path.join(" ");
    writeln!(out, "## `{}`", path_str)?;
    writeln!(out)?;

    if let Some(about) = command.get_about() {
        writeln!(out, "{}", about)?;
        writeln!(out)?;
    }

    writeln!(out, "```text")?;
    writeln!(out, "{}", render_help_block(command.clone())?)?;
    writeln!(out, "```")?;
    writeln!(out)?;

    let mut children: Vec<_> = command
        .get_subcommands()
        .filter(|sub| sub.get_name() != "help")
        .cloned()
        .collect();
    children.sort_by(|a, b| a.get_name().cmp(b.get_name()));

    for child in children {
        let mut child_path = path.to_vec();
        child_path.push(child.get_name().to_string());
        render_command_markdown(&child, &child_path, out)?;
    }

    Ok(())
}

fn render_help_block(mut command: clap::Command) -> Result<String> {
    command = command.term_width(100).max_term_width(100);
    command = command.disable_colored_help(true);

    let mut buffer = Vec::new();
    command
        .write_long_help(&mut buffer)
        .context("failed to render clap help")?;
    let text = String::from_utf8(buffer).context("clap help was not valid UTF-8")?;
    Ok(text.trim_end().to_string())
}

fn render_config_reference() -> String {
    let defaults = config::ProjectConfig::default();

    let mut out = String::new();
    writeln!(&mut out, "# Configuration Reference").expect("write to string");
    writeln!(&mut out).expect("write to string");
    writeln!(&mut out, "{}", GENERATED_BANNER).expect("write to string");
    writeln!(&mut out).expect("write to string");
    writeln!(
        &mut out,
        "This reference is generated from Rust config defaults."
    )
    .expect("write to string");
    writeln!(&mut out).expect("write to string");

    writeln!(&mut out, "## Location").expect("write to string");
    writeln!(&mut out).expect("write to string");
    writeln!(
        &mut out,
        "Optional runtime defaults are read from `.batty/config.toml` when the file is present. Team topology and day-to-day runtime settings live in `.batty/team_config/team.yaml`."
    )
    .expect("write to string");
    writeln!(&mut out).expect("write to string");

    writeln!(&mut out, "## Fields").expect("write to string");
    writeln!(&mut out).expect("write to string");
    writeln!(&mut out, "| Key | Type | Default | Description |").expect("write to string");
    writeln!(&mut out, "| --- | --- | --- | --- |").expect("write to string");

    let rows = [
        (
            "defaults.agent",
            "string",
            defaults.defaults.agent.clone(),
            "Default agent name for runtime paths that consult `.batty/config.toml`.",
        ),
        (
            "defaults.policy",
            "enum (`observe`, `suggest`, `act`)",
            policy_name(defaults.defaults.policy).to_string(),
            "Default policy tier for prompt handling.",
        ),
        (
            "defaults.dod",
            "string or null",
            defaults
                .defaults
                .dod
                .clone()
                .unwrap_or_else(|| "(none)".to_string()),
            "Definition of done command run after task completion.",
        ),
        (
            "defaults.max_retries",
            "integer",
            defaults.defaults.max_retries.to_string(),
            "Maximum retries for failed DoD commands.",
        ),
        (
            "supervisor.enabled",
            "boolean",
            defaults.supervisor.enabled.to_string(),
            "Enable Tier 2 supervisor escalation.",
        ),
        (
            "supervisor.program",
            "string",
            defaults.supervisor.program.clone(),
            "Program used for supervisor calls.",
        ),
        (
            "supervisor.args",
            "array[string]",
            format!("[{}]", defaults.supervisor.args.join(", ")),
            "Arguments passed to the supervisor program.",
        ),
        (
            "supervisor.timeout_secs",
            "integer",
            defaults.supervisor.timeout_secs.to_string(),
            "Supervisor command timeout in seconds.",
        ),
        (
            "supervisor.trace_io",
            "boolean",
            defaults.supervisor.trace_io.to_string(),
            "Log supervisor prompts and responses for debugging.",
        ),
        (
            "detector.silence_timeout_secs",
            "integer",
            defaults.detector.silence_timeout_secs.to_string(),
            "Silence threshold before unknown-request fallback triggers.",
        ),
        (
            "detector.answer_cooldown_millis",
            "integer",
            defaults.detector.answer_cooldown_millis.to_string(),
            "Minimum delay between automatic answers.",
        ),
        (
            "detector.unknown_request_fallback",
            "boolean",
            defaults.detector.unknown_request_fallback.to_string(),
            "Escalate unresolved output to supervisor when no known prompt matches.",
        ),
        (
            "detector.idle_input_fallback",
            "boolean",
            defaults.detector.idle_input_fallback.to_string(),
            "Allow idle-output input prompts to trigger response handling.",
        ),
        (
            "dangerous_mode.enabled",
            "boolean",
            defaults.dangerous_mode.enabled.to_string(),
            "Enable dangerous-mode flags for supported agent wrappers.",
        ),
        (
            "policy.auto_answer",
            "table[string -> string]",
            "{}".to_string(),
            "Prompt-to-answer overrides for runtime paths that use this config.",
        ),
    ];

    for (key, kind, default, description) in rows {
        writeln!(
            &mut out,
            "| `{}` | {} | `{}` | {} |",
            key,
            kind.replace('|', "\\|"),
            default.replace('|', "\\|"),
            description
        )
        .expect("write to string");
    }

    writeln!(&mut out).expect("write to string");
    writeln!(&mut out, "## Default Template").expect("write to string");
    writeln!(&mut out).expect("write to string");
    writeln!(&mut out, "```toml").expect("write to string");
    writeln!(&mut out, "[defaults]").expect("write to string");
    writeln!(&mut out, "agent = \"{}\"", defaults.defaults.agent).expect("write to string");
    writeln!(
        &mut out,
        "policy = \"{}\"",
        policy_name(defaults.defaults.policy)
    )
    .expect("write to string");
    writeln!(&mut out, "max_retries = {}", defaults.defaults.max_retries).expect("write to string");
    writeln!(&mut out).expect("write to string");
    writeln!(&mut out, "[supervisor]").expect("write to string");
    writeln!(&mut out, "enabled = {}", defaults.supervisor.enabled).expect("write to string");
    writeln!(&mut out, "program = \"{}\"", defaults.supervisor.program).expect("write to string");
    writeln!(
        &mut out,
        "args = [{}]",
        defaults
            .supervisor
            .args
            .iter()
            .map(|arg| format!("\"{}\"", arg))
            .collect::<Vec<_>>()
            .join(", ")
    )
    .expect("write to string");
    writeln!(
        &mut out,
        "timeout_secs = {}",
        defaults.supervisor.timeout_secs
    )
    .expect("write to string");
    writeln!(&mut out, "trace_io = {}", defaults.supervisor.trace_io).expect("write to string");
    writeln!(&mut out).expect("write to string");
    writeln!(&mut out, "[detector]").expect("write to string");
    writeln!(
        &mut out,
        "silence_timeout_secs = {}",
        defaults.detector.silence_timeout_secs
    )
    .expect("write to string");
    writeln!(
        &mut out,
        "answer_cooldown_millis = {}",
        defaults.detector.answer_cooldown_millis
    )
    .expect("write to string");
    writeln!(
        &mut out,
        "unknown_request_fallback = {}",
        defaults.detector.unknown_request_fallback
    )
    .expect("write to string");
    writeln!(
        &mut out,
        "idle_input_fallback = {}",
        defaults.detector.idle_input_fallback
    )
    .expect("write to string");
    writeln!(&mut out).expect("write to string");
    writeln!(&mut out, "[dangerous_mode]").expect("write to string");
    writeln!(&mut out, "enabled = {}", defaults.dangerous_mode.enabled).expect("write to string");
    writeln!(&mut out).expect("write to string");
    writeln!(&mut out, "[policy.auto_answer]").expect("write to string");
    writeln!(&mut out, "\"Continue? [y/n]\" = \"y\"").expect("write to string");
    writeln!(&mut out, "```").expect("write to string");

    format!("{}\n", out.trim_end())
}

fn policy_name(policy: config::Policy) -> &'static str {
    match policy {
        config::Policy::Observe => "observe",
        config::Policy::Suggest => "suggest",
        config::Policy::Act => "act",
    }
}

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

    #[test]
    fn cli_reference_contains_core_commands() {
        let markdown = render_cli_reference().unwrap();
        assert!(markdown.contains("## `batty`"));
        assert!(markdown.contains("## `batty init`"));
        assert!(markdown.contains("## `batty start`"));
        assert!(markdown.contains("## `batty stop`"));
        assert!(markdown.contains("## `batty status`"));
        assert!(markdown.contains("## `batty send`"));
    }

    #[test]
    fn config_reference_contains_default_values() {
        let markdown = render_config_reference();
        assert!(markdown.contains("`defaults.agent`"));
        assert!(markdown.contains("`defaults.max_retries`"));
        assert!(markdown.contains("[supervisor]"));
        assert!(markdown.contains("[detector]"));
    }
}
