//! Turning a spec into a Go program that declares the same CLI in cobra.
//!
//! The point is a measurement nobody has to take on trust. usage-go's row in
//! `go/README.md` is reproducible — `mise run perf:go` — and cobra's was measured
//! by hand against a program that was not in the repository, which makes it a
//! claim rather than a number. Generated from the same spec, by the same
//! traversal that writes the usage tables, the two rows describe the same CLI and
//! the comparison is between parsers rather than between two people's
//! transcriptions of mise.
//!
//! Emitted as Go rather than through `shadow.rs`, which writes Rust: the shape is
//! not a variation on the derive's — cobra builds its tree with statements, one
//! `&cobra.Command{…}` and a `Flags()` call per entry — and threading a language
//! through that emitter would obscure both.
//!
//! What cobra cannot express is counted and printed, as the Rust dialects do. A
//! shadow that quietly dropped half the spec would measure a smaller CLI and
//! flatter the framework it was declaring.

use super::*;

/// What a spec property turned into, or why it did not.
#[derive(Default)]
struct Skipped {
    counts: BTreeMap<&'static str, usize>,
}

impl Skipped {
    fn note(&mut self, what: &'static str) {
        *self.counts.entry(what).or_default() += 1;
    }

    fn report(&self) {
        if self.counts.is_empty() {
            println!("  nothing dropped: cobra expressed the whole spec");
            return;
        }
        println!("  dropped, because cobra cannot express it:");
        for (what, n) in &self.counts {
            println!("    {what}: {n}");
        }
    }
}

/// Write a cobra program declaring `spec_path`'s CLI into `out_dir`.
pub fn generate(spec_path: &Path, out_dir: &Path) {
    let kdl = match std::fs::read_to_string(spec_path) {
        Ok(kdl) => kdl,
        Err(e) => fail(&format!("reading {}: {e}", spec_path.display())),
    };
    let spec: Spec = match kdl.parse() {
        Ok(spec) => spec,
        Err(e) => fail(&format!("parsing {}: {e}", spec_path.display())),
    };

    let mut skipped = Skipped::default();
    let source = render(&spec, spec_path, &mut skipped);

    if let Err(e) = std::fs::create_dir_all(out_dir) {
        fail(&format!("creating {}: {e}", out_dir.display()));
    }
    let main = out_dir.join("main.go");
    if let Err(e) = std::fs::write(&main, source) {
        fail(&format!("writing {}: {e}", main.display()));
    }
    println!("cobra shadow: {}", main.display());
    skipped.report();
}

fn render(spec: &Spec, spec_path: &Path, skipped: &mut Skipped) -> String {
    let mut out = String::new();
    let bin = &spec.bin;

    let _ = writeln!(
        out,
        "// Code generated by `xtask gen-shadow {} <out> cobra`. DO NOT EDIT.\n\
         //\n\
         // {} declared in cobra, from the same spec the usage tables are generated from, so\n\
         // that the two rows of `go/README.md`'s table describe the same CLI. Regenerate with\n\
         // `mise run gen-shadow` rather than editing: a hand-edit here is a difference no\n\
         // reviewer can see, in a program whose whole job is to be comparable.\n\
         //\n\
         // The tree is built inside the loop on purpose. That is what cobra does on every\n\
         // process start — a `&cobra.Command` per subcommand, each with its own flag set — and\n\
         // it is the cost the comparison is about. Hoisting it out would measure a parser\n\
         // against a program that had already paid for its model.\n\
         package main\n\n\
         import (\n\
         \t\"fmt\"\n\
         \t\"os\"\n\
         \t\"strconv\"\n\n\
         \t\"github.com/spf13/cobra\"\n\
         )\n",
        spec_path.display(),
        bin,
    );

    let mut body = String::new();
    // A counter rather than a path of indices: `root1` and `root1`'s own eleventh child both
    // spell themselves `root11`, and Go says so with "no new variables on left side of :=".
    let mut next = 0usize;
    emit_command(&mut body, &spec.cmd, "root", bin, &mut next, skipped);

    let _ = writeln!(
        out,
        "// build declares the whole CLI, as a cobra program's `init` or `main` would.\n\
         func build() *cobra.Command {{\n\
         {body}\treturn root\n\
         }}\n"
    );

    // The same protocol as `go/internal/bench/parse-n`, so the two programs' numbers can sit
    // in one table: N binds of the same command line, and one line saying whether the last
    // one arrived somewhere.
    let _ = writeln!(
        out,
        "func main() {{\n\
         \tn := 1\n\
         \tif v, err := strconv.Atoi(os.Getenv(\"PARSE_N\")); err == nil {{\n\
         \t\tn = v\n\
         \t}}\n\n\
         \t// Find and ParseFlags rather than Execute: the comparison is about resolving a\n\
         \t// command line, and Execute would run the command as well. This is the same work\n\
         \t// the usage side does — reach a command, bind its flags — and no more.\n\
         \tseen := 0\n\
         \tfor i := 0; i < n; i++ {{\n\
         \t\troot := build()\n\
         \t\tcmd, flags, err := root.Find(os.Args[1:])\n\
         \t\tif err == nil {{\n\
         \t\t\terr = cmd.ParseFlags(flags)\n\
         \t\t}}\n\
         \t\tif err == nil && cmd != root {{\n\
         \t\t\tseen = 1\n\
         \t\t}}\n\
         \t}}\n\
         \tfmt.Println(seen)\n\
         }}"
    );
    out
}

