Skip to content
use crate::nanoargs::parser::ArgParser;
use std::fmt::Write as _;

/// Intermediate representation of a single help line item.
/// Built once by the shared assembly logic, then formatted as plain text.
struct HelpEntry {
    /// The left column label (e.g. "-v, --verbose" or "--output <FILE>").
    label: String,
    /// The right column description (e.g. "Enable verbose output (required) [default: x]").
    description: String,
}

// ── Leaf text helpers (color styling not supported in this vendored build) ──

fn green(s: &str) -> String {
    s.to_string()
}

fn cyan(s: &str) -> String {
    s.to_string()
}

fn dim(s: &str) -> String {
    s.to_string()
}

fn bold_yellow(s: &str) -> String {
    s.to_string()
}

fn bold(s: &str) -> String {
    s.to_string()
}

// ── Display width helper ───────────────────────────────────────────────────

/// Returns the visible width of a string.
fn display_width(s: &str) -> usize {
    s.len()
}

/// Formats a help section with column-aligned descriptions.
/// Uses `bold_yellow()` for the header and `display_width()` for column alignment.
/// Returns empty string if entries is empty.
fn format_section(header: &str, entries: &[HelpEntry]) -> String {
    if entries.is_empty() {
        return String::new();
    }
    let max_width = entries
        .iter()
        .map(|e| display_width(&e.label))
        .max()
        .unwrap_or(0);
    let mut out = String::new();
    write!(out, "\n{}\n", bold_yellow(header)).unwrap();
    for entry in entries {
        let pad = max_width.saturating_sub(display_width(&entry.label));
        writeln!(
            out,
            "  {}{:pad$}  {}",
            entry.label,
            "",
            entry.description,
            pad = pad
        )
        .unwrap();
    }
    out
}

