use std::fmt;

use std::path::PathBuf;

use heck::AsPascalCase;
use indexmap::IndexMap;

use crate::spec::cmd::SpecCommand;
use crate::Spec;

pub mod python;
pub mod typescript;

#[derive(Debug, Clone)]
pub enum SdkLanguage {
    TypeScript,
    Python,
}

#[derive(Debug, Clone)]
pub struct SdkOptions {
    pub language: SdkLanguage,
    pub package_name: Option<String>,
    pub source_file: Option<String>,
}

#[derive(Debug)]
pub struct SdkOutput {
    pub files: Vec<SdkFile>,
}

#[derive(Debug)]
pub struct SdkFile {
    pub path: PathBuf,
    pub content: String,
}

pub fn generate(spec: &Spec, opts: &SdkOptions) -> SdkOutput {
    match opts.language {
        SdkLanguage::TypeScript => typescript::generate(spec, opts),
        SdkLanguage::Python => python::generate(spec, opts),
    }
}

/// Escape JSDoc-terminating sequences in comment text.
pub(crate) fn escape_jsdoc(s: &str) -> String {
    s.replace("*/", r"*\/")
}

/// Escape triple-quote sequences and backslashes in Python docstrings.
pub(crate) fn escape_py_docstring(s: &str) -> String {
    s.replace('\\', r"\\").replace(r#"""""#, r#"\"\"\""#)
}

/// Escape backslashes and double quotes for Python string literals.
pub(crate) fn escape_py_string(s: &str) -> String {
    s.replace('\\', r"\\").replace('"', r#"\""#)
}

/// Escape backslashes and double quotes for TypeScript string literals.
pub(crate) fn escape_ts_string(s: &str) -> String {
    s.replace('\\', r"\\").replace('"', r#"\""#)
}

/// A simple code writer with indentation management.
pub(crate) struct CodeWriter {
    buf: String,
    indent: usize,
    indent_str: &'static str,
}

impl CodeWriter {
    pub fn new() -> Self {
        Self {
            buf: String::new(),
            indent: 0,
            indent_str: "  ",
        }
    }

    /// Create a CodeWriter with custom indent string (e.g. "    " for Python).
    pub fn with_indent(indent_str: &'static str) -> Self {
        Self {
            buf: String::new(),
            indent: 0,
            indent_str,
        }
    }

    pub fn line(&mut self, s: &str) {
        if !s.is_empty() {
            for _ in 0..self.indent {
                self.buf.push_str(self.indent_str);
            }
        }
        self.buf.push_str(s);
        self.buf.push('\n');
    }

    pub fn indent(&mut self) {
        self.indent += 1;
    }

    pub fn dedent(&mut self) {
        self.indent = self.indent.saturating_sub(1);
    }

    pub fn finish(self) -> String {
        self.buf
    }
}

impl fmt::Display for CodeWriter {
    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
        write!(f, "{}", self.buf)
    }
}

pub(crate) fn generated_header(comment_prefix: &str, source: &Option<String>) -> String {
    match source {
        Some(s) => {
            format!("{comment_prefix} @generated by usage-cli from {s}. Do not edit manually.")
        }
        None => format!("{comment_prefix} @generated by usage-cli. Do not edit manually."),
    }
}

/// Returns the PascalCase type name for a command: the command name, or the package name for root.
pub(crate) fn command_type_name(cmd: &SpecCommand, package_name: &str) -> String {
    if cmd.name.is_empty() {
        AsPascalCase(package_name).to_string()
    } else {
        AsPascalCase(&cmd.name).to_string()
    }
}

// ---------------------------------------------------------------------------
// Choice type collection with collision detection
// ---------------------------------------------------------------------------

/// Maps choice type definitions and provides collision-aware type name lookup.
///
/// When two commands have the same arg/flag name with different choices,
/// the type name is prefixed with the command's PascalCase name to avoid collision.
pub(crate) struct ChoiceTypeMap {
    /// Resolved type name -> choice values
    pub types: IndexMap<String, Vec<String>>,
    /// (cmd_name, item_name) -> resolved type name
    name_map: IndexMap<(String, String), String>,
}

impl ChoiceTypeMap {
    /// Look up the resolved type name for a choice arg/flag.
    /// `cmd_name` is the command's name (empty string for root).
    /// `item_name` is the arg or flag name.
    pub fn lookup(&self, cmd_name: &str, item_name: &str) -> Option<&str> {
        self.name_map
            .get(&(cmd_name.to_string(), item_name.to_string()))
            .map(|s| s.as_str())
    }

    /// Iterate over type definitions (name, choices).
    pub fn iter(&self) -> indexmap::map::Iter<'_, String, Vec<String>> {
        self.types.iter()
    }

    /// Check if there are no choice types.
    pub fn is_empty(&self) -> bool {
        self.types.is_empty()
    }
}

struct ChoiceEntry {
    base_name: String,
    item_name: String,
    cmd_name: String,
    cmd_prefix: String,
    choices: Vec<String>,
}

/// Collects all unique choice types across a command tree.
/// When two commands have the same arg/flag name with different choices,
/// the type name is prefixed with the command's PascalCase name to avoid collision.
pub(crate) fn collect_choice_types(cmd: &SpecCommand) -> ChoiceTypeMap {
    let mut all_entries: Vec<ChoiceEntry> = Vec::new();
    collect_choice_entries(cmd, &mut all_entries);

    // Group by base type name, check for choice differences
    let mut base_groups: IndexMap<String, Vec<&ChoiceEntry>> = IndexMap::new();
    for entry in &all_entries {
        base_groups
            .entry(entry.base_name.clone())
            .or_default()
            .push(entry);
    }

    let mut types = IndexMap::new();
    let mut name_map = IndexMap::new();
    for (base_name, entries) in &base_groups {
        let all_same = entries.windows(2).all(|w| w[0].choices == w[1].choices);
        if all_same {
            types.insert(base_name.clone(), entries[0].choices.clone());
            for entry in entries {
                name_map.insert(
                    (entry.cmd_name.clone(), entry.item_name.clone()),
                    base_name.clone(),
                );
            }
        } else {
            for entry in entries {
                let prefixed = format!("{}{}", entry.cmd_prefix, base_name);
                types.insert(prefixed.clone(), entry.choices.clone());
                name_map.insert((entry.cmd_name.clone(), entry.item_name.clone()), prefixed);
            }
        }
    }

    ChoiceTypeMap { types, name_map }
}

fn collect_choice_entries(cmd: &SpecCommand, entries: &mut Vec<ChoiceEntry>) {
    if cmd.hide {
        return;
    }

    let cmd_prefix = if cmd.name.is_empty() {
        String::new()
    } else {
        AsPascalCase(&cmd.name).to_string()
    };
    let cmd_name = cmd.name.clone();

    for arg in &cmd.args {
        if arg.hide {
            continue;
        }
        if let Some(choices) = &arg.choices {
            let base_name = format!("{}Choice", AsPascalCase(&arg.name));
            entries.push(ChoiceEntry {
                base_name,
                item_name: arg.name.clone(),
                cmd_name: cmd_name.clone(),
                cmd_prefix: cmd_prefix.clone(),
                choices: choices.choices.clone(),
            });
        }
    }

    for flag in &cmd.flags {
        if flag.hide {
            continue;
        }
        if let Some(arg) = &flag.arg {
            if let Some(choices) = &arg.choices {
                let base_name = format!("{}Choice", AsPascalCase(&flag.name));
                entries.push(ChoiceEntry {
                    base_name,
                    item_name: flag.name.clone(),
                    cmd_name: cmd_name.clone(),
                    cmd_prefix: cmd_prefix.clone(),
                    choices: choices.choices.clone(),
                });
            }
        }
    }

    for subcmd in cmd.subcommands.values() {
        collect_choice_entries(subcmd, entries);
    }
}

/// Collects type names that need to be imported from the types module.
pub(crate) fn collect_type_imports(
    cmd: &SpecCommand,
    package_name: &str,
    choice_types: &ChoiceTypeMap,
) -> Vec<String> {
    let mut imports = Vec::new();
    collect_type_imports_recursive(cmd, package_name, choice_types, &mut imports);
    imports.sort();
    imports.dedup();
    imports
}

fn collect_type_imports_recursive(
    cmd: &SpecCommand,
    package_name: &str,
    choice_types: &ChoiceTypeMap,
    imports: &mut Vec<String>,
) {
    if cmd.hide {
        return;
    }

    let name = command_type_name(cmd, package_name);
    let has_args = cmd.args.iter().any(|a| !a.hide);
    let has_flags = cmd.flags.iter().any(|f| !f.hide);

    if has_args {
        imports.push(format!("{name}Args"));
    }
    if has_flags {
        imports.push(format!("{name}Flags"));
    }

    for arg in &cmd.args {
        if !arg.hide && arg.choices.is_some() {
            if let Some(type_name) = choice_types.lookup(&cmd.name, &arg.name) {
                imports.push(type_name.to_string());
            }
        }
    }
    for flag in &cmd.flags {
        if !flag.hide {
            if let Some(arg) = &flag.arg {
                if arg.choices.is_some() {
                    if let Some(type_name) = choice_types.lookup(&cmd.name, &flag.name) {
                        imports.push(type_name.to_string());
                    }
                }
            }
        }
    }

    for subcmd in cmd.subcommands.values() {
        collect_type_imports_recursive(subcmd, package_name, choice_types, imports);
    }
}

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

    #[test]
    fn test_code_writer_display() {
        let mut w = CodeWriter::with_indent("    ");
        w.line("hello");
        w.line("world");
        let displayed = format!("{w}");
        assert!(displayed.contains("hello"));
        assert!(displayed.contains("world"));
    }

    #[test]
    fn test_command_type_name_empty() {
        let cmd = SpecCommand::default();
        assert!(cmd.name.is_empty());
        let result = command_type_name(&cmd, "mypackage");
        assert_eq!(result, "Mypackage");
    }

    #[test]
    fn test_generated_header_with_source() {
        let result = generated_header("//", &Some("test.kdl".to_string()));
        assert!(result.contains("test.kdl"));
    }

    #[test]
    fn test_generated_header_without_source() {
        let result = generated_header("//", &None);
        assert!(!result.contains("test.kdl"));
        assert!(result.contains("@generated"));
    }

    /// Hidden command with choices — covers collect_choice_entries skip paths
    /// and collect_type_imports_recursive cmd.hide path.
    #[test]
    fn test_hidden_command_with_choices() {
        let spec: crate::Spec = r##"
            bin "app"
            cmd "visible" help="Visible" {
                arg "env" help="Environment" {
                    choices "dev" "prod"
                }
            }
            cmd "hidden" hide=#true help="Hidden" {
                arg "mode" help="Mode" {
                    choices "fast" "slow"
                }
                flag "--level <n>" help="Level" {
                    choices "1" "2" "3"
                }
            }
        "##
        .parse()
        .unwrap();
        let choice_types = collect_choice_types(&spec.cmd);
        // hidden command's choices should not be collected
        assert!(choice_types.lookup("hidden", "mode").is_none());
        assert!(choice_types.lookup("hidden", "level").is_none());
        // visible command's choices should be collected
        assert!(choice_types.lookup("visible", "env").is_some());
    }

    /// Hidden arg/flag with choices — covers skip paths in collect_choice_entries.
    #[test]
    fn test_hidden_arg_flag_with_choices() {
        let spec: crate::Spec = r##"
            bin "app"
            arg "visible_choice" help="Visible" {
                choices "a" "b"
            }
            arg "hidden_choice" hide=#true help="Hidden" {
                choices "x" "y"
            }
            flag "--visible-flag <val>" help="Visible" {
                choices "m" "n"
            }
            flag "--hidden-flag <val>" hide=#true help="Hidden" {
                choices "p" "q"
            }
        "##
        .parse()
        .unwrap();
        let choice_types = collect_choice_types(&spec.cmd);
        assert!(choice_types.lookup("app", "visible_choice").is_some());
        assert!(choice_types.lookup("app", "hidden_choice").is_none());
        assert!(choice_types.lookup("app", "visible-flag").is_some());
        assert!(choice_types.lookup("app", "hidden-flag").is_none());
    }

    /// Flag with arg choices — covers flag.arg choices import path.
    #[test]
    fn test_flag_arg_choices_import() {
        let spec: crate::Spec = r##"
            bin "app"
            flag "--shell <shell>" help="Shell type" {
                choices "bash" "zsh" "fish"
            }
        "##
        .parse()
        .unwrap();
        let choice_types = collect_choice_types(&spec.cmd);
        let mut imports = Vec::new();
        collect_type_imports_recursive(&spec.cmd, "app", &choice_types, &mut imports);
        assert!(imports.iter().any(|i| i.contains("Choice")));
    }
}
