use crate::handlers;
use crate::parse::WordSet;

pub struct CommandDoc {
    pub name: String,
    pub kind: DocKind,
    pub url: &'static str,
    pub description: String,
    pub aliases: Vec<String>,
    pub category: String,
    pub examples: Vec<String>,
}

pub enum DocKind {
    Handler,
}

impl CommandDoc {
    pub fn handler(name: &'static str, url: &'static str, description: impl Into<String>, category: &str) -> Self {
        let raw = description.into();
        let description = raw
            .lines()
            .map(|line| {
                if line.is_empty() || line.starts_with("- ") {
                    line.to_string()
                } else {
                    format!("- {line}")
                }
            })
            .collect::<Vec<_>>()
            .join("\n");
        Self { name: name.to_string(), kind: DocKind::Handler, url, description, aliases: Vec::new(), category: category.to_string(), examples: Vec::new() }
    }

    pub fn wordset(name: &'static str, url: &'static str, words: &WordSet, category: &str) -> Self {
        Self::handler(name, url, doc(words).build(), category)
    }

    pub fn wordset_multi(name: &'static str, url: &'static str, words: &WordSet, multi: &[(&str, WordSet)], category: &str) -> Self {
        Self::handler(name, url, doc_multi(words, multi).build(), category)
    }


}

#[derive(Default)]
pub struct DocBuilder {
    subcommands: Vec<String>,
    flags: Vec<String>,
    sections: Vec<String>,
}

impl DocBuilder {
    pub fn new() -> Self {
        Self::default()
    }

    pub fn wordset(mut self, words: &WordSet) -> Self {
        for item in words.iter() {
            if item.starts_with('-') {
                self.flags.push(item.to_string());
            } else {
                self.subcommands.push(item.to_string());
            }
        }
        self
    }

    pub fn multi_word(mut self, multi: &[(&str, WordSet)]) -> Self {
        for (prefix, actions) in multi {
            for action in actions.iter() {
                self.subcommands.push(format!("{prefix} {action}"));
            }
        }
        self
    }

    pub fn triple_word(mut self, triples: &[(&str, &str, WordSet)]) -> Self {
        for (a, b, actions) in triples {
            for action in actions.iter() {
                self.subcommands.push(format!("{a} {b} {action}"));
            }
        }
        self
    }

    pub fn subcommand(mut self, name: impl Into<String>) -> Self {
        self.subcommands.push(name.into());
        self
    }

    pub fn section(mut self, text: impl Into<String>) -> Self {
        let s = text.into();
        if !s.is_empty() {
            self.sections.push(s);
        }
        self
    }

    pub fn build(self) -> String {
        let mut lines = Vec::new();
        if !self.subcommands.is_empty() {
            let mut subs = self.subcommands;
            subs.sort();
            lines.push(format!("- Subcommands: {}", subs.join(", ")));
        }
        if !self.flags.is_empty() {
            lines.push(format!("- Flags: {}", self.flags.join(", ")));
        }
        for s in self.sections {
            if s.starts_with("- ") {
                lines.push(s);
            } else {
                lines.push(format!("- {s}"));
            }
        }
        lines.join("\n")
    }
}

pub fn doc(words: &WordSet) -> DocBuilder {
    DocBuilder::new().wordset(words)
}

pub fn doc_multi(words: &WordSet, multi: &[(&str, WordSet)]) -> DocBuilder {
    DocBuilder::new().wordset(words).multi_word(multi)
}

pub fn wordset_items(words: &WordSet) -> String {
    let items: Vec<&str> = words.iter().collect();
    items.join(", ")
}


pub fn all_command_docs() -> Vec<CommandDoc> {
    let mut docs = handlers::handler_docs();
    docs.sort_by_key(|a| a.name.to_ascii_lowercase());
    docs
}

const GLOSSARY: &str = "\
| Term | Meaning |\n\
|------|---------|\n\
| **Allowed standalone flags** | Flags that take no value (`--verbose`, `-v`). Listed on flat commands. |\n\
| **Flags** | Same as standalone flags, but in the shorter format used within subcommand entries. |\n\
| **Allowed valued flags** | Flags that require a value (`--output file`, `-j 4`). |\n\
| **Valued** | Same as valued flags, in shorter format within subcommand entries. |\n\
| **Bare invocation allowed** | The command can be run with no arguments at all. |\n\
| **Subcommands** | Named subcommands that are allowed (e.g. `git log`, `cargo test`). |\n\
| **Positional arguments only** | No specific flags are listed; only positional arguments are accepted. |\n\
| **(requires --flag)** | A guarded subcommand that is only allowed when a specific flag is present (e.g. `cargo fmt` requires `--check`). |\n\
\n\
Unlisted flags, subcommands, and commands are not allowed.\n";

pub fn render_markdown(docs: &[CommandDoc]) -> String {
    let mut out = format!(
        "# Supported Commands\n\n\
         Auto-generated by `safe-chains --list-commands`. These commands, subcommands, and flags are safe to run individually or in combination.\n\n\
         ## Glossary\n\n{GLOSSARY}\n",
    );

    for doc in docs {
        out.push_str(&render_command_entry(doc));
    }

    out
}