impl ArgParser {
    /// Generate formatted help text for this parser.
    ///
    /// Includes the description, usage line, options, positional arguments,
    /// and subcommands sections. When the `color` feature is enabled and the
    /// terminal supports it, the output includes ANSI color codes.
    #[allow(clippy::too_many_lines)]
    pub fn help_text(&self) -> String {
        let mut out = String::new();
        let name = self.program_name.as_deref().unwrap_or("program");

        // Description
        if let Some(ref desc) = self.program_desc {
            out.push_str(desc);
            out.push_str("\n\n");
        }

        // Usage summary line
        let has_subcommands = !self.subcommands.is_empty();
        let has_visible =
            self.flags.iter().any(|f| !f.hidden) || self.options.iter().any(|o| !o.hidden);

        write!(out, "{} {}", bold_yellow("Usage:"), bold(name)).unwrap();
        if has_visible {
            out.push_str(" [OPTIONS]");
        }
        if has_subcommands {
            write!(out, " {}", cyan("<SUBCOMMAND>")).unwrap();
        } else {
            for pos in &self.positionals {
                let suffix = if pos.multi { "..." } else { "" };
                if pos.required {
                    write!(out, " {}{}", cyan(&format!("<{}>", pos.name)), suffix).unwrap();
                } else {
                    write!(out, " {}{}", cyan(&format!("[{}]", pos.name)), suffix).unwrap();
                }
            }
        }
        out.push('\n');

        // Options (flags + options combined under one header)
        {
            let mut entries: Vec<HelpEntry> = Vec::new();
            for flag in self.flags.iter().filter(|f| !f.hidden) {
                let label = match flag.short {
                    Some(c) => format!(
                        "{}, {}",
                        green(&format!("-{c}")),
                        green(&format!("--{}", flag.long))
                    ),
                    None => format!("    {}", green(&format!("--{}", flag.long))),
                };
                entries.push(HelpEntry {
                    label,
                    description: flag.description.clone(),
                });
            }
            for opt in self.options.iter().filter(|o| !o.hidden) {
                let placeholder_str = cyan(&format!("<{}>", opt.placeholder));
                let label = match opt.short {
                    Some(c) => format!(
                        "{}, {} {}",
                        green(&format!("-{c}")),
                        green(&format!("--{}", opt.long)),
                        placeholder_str
                    ),
                    None => format!(
                        "    {} {}",
                        green(&format!("--{}", opt.long)),
                        placeholder_str
                    ),
                };
                let req = if opt.required {
                    format!(" {}", dim("(required)"))
                } else {
                    String::new()
                };
                let multi_hint = if opt.multi {
                    format!(" {}", dim("(multiple)"))
                } else {
                    String::new()
                };
                let default_hint = match &opt.default {
                    Some(val) => format!(" {}", dim(&format!("[default: {val}]"))),
                    None => String::new(),
                };
                let env_hint = match &opt.env_var {
                    Some(var) => format!(" {}", dim(&format!("[env: {var}]"))),
                    None => String::new(),
                };
                let validator_hint = opt
                    .validator
                    .as_ref()
                    .and_then(|v| v.hint())
                    .map(|h| format!(" {}", dim(&format!("[{h}]"))))
                    .unwrap_or_default();
                entries.push(HelpEntry {
                    label,
                    description: format!(
                        "{}{req}{multi_hint}{default_hint}{env_hint}{validator_hint}",
                        opt.description
                    ),
                });
            }
            out.push_str(&format_section("Options:", &entries));
        }

        // Argument Groups
        if !self.groups.is_empty() {
            let entries: Vec<HelpEntry> = self
                .groups
                .iter()
                .map(|g| {
                    let members_str = g
                        .members
                        .iter()
                        .map(|m| green(&format!("--{m}")))
                        .collect::<Vec<_>>()
                        .join(", ");
                    HelpEntry {
                        label: g.name.clone(),
                        description: format!("{} {}", members_str, dim("(at least one required)")),
                    }
                })
                .collect();
            out.push_str(&format_section("Argument Groups:", &entries));
        }

        // Conflicts
        if !self.conflicts.is_empty() {
            let entries: Vec<HelpEntry> = self
                .conflicts
                .iter()
                .map(|c| {
                    let members_str = c
                        .members
                        .iter()
                        .map(|m| green(&format!("--{m}")))
                        .collect::<Vec<_>>()
                        .join(", ");
                    HelpEntry {
                        label: c.name.clone(),
                        description: format!("{} {}", members_str, dim("(mutually exclusive)")),
                    }
                })
                .collect();
            out.push_str(&format_section("Conflicts:", &entries));
        }

        // Positionals (omitted when subcommands are present)
        if !has_subcommands {
            let entries: Vec<HelpEntry> = self
                .positionals
                .iter()
                .map(|pos| {
                    let multi_suffix = if pos.multi { "..." } else { "" };
                    let label = format!("{}{}", green(&pos.name), multi_suffix);
                    let req = if pos.required {
                        format!(" {}", dim("(required)"))
                    } else {
                        String::new()
                    };
                    let default_hint = match &pos.default {
                        Some(val) => format!(" {}", dim(&format!("[default: {val}]"))),
                        None => String::new(),
                    };
                    let validator_hint = pos
                        .validator
                        .as_ref()
                        .and_then(|v| v.hint())
                        .map(|h| format!(" {}", dim(&format!("[{h}]"))))
                        .unwrap_or_default();
                    HelpEntry {
                        label,
                        description: format!(
                            "{}{req}{default_hint}{validator_hint}",
                            pos.description
                        ),
                    }
                })
                .collect();
            out.push_str(&format_section("Positional arguments:", &entries));
        }

        // Subcommands
        if has_subcommands {
            let entries: Vec<HelpEntry> = self
                .subcommands
                .iter()
                .map(|subcmd| HelpEntry {
                    label: green(&subcmd.name),
                    description: subcmd.description.clone(),
                })
                .collect();
            out.push_str(&format_section("Subcommands:", &entries));
        }

        out
    }
}