/*
Kam CLI entrypoint with comprehensive help localization.

This file builds the clap Command from the derive-generated `Cli` struct,
injects localized about/help strings (top-level, subcommands and arguments)
based on keys present in `src/i18n/{en,zh}.toml`, then parses arguments
using the localized Command so `--help` and subcommand help display translations.

Notes:
- Translated keys used:
  - cli.about / cli.long_about
  - cli.flags.<arg_id>
  - cli.commands.<cmd>[.<subcmd>...].summary
  - cli.commands.<cmd>[.<subcmd>...].description
  - cli.commands.<cmd>[.<subcmd>...].flags.<arg_id>

- The i18n layer (`kam::i18n::tr_key`) returns the original key if there is no
  translation present; we use that to detect missing translations and fall back to
  the default static help strings generated by clap.
*/

use clap::error::ErrorKind;
use clap::{CommandFactory, FromArgMatches};
use colored::Colorize;
use dotenvy::dotenv;
use kam::errors::KamError;
use std::error::Error;

use kam::cli::{Cli, Commands};

fn print_error_chain(e: &KamError) {
    use kam::utils::Utils;
    Utils::error(&format!("{}", e));
    let mut source = e.source();
    while let Some(s) = source {
        eprintln!("  {} {}", "→".dimmed(), s.to_string().dimmed());
        source = s.source();
    }
}

fn dedupe_help(raw: &str) -> String {
    // Split into lines for simple, robust processing.
    let lines: Vec<&str> = raw.lines().collect();

    // Helper: for a contiguous section (lines slice) split into blocks
    // separated by blank lines, then keep the last occurrence of each
    // block keyed by its first (header) line.
    fn dedupe_section(section_lines: &[&str]) -> Vec<String> {
        use std::collections::HashMap;
        let mut blocks: Vec<Vec<String>> = Vec::new();
        let mut i = 0;
        while i < section_lines.len() {
            // skip leading blank lines
            while i < section_lines.len() && section_lines[i].trim().is_empty() {
                i += 1;
            }
            if i >= section_lines.len() {
                break;
            }
            // collect block until next blank line
            let mut block: Vec<String> = Vec::new();
            while i < section_lines.len() && !section_lines[i].trim().is_empty() {
                block.push(section_lines[i].to_string());
                i += 1;
            }
            blocks.push(block);
        }

        // Keep the last occurrence of each block header (header = first line).
        let mut map: HashMap<String, Vec<String>> = HashMap::new();
        let mut order: Vec<String> = Vec::new();
        for block in blocks.into_iter().rev() {
            if block.is_empty() {
                continue;
            }
            let header = block[0].trim_start().to_string();
            map.entry(header.clone()).or_insert_with(|| {
                order.push(header.clone());
                block
            });
        }
        order.reverse();

        // Reconstruct lines for this section.
        let mut out: Vec<String> = Vec::new();
        for key in order {
            if let Some(block) = map.get(&key) {
                out.extend(block.clone());
                out.push(String::new()); // blank line between blocks
            }
        }
        if out.last().map(|s| s.is_empty()) == Some(true) {
            out.pop();
        }
        out
    }

    // Walk lines and process `Arguments:` and `Options:` sections.
    let mut out_lines: Vec<String> = Vec::new();
    let mut i = 0;
    while i < lines.len() {
        let line = lines[i];
        let trimmed = line.trim_end();
        if trimmed == "Arguments:" || trimmed == "Options:" {
            // emit section header
            out_lines.push(line.to_string());
            i += 1;

            // collect section lines until next top-level header (line ending with ':')
            // or EOF.
            let mut sec_lines: Vec<&str> = Vec::new();
            while i < lines.len() {
                let nxt = lines[i];
                // stop if next is a new top-level header like 'Commands:' or 'Options:'
                if nxt.trim_end().ends_with(':') {
                    break;
                }
                sec_lines.push(nxt);
                i += 1;
            }

            // Dedupe this section and append the cleaned lines.
            let cleaned_block = dedupe_section(&sec_lines);
            for l in cleaned_block {
                out_lines.push(l);
            }

            // preserve a single blank line boundary if present in original
            if i < lines.len() && lines[i].trim().is_empty() {
                out_lines.push(String::new());
                // consume the original blank line
                i += 1;
            }
        } else {
            out_lines.push(line.to_string());
            i += 1;
        }
    }

    // Normalize the Usage line: collapse consecutive duplicated tokens while preserving order.
    // Example: `Usage: kam install [OPTIONS] [PATH] [PATH]` -> `Usage: kam install [OPTIONS] [PATH]`
    for ln in out_lines.iter_mut() {
        if ln.trim_start().starts_with("Usage:") {
            let tokens: Vec<&str> = ln.split_whitespace().collect();
            if tokens.len() > 1 {
                let mut new_tokens: Vec<&str> = Vec::new();
                // Keep the 'Usage:' prefix and collapse ONLY consecutive duplicates.
                new_tokens.push(tokens[0]);
                for tok in tokens.into_iter().skip(1) {
                    if new_tokens.last().copied() != Some(tok) {
                        new_tokens.push(tok);
                    }
                }
                *ln = new_tokens.join(" ");
            }
        }
    }

    // Make sure final string ends with a newline (like clap's help does).
    let mut res = out_lines.join("\n");
    if !res.ends_with('\n') {
        res.push('\n');
    }
    res
}