fn category_display_name(slug: &str) -> &'static str {
    match slug {
        "ai" => "AI Tools",
        "android" => "Android",
        "ansible" => "Ansible",
        "api" => "API / Load Testing",
        "archive" => "Compression / Archive",
        "binary" => "Binary Analysis",
        "blockchain" => "Blockchain / Crypto",
        "build" => "Build Systems",
        "builtins" => "Shell Builtins",
        "clipboard" => "Clipboard",
        "compile" => "Compilation Toolchains",
        "configmgmt" => "Config Management",
        "cppkg" => "C++ Package Managers",
        "crypto" => "Cryptography",
        "c" => "C / C++",
        "cloud" => "Cloud Providers",
        "containers" => "Containers",
        "crystal" => "Crystal",
        "d" => "D",
        "dart" => "Dart / Flutter",
        "data" => "Data Processing",
        "db" => "Database Clients",
        "editors" => "Editors",
        "embedded" => "Embedded",
        "dotnet" => ".NET",
        "elixir" => "Elixir / Erlang",
        "erlang" => "Erlang",
        "game" => "Game Engines",
        "gleam" => "Gleam",
        "media" => "Media",
        "migrations" => "Database Migrations",
        "ml" => "ML / Observability",
        "mobile" => "Mobile Frameworks",
        "niche" => "Niche / Esoteric",
        "nix" => "Nix",
        "forges" => "Code Forges",
        "fs" => "Filesystem",
        "fuzzy" => "Fuzzy Finders",
        "go" => "Go",
        "hash" => "Hashing",
        "haskell" => "Haskell",
        "julia" => "Julia",
        "jvm" => "JVM",
        "kafka" => "Kafka",
        "lisp" => "Common Lisp",
        "lua" => "Lua",
        "magick" => "ImageMagick",
        "net" => "Networking",
        "nim" => "Nim",
        "node" => "Node.js",
        "ocaml" => "OCaml",
        "pdf" => "PDF / Document",
        "perl" => "Perl",
        "php" => "PHP",
        "proof" => "Theorem Provers",
        "scaffold" => "Project Scaffolders",
        "serverless" => "Serverless / IaC",
        "pm" => "Package Managers",
        "python" => "Python",
        "r" => "R",
        "racket" => "Racket",
        "roc" => "Roc",
        "ruby" => "Ruby",
        "rust" => "Rust",
        "search" => "Search",
        "swift" => "Swift",
        "sysinfo" => "System Info",
        "system" => "System",
        "tex" => "TeX / LaTeX",
        "text" => "Text Processing",
        "tools" => "Developer Tools",
        "vcs" => "Version Control",
        "wasm" => "WebAssembly",
        "wrappers" => "Shell Wrappers",
        "xcode" => "Xcode",
        other => panic!("unknown category '{other}' — add it to category_display_name() in src/docs.rs")
    }
}

fn render_command_entry(doc: &CommandDoc) -> String {
    let mut out = String::new();
    out.push_str(&format!("### `{}`\n", doc.name));
    out.push_str(&format!(
        "<p class=\"cmd-url\"><a href=\"{}\">{}</a></p>\n\n",
        doc.url, doc.url,
    ));
    if !doc.aliases.is_empty() {
        let alias_str: Vec<String> = doc.aliases.iter().map(|a| format!("`{a}`")).collect();
        out.push_str(&format!("Aliases: {}\n\n", alias_str.join(", ")));
    }
    out.push_str(&format!("{}\n\n", doc.description));
    if !doc.examples.is_empty() {
        out.push_str("**Examples:**\n\n");
        for ex in &doc.examples {
            out.push_str(&format!("- `{ex}`\n"));
        }
        out.push('\n');
    }
    out
}