/// One command and everything under it, as statements building `var`.
fn emit_command(
    out: &mut String,
    cmd: &SpecCommand,
    var: &str,
    use_name: &str,
    next: &mut usize,
    skipped: &mut Skipped,
) {
    let mut fields = vec![format!("Use: {}", go_string(use_name))];
    if let Some(help) = cmd.help.as_deref().or(cmd.help_long.as_deref()) {
        fields.push(format!("Short: {}", go_string(help)));
    }
    if let Some(long) = &cmd.help_long {
        fields.push(format!("Long: {}", go_string(long)));
    }
    // Both lists: cobra has one `Aliases`, and hiding an alias is not something it can say.
    let aliases: Vec<String> = cmd
        .aliases
        .iter()
        .chain(cmd.hidden_aliases.iter())
        .cloned()
        .collect();
    if !aliases.is_empty() {
        fields.push(format!("Aliases: {}", string_slice(&aliases)));
    }
    if !cmd.hidden_aliases.is_empty() {
        skipped.note("hidden aliases (cobra hides a command, not an alias)");
    }
    if cmd.hide {
        fields.push("Hidden: true".to_string());
    }
    // A leaf needs a Run for cobra to treat it as runnable; without one it prints help and
    // `Find` still resolves it, which is all this program asks of it.
    fields.push("Run: func(*cobra.Command, []string) {}".to_string());

    let _ = writeln!(out, "\t{var} := &cobra.Command{{{}}}", fields.join(", "));

    for flag in &cmd.flags {
        emit_flag(out, flag, var, skipped);
    }
    for arg in &cmd.args {
        note_arg(arg, skipped);
    }

    for (name, sub) in &cmd.subcommands {
        // An alias is keyed beside the canonical name in this map, and declaring the command
        // twice would say the tree is bigger than it is.
        if name != &sub.name {
            continue;
        }
        *next += 1;
        let child = format!("cmd{next}");
        emit_command(out, sub, &child, name, next, skipped);
        let _ = writeln!(out, "\t{var}.AddCommand({child})");
    }

    // One per mount, wherever it is. mise's are on `run` and `tasks` rather than at the root,
    // so a check that only looked there dropped them without saying so — in the one report
    // this whole shadow relies on for honesty. Counted per entry because a command may graft
    // in more than one, and the report is a count.
    for _ in &cmd.mounts {
        skipped.note("mounts (another spec grafted in at run time)");
    }
}

/// One flag, registered on the command that declares it.
fn emit_flag(out: &mut String, flag: &SpecFlag, var: &str, skipped: &mut Skipped) {
    let Some(long) = flag.long.first() else {
        // cobra has no vocabulary for a short-only flag: `Flags().BoolP` takes a name and a
        // shorthand, and the name is the long form.
        skipped.note("short-only flags");
        return;
    };
    let short = flag
        .short
        .first()
        .map(|c| c.to_string())
        .unwrap_or_default();
    let help = flag
        .help
        .as_deref()
        .or(flag.help_first_line.as_deref())
        .unwrap_or_default();

    // Which flag set: a global belongs to the command's persistent set, which is how cobra
    // spells "in scope for everything below".
    let set = if flag.global {
        "PersistentFlags"
    } else {
        "Flags"
    };

    let (method, default) = match (&flag.arg, flag.count, flag.var) {
        (None, true, _) => ("Count", String::new()),
        (None, false, _) => ("Bool", "false".to_string()),
        (Some(_), _, true) => ("StringArray", "nil".to_string()),
        (Some(_), _, false) => (
            "String",
            go_string(flag.default.first().map(String::as_str).unwrap_or("")),
        ),
    };

    let with_short = if short.is_empty() { "" } else { "P" };
    let mut args = vec![go_string(long)];
    if !short.is_empty() {
        args.push(go_string(&short));
    }
    if !default.is_empty() {
        args.push(default);
    }
    args.push(go_string(help));

    let _ = writeln!(
        out,
        "\t{var}.{set}().{method}{with_short}({})",
        args.join(", ")
    );

    if flag.hide {
        let _ = writeln!(out, "\t_ = {var}.{set}().MarkHidden({})", go_string(long));
    }
    if flag.negate.is_some() {
        skipped.note("negations (`--no-x` is a flag of its own in cobra)");
    }
    if flag.long.len() > 1 {
        skipped.note("second and later long forms");
    }
    if flag.short.len() > 1 {
        skipped.note("second and later short forms");
    }
    if flag.required {
        // Expressible, and left out on purpose: `MarkFlagRequired` is enforced by `Execute`,
        // which this program does not call, so declaring it would cost nothing and be
        // measured as though it did.
        skipped.note("required (enforced by Execute, which is not what is measured)");
    }
}

/// Positionals, which cobra does not declare.
fn note_arg(arg: &SpecArg, skipped: &mut Skipped) {
    let _ = arg;
    skipped.note("positional arguments (cobra validates a count, not a name)");
}

fn go_string(s: &str) -> String {
    let mut out = String::with_capacity(s.len() + 2);
    out.push('"');
    for c in s.chars() {
        match c {
            '"' => out.push_str("\\\""),
            '\\' => out.push_str("\\\\"),
            '\n' => out.push_str("\\n"),
            '\r' => out.push_str("\\r"),
            '\t' => out.push_str("\\t"),
            c if (c as u32) < 0x20 => {
                let _ = write!(out, "\\x{:02x}", c as u32);
            }
            c => out.push(c),
        }
    }
    out.push('"');
    out
}

fn string_slice(items: &[String]) -> String {
    let quoted: Vec<String> = items.iter().map(|s| go_string(s)).collect();
    format!("[]string{{{}}}", quoted.join(", "))
}