#[cfg(test)]
mod dedupe_tests {
    use super::dedupe_help;

    #[test]
    fn dedupe_usage_collapses_duplicate_tokens() {
        let raw = "Usage: kam install [OPTIONS] [PATH] [PATH]\n";
        let cleaned = dedupe_help(raw);
        assert!(cleaned.contains("Usage: kam install [OPTIONS] [PATH]\n"));
    }

    #[test]
    fn dedupe_arguments_and_options_sections_keep_last_block() {
        let raw = "Arguments:\n    description 1\n\nArguments:\n    description 2\n\nOptions:\n    -a    alpha\n\nOptions:\n    -a    alpha\n    -b    beta\n";
        let cleaned = dedupe_help(raw);
        assert!(cleaned.contains("description 2"));
        assert!(cleaned.contains("-b"));
    }
}

fn main() {
    // Load environment variables (e.g., KAM_UI_LANGUAGE overrides)
    dotenv().ok();

    // Initialize i18n early so we can use translations when building the CLI help.
    kam::i18n::init();

    // Build the clap `Command` from the derive-generated parser so we can customize it.
    let mut cmd = Cli::command();

    // Localize top-level about / long_about (if translations exist).
    let about_text = kam::i18n::tr_key("cli.about");
    if about_text != "cli.about" {
        // Use an owned String to avoid lifetime issues with clap's API.
        cmd = cmd.about(about_text.to_string());
    }
    let long_about_text = kam::i18n::tr_key("cli.long_about");
    if long_about_text != "cli.long_about" {
        cmd = cmd.long_about(long_about_text.to_string());
    }

    // Localize top-level/global arguments (if translations exist).
    // Collect replacements first, then apply, to avoid borrowing issues.
    //
    // Avoid adding duplicate args: if an argument with the same id already exists
    // on the command we skip re-adding it to prevent clap from panicking about
    // duplicate long/short option names.
    {
        let mut replacements = Vec::new();
        for arg in cmd.get_arguments() {
            let arg_id = arg.get_id().as_str();
            let key = format!("cli.flags.{}", arg_id);
            let tr = kam::i18n::tr_key(&key);
            if tr != key {
                replacements.push(arg.clone().help(tr.to_string()));
            }
        }

        if !replacements.is_empty() {
            for repl in replacements {
                let repl_id = repl.get_id().as_str();
                // If an arg with the same id already exists on this command, skip adding
                // to avoid duplicate argument names (which causes a clap panic).
                if cmd.get_arguments().any(|a| a.get_id().as_str() == repl_id) {
                    if std::env::var("KAM_DEBUG_I18N")
                        .map(|v| v == "1")
                        .unwrap_or(false)
                    {
                        eprintln!(
                            "Skipping localization for arg '{}' (already present)",
                            repl_id
                        );
                    }
                    continue;
                }
                cmd = cmd.clone().arg(repl);
            }
        }
    }

    // Recursive localization for subcommands and nested subcommands.
    fn localize_command(cmd: &mut clap::Command, prefix: &str) {
        for sub in cmd.get_subcommands_mut() {
            let name = sub.get_name();
            let sub_prefix = format!("{}.{}", prefix, name);

            // Localize summary/about
            let summary_key = format!("{}.summary", sub_prefix);
            let summary = kam::i18n::tr_key(&summary_key);
            if summary != summary_key {
                *sub = sub.clone().about(summary.to_string());
            }

            // Localize long description (if present)
            let desc_key = format!("{}.description", sub_prefix);
            let desc = kam::i18n::tr_key(&desc_key);
            if desc != desc_key {
                *sub = sub.clone().long_about(desc.to_string());
            }

            // Collect argument replacements (clone Arg objects and localized help)
            let mut arg_repls: Vec<clap::Arg> = Vec::new();
            for arg in sub.get_arguments() {
                let arg_id = arg.get_id().as_str();
                let arg_key = format!("{}.flags.{}", sub_prefix, arg_id);
                let arg_tr = kam::i18n::tr_key(&arg_key);
                if arg_tr != arg_key {
                    arg_repls.push(arg.clone().help(arg_tr.to_string()));
                }
            }

            // Apply argument replacements (adding an Arg with the same id will
            // overwrite the previous definition in clap's builder).
            for repl in arg_repls {
                let repl_id = repl.get_id().as_str();
                // If an arg with the same id already exists on this subcommand, skip adding
                // to avoid duplicate argument names (which can cause a clap panic).
                if sub.get_arguments().any(|a| a.get_id().as_str() == repl_id) {
                    if std::env::var("KAM_DEBUG_I18N")
                        .map(|v| v == "1")
                        .unwrap_or(false)
                    {
                        eprintln!(
                            "Skipping localization for arg '{}' (already present)",
                            repl_id
                        );
                    }
                    continue;
                }
                *sub = sub.clone().arg(repl);
            }

            // Recurse into nested subcommands.
            localize_command(sub, &sub_prefix);
        }
    }

    // Kick off recursive localization for subcommands under "cli.commands".
    localize_command(&mut cmd, "cli.commands");

    // The `help` subcommand is provided as a real subcommand (see `Commands::Help`)
    // and is localized above via `localize_command`. Disable clap's built-in `help`
    // subcommand so it doesn't collide with the derived `Help` subcommand.
    cmd = cmd.clone().disable_help_subcommand(true);

    // Parse using the localized Command so help output is translated.
    // Use a cloned `cmd` so we can still reference the original for printing help later.
    //
    // Heuristic: when `-S`/`-s` is used and the next token looks like a target
    // (not an option and not a real subcommand) automatically insert `--` so
    // `kam -S <target>` works without the user having to type `--`.
    // Intercept help tokens early so we can post-process help output (dedupe
    // duplicate argument/option blocks introduced by adding translations).
    //
    // `dedupe_help` was moved to module scope so it can be used for both explicit
    // help printing and parse-time error formatting.
    //
    // Debugging aid: when KAM_DEBUG_I18N=1, dump argument metadata to stderr
    // so we can detect conflicting or duplicate long/short option names.
    if std::env::var("KAM_DEBUG_I18N")
        .map(|v| v == "1")
        .unwrap_or(false)
    {
        eprintln!("KAM_DEBUG_I18N=1: dumping top-level arguments and subcommands...");

        // Top-level arguments
        for arg in cmd.get_arguments() {
            let id = arg.get_id().as_str();
            let long = arg.get_long().unwrap_or("");
            let short = arg.get_short().map(|c| c.to_string()).unwrap_or_default();
            eprintln!("TOP ARG: id='{}' long='{}' short='{}'", id, long, short);
        }

        // Recursively dump subcommands and their arguments
        fn dump_subs(c: &clap::Command, prefix: &str) {
            for sub in c.get_subcommands() {
                let name = sub.get_name();
                eprintln!("SUBCMD: {}{}", prefix, name);
                for a in sub.get_arguments() {
                    let id = a.get_id().as_str();
                    let long = a.get_long().unwrap_or("");
                    let short = a.get_short().map(|c| c.to_string()).unwrap_or_default();
                    eprintln!("  ARG: id='{}' long='{}' short='{}'", id, long, short);
                }
                dump_subs(sub, &format!("{}{} ", prefix, name));
            }
        }

        dump_subs(&cmd, "");
    }

    let matches = {
        // Collect argv and allow the CLI helper to insert `--` when appropriate so
        // combined short flags like `-Syu` are handled consistently with runtime.
        let args_os: Vec<std::ffi::OsString> = std::env::args_os().collect();
        let args = kam::cli::inject_double_dash_for_targets(args_os, &mut cmd);

        // Intercept explicit help tokens (-h / --help) so we can post-process them.
        let toks: Vec<String> = args
            .iter()
            .map(|s| s.to_string_lossy().into_owned())
            .collect();
        if let Some(help_pos) = toks.iter().position(|t| t == "--help" || t == "-h") {
            // Build list of non-option tokens (subcommand path) up to the help token.
            let sub_names: Vec<String> = toks[1..help_pos]
                .iter()
                .filter_map(|s| {
                    if !s.starts_with('-') && s != "--" {
                        Some(s.clone())
                    } else {
                        None
                    }
                })
                .collect();

            // Traverse the command tree using cloned Command values (avoid mutable borrows).
            let mut cur = cmd.clone();
            let mut found = true;
            for name in sub_names {
                // Collect owned clones so we don't hold borrows into `cur` while assigning to it.
                let sub_clones: Vec<clap::Command> = cur.get_subcommands().cloned().collect();
                match sub_clones
                    .into_iter()
                    .find(|s| s.get_name() == name.as_str())
                {
                    Some(s) => cur = s,
                    None => {
                        // token wasn't a subcommand, stop descent
                        found = false;
                        break;
                    }
                }
            }

            if !found {
                // Could not locate a nested subcommand; fall back to top-level help.
                let mut tmp = cmd.clone();
                let mut buf: Vec<u8> = Vec::new();
                if tmp.write_long_help(&mut buf).is_ok() {
                    let raw = String::from_utf8_lossy(&buf);
                    let cleaned = dedupe_help(&raw);
                    // print deduped help and exit
                    print!("{}", cleaned);
                    if !cleaned.ends_with('\n') {
                        println!();
                    }
                } else {
                    if let Err(e) = tmp.print_long_help() {
                        kam::utils::Utils::error(&format!("Failed to write help: {}", e));
                        std::process::exit(1);
                    }
                    println!();
                }
                std::process::exit(0);
            }

            // Capture long help into a buffer so we can dedupe sections.
            let mut buf: Vec<u8> = Vec::new();
            if cur.write_long_help(&mut buf).is_ok() {
                let raw = String::from_utf8_lossy(&buf);
                let cleaned = dedupe_help(&raw);
                // print deduped help and exit
                print!("{}", cleaned);
                if !cleaned.ends_with('\n') {
                    println!();
                }
            } else {
                // Fallback: print directly
                let _ = cur.print_long_help();
                println!();
            }

            std::process::exit(0);
        }

        // Parse from the (possibly modified) argv list so clap receives the `--`
        // we've injected when appropriate.
        match cmd.clone().try_get_matches_from(args) {
            Ok(m) => m,
            Err(e) => {
                // Run the error's textual output through the same dedupe helper we use
                // for localized help output so duplicate "Arguments/Options" blocks are collapsed
                // and the Usage line is normalized.
                let raw = e.to_string();
                let cleaned = dedupe_help(&raw);

                // Clap prints help/version to stdout and errors to stderr by default.
                match e.kind() {
                    ErrorKind::DisplayHelp | ErrorKind::DisplayVersion => {
                        print!("{}", cleaned);
                        if !cleaned.ends_with('\n') {
                            println!();
                        }
                        std::process::exit(0);
                    }
                    _ => {
                        eprintln!("{}", cleaned);
                        // For parsing errors, use exit code 2 to match clap's default.
                        std::process::exit(2);
                    }
                }
            }
        }
    };

    // Convert ArgMatches to typed `Cli` (handling parse errors).
    let cli = match Cli::from_arg_matches(&matches) {
        Ok(c) => c,
        Err(e) => {
            eprintln!("Error parsing arguments: {}", e);
            std::process::exit(2);
        }
    };

    // If the caller requested an explicit index update and they did not also provide
    // pacman-style flags, perform the equivalent of `kam repo sync --force` and exit.
    if cli.update_index && !cli.sync_flag && !cli.search_flag {
        let base = kam::cmds::repo::effective_base_url(cli.modules_url.as_deref());
        match kam::cmds::repo::repo_sync_with_jobs(&base, true, None, cli.quiet) {
            Ok(()) => return,
            Err(e) => {
                print_error_chain(&e);
                std::process::exit(1);
            }
        }
    }

    // Handle pacman-style top-level flags (-S / -s) before dispatching to subcommands.
    // - `-Ss <query>` performs a remote search
    // - `-S <moduleId>` downloads the latest release ZIP for the given module(s)
    if cli.sync_flag || cli.search_flag {
        // SPECIAL CASE: When sync/search flags are present but no targets were parsed,
        // it might be because clap interpreted a search term as a subcommand.
        // In this case, we check if the command is one that might be used as a search term.
        let effective_targets = if cli.targets.is_empty() {
            // If no targets were collected but a command was parsed, the command might have been
            // intended as a search term (e.g. "kam -Ss cache" where "cache" is a subcommand)
            // This is a limitation of clap where subcommands take precedence over positional args
            match &cli.command {
                Some(_) => {
                    // For now, we'll just use an empty vector and let the repo module handle the error
                    // The correct solution would require changing the CLI architecture to avoid this conflict
                    cli.targets.clone()
                }
                None => cli.targets.clone(),
            }
        } else {
            cli.targets.clone()
        };

        // If `--update`/`-u` is present, perform a forced index sync before continuing.
        if cli.update_index {
            let base = kam::cmds::repo::effective_base_url(cli.modules_url.as_deref());
            match kam::cmds::repo::repo_sync_with_jobs(&base, true, None, cli.quiet) {
                Ok(()) => {}
                Err(e) => {
                    print_error_chain(&e);
                    std::process::exit(1);
                }
            }
        }

        match kam::cmds::repo::handle_pacman_style(
            cli.sync_flag,
            cli.search_flag,
            effective_targets,
            cli.assume_yes,
            cli.modules_url.clone(),
            cli.quiet,
        ) {
            Ok(()) => return,
            Err(e) => {
                print_error_chain(&e);
                std::process::exit(1);
            }
        }
    }

    // Dispatch to subcommand handlers (command is optional).
    // If no subcommand is provided (None) we treat it as a no-op returning Ok(()).
    let res: Result<(), KamError> = match cli.command {
        Some(Commands::Init(args)) => kam::cmds::init::run(args),
        Some(Commands::Build(args)) => kam::cmds::build::run(args),
        Some(Commands::Version(args)) => kam::cmds::version::run(args),
        Some(Commands::Cache(args)) => kam::cmds::cache::run(args),
        Some(Commands::Tmpl(args)) => kam::cmds::tmpl::run(args),
        Some(Commands::Validate(args)) => kam::cmds::validate::run(args),
        Some(Commands::Completions(args)) => kam::cmds::completion::run(args),
        Some(Commands::Secret(args)) => kam::cmds::secret::run(args),
        Some(Commands::Sign(args)) => kam::cmds::sign::run(args),
        Some(Commands::Verify(args)) => kam::cmds::verify::run(args),
        Some(Commands::Check(args)) => kam::cmds::check::run(args),
        Some(Commands::Export(args)) => kam::cmds::export::run(args),
        Some(Commands::Config(args)) => kam::cmds::config::run(args),
        Some(Commands::Toml(args)) => kam::cmds::toml::run(args),
        Some(Commands::Install(args)) => kam::cmds::install::run(args),
        Some(Commands::Repo(args)) => {
            kam::cmds::repo::run_with_modules_url(args, cli.modules_url.clone())
        }
        Some(Commands::Help(args)) => {
            // Print top-level or subcommand help using a cloned, mutable Command so
            // we can traverse nested subcommand definitions and call `print_long_help()`.
            if args.subcommand.is_empty() {
                let mut tmp = cmd.clone();
                let mut buf: Vec<u8> = Vec::new();
                if tmp.write_long_help(&mut buf).is_ok() {
                    let raw = String::from_utf8_lossy(&buf);
                    let cleaned = dedupe_help(&raw);
                    print!("{}", cleaned);
                    if !cleaned.ends_with('\n') {
                        println!();
                    }
                } else {
                    if let Err(e) = tmp.print_long_help() {
                        kam::utils::Utils::error(&format!("Failed to write help: {}", e));
                        std::process::exit(1);
                    }
                    println!();
                }
                Ok(())
            } else {
                // Traverse subcommands using owned clones (avoids mutable borrow conflicts).
                // Traverse subcommands using owned clones (avoids mutable borrow conflicts).
                let mut cur = cmd.clone();
                for name in &args.subcommand {
                    // Collect owned clones before searching so we don't hold borrows
                    // into `cur` while assigning to it.
                    let sub_clones: Vec<clap::Command> = cur.get_subcommands().cloned().collect();
                    match sub_clones
                        .into_iter()
                        .find(|s| s.get_name() == name.as_str())
                    {
                        Some(s) => cur = s,
                        None => {
                            kam::utils::Utils::error(&format!("Unknown subcommand: {}", name));
                            std::process::exit(2);
                        }
                    }
                }
                let mut buf: Vec<u8> = Vec::new();
                if cur.write_long_help(&mut buf).is_ok() {
                    let raw = String::from_utf8_lossy(&buf);
                    let cleaned = dedupe_help(&raw);
                    print!("{}", cleaned);
                    if !cleaned.ends_with('\n') {
                        println!();
                    }
                } else {
                    if let Err(e) = cur.print_long_help() {
                        kam::utils::Utils::error(&format!("Failed to write help: {}", e));
                        std::process::exit(1);
                    }
                    println!();
                }
                Ok(())
            }
        }
        Some(Commands::Env(args)) => kam::cmds::env::run(args),
        Some(Commands::About(args)) => kam::cmds::about::run(args),
        None => {
            // No subcommand provided: show top-level help (deduped) instead of performing background operations.
            let mut tmp = cmd.clone();
            let mut buf: Vec<u8> = Vec::new();
            if tmp.write_long_help(&mut buf).is_ok() {
                let raw = String::from_utf8_lossy(&buf);
                let cleaned = dedupe_help(&raw);
                print!("{}", cleaned);
                if !cleaned.ends_with('\n') {
                    println!();
                }
            } else {
                if let Err(e) = tmp.print_long_help() {
                    kam::utils::Utils::error(&format!("Failed to write help: {}", e));
                    std::process::exit(1);
                }
                println!();
            }
            Ok(())
        }
    };

    if let Err(e) = res {
        print_error_chain(&e);
        std::process::exit(1);
    }
}