pub fn render_book(docs: &[CommandDoc], output_dir: &std::path::Path) {
    use std::collections::BTreeMap;
    use std::fs;

    let commands_dir = output_dir.join("src").join("commands");
    fs::create_dir_all(&commands_dir).expect("failed to create commands dir");

    let mut by_category: BTreeMap<&str, Vec<&CommandDoc>> = BTreeMap::new();
    for doc in docs {
        by_category.entry(&doc.category).or_default().push(doc);
    }

    let total: usize = by_category.values().map(|v| v.len()).sum();

    let includes_dir = output_dir.join("src").join("includes");
    fs::create_dir_all(&includes_dir).expect("failed to create includes dir");
    fs::write(includes_dir.join("command-count.md"), format!("{total}\n"))
        .expect("failed to write command-count.md");

    let version = env!("CARGO_PKG_VERSION");
    fs::write(
        output_dir.join("src").join("version-footer.js"),
        format!(
            "document.addEventListener('DOMContentLoaded', function() {{\n\
             \x20   var nav = document.querySelector('.nav-wide-wrapper') || document.querySelector('.nav-wrapper');\n\
             \x20   if (nav) {{\n\
             \x20       var footer = document.createElement('div');\n\
             \x20       footer.className = 'version-footer';\n\
             \x20       footer.textContent = 'safe-chains v{version} · {total} commands';\n\
             \x20       nav.parentNode.insertBefore(footer, nav.nextSibling);\n\
             \x20   }}\n\
             }});\n"
        ),
    )
    .expect("failed to write version-footer.js");

    let mut readme = format!(
        "# Command Reference\n\n\
         safe-chains knows {total} commands across {} categories.\n\n\
         ## Glossary\n\n{GLOSSARY}\n",
        by_category.len(),
    );
    for (slug, cmds) in &by_category {
        let name = category_display_name(slug);
        readme.push_str(&format!(
            "- [{}]({}.md) ({} commands)\n",
            name, slug, cmds.len(),
        ));
    }
    readme.push('\n');
    fs::write(commands_dir.join("README.md"), &readme)
        .expect("failed to write commands/README.md");

    for (slug, cmds) in &by_category {
        let name = category_display_name(slug);
        let mut page = format!("# {name}\n\n");
        for doc in cmds {
            page.push_str(&render_command_entry(doc));
        }
        fs::write(commands_dir.join(format!("{slug}.md")), &page)
            .expect("failed to write category page");
    }

    eprintln!("Generated {} category pages:", by_category.len());
    for slug in by_category.keys() {
        eprintln!("  - [{}](commands/{}.md)", category_display_name(slug), slug);
    }
}

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

    #[test]
    fn all_commands_have_url() {
        for doc in all_command_docs() {
            assert!(!doc.url.is_empty(), "{} has no documentation URL", doc.name);
            assert!(
                doc.url.starts_with("https://"),
                "{} URL must use https: {}",
                doc.name,
                doc.url
            );
        }
    }

    #[test]
    fn all_commands_have_valid_category() {
        for doc in all_command_docs() {
            assert!(!doc.category.is_empty(), "{} has no category", doc.name);
            category_display_name(&doc.category);
        }
    }

    #[test]
    fn builder_two_sections() {
        let ws = WordSet::new(&["--version", "list", "show"]);
        assert_eq!(doc(&ws).build(), "- Subcommands: list, show\n- Flags: --version");
    }

    #[test]
    fn builder_subcommands_only() {
        let ws = WordSet::new(&["list", "show"]);
        assert_eq!(doc(&ws).build(), "- Subcommands: list, show");
    }

    #[test]
    fn builder_flags_only() {
        let ws = WordSet::new(&["--check", "--version"]);
        assert_eq!(doc(&ws).build(), "- Flags: --check, --version");
    }

    #[test]
    fn builder_three_sections() {
        let ws = WordSet::new(&["--version", "list", "show"]);
        assert_eq!(
            doc(&ws).section("Guarded: foo (bar only).").build(),
            "- Subcommands: list, show\n- Flags: --version\n- Guarded: foo (bar only)."
        );
    }

    #[test]
    fn builder_multi_word_merged() {
        let ws = WordSet::new(&["--version", "info", "show"]);
        let multi: &[(&str, WordSet)] =
            &[("config", WordSet::new(&["get", "list"]))];
        assert_eq!(
            doc_multi(&ws, multi).build(),
            "- Subcommands: config get, config list, info, show\n- Flags: --version"
        );
    }

    #[test]
    fn builder_multi_word_with_extra_section() {
        let ws = WordSet::new(&["--version", "show"]);
        let multi: &[(&str, WordSet)] =
            &[("config", WordSet::new(&["get", "list"]))];
        assert_eq!(
            doc_multi(&ws, multi).section("Guarded: foo.").build(),
            "- Subcommands: config get, config list, show\n- Flags: --version\n- Guarded: foo."
        );
    }

    #[test]
    fn builder_no_flags_with_extra() {
        let ws = WordSet::new(&["list", "show"]);
        assert_eq!(
            doc(&ws).section("Also: foo.").build(),
            "- Subcommands: list, show\n- Also: foo."
        );
    }

    #[test]
    fn builder_custom_sections_only() {
        assert_eq!(
            DocBuilder::new()
                .section("Read-only: foo.")
                .section("Always safe: bar.")
                .section("Guarded: baz.")
                .build(),
            "- Read-only: foo.\n- Always safe: bar.\n- Guarded: baz."
        );
    }

    #[test]
    fn builder_triple_word() {
        let ws = WordSet::new(&["--version", "diff"]);
        let triples: &[(&str, &str, WordSet)] =
            &[("git", "remote", WordSet::new(&["list"]))];
        assert_eq!(
            doc(&ws).triple_word(triples).build(),
            "- Subcommands: diff, git remote list\n- Flags: --version"
        );
    }

    #[test]
    fn builder_subcommand_method() {
        let ws = WordSet::new(&["--version", "list"]);
        assert_eq!(
            doc(&ws).subcommand("plugin-list").build(),
            "- Subcommands: list, plugin-list\n- Flags: --version"
        );
    }

}
