use clap::{Arg, ArgAction, Command};
use mlua::prelude::*;

use crate::error::{Result, SenlError};

/// Build a `clap::Command` from a Lua CLI definition table.
pub fn build_command(cli_def: &LuaTable) -> Result<Command> {
    let name: String = cli_def.get("name")?;
    let about: Option<String> = cli_def.get("about")?;
    let version: Option<String> = cli_def.get("version")?;

    let mut cmd = Command::new(name);
    if let Some(about) = about {
        cmd = cmd.about(about);
    }
    if let Some(version) = version {
        cmd = cmd.version(version);
    }

    let commands: LuaTable = cli_def
        .get("commands")
        .map_err(|_| SenlError::App("CLI definition must have a 'commands' table".into()))?;

    for pair in commands.pairs::<String, LuaTable>() {
        let (cmd_name, cmd_def) = pair?;
        let sub = build_subcommand(&cmd_name, &cmd_def)?;
        cmd = cmd.subcommand(sub);
    }

    cmd = cmd.subcommand_required(true);
    cmd = cmd.arg_required_else_help(true);

    Ok(cmd)
}

fn build_subcommand(name: &str, def: &LuaTable) -> Result<Command> {
    let about: Option<String> = def.get("about")?;
    let mut sub = Command::new(name.to_string());
    if let Some(about) = about {
        sub = sub.about(about);
    }

    if let Ok(args) = def.get::<LuaTable>("args") {
        for pair in args.sequence_values::<LuaTable>() {
            let arg_def = pair?;
            let arg_name: String = arg_def.get("name")?;
            let help: String = arg_def.get::<Option<String>>("help")?.unwrap_or_default();
            let required: bool = arg_def.get::<Option<bool>>("required")?.unwrap_or(true);

            let arg = Arg::new(arg_name.clone()).help(help).required(required);
            sub = sub.arg(arg);
        }
    }

    if let Ok(flags) = def.get::<LuaTable>("flags") {
        for pair in flags.sequence_values::<LuaTable>() {
            let flag_def = pair?;
            let long: String = flag_def.get("long")?;
            let short: Option<String> = flag_def.get::<Option<String>>("short")?;
            let help: String = flag_def.get::<Option<String>>("help")?.unwrap_or_default();

            let mut arg = Arg::new(long.clone())
                .long(long)
                .help(help)
                .action(ArgAction::SetTrue);

            if let Some(s) = short {
                if let Some(c) = s.chars().next() {
                    arg = arg.short(c);
                }
            }
            sub = sub.arg(arg);
        }
    }

    // Options (value-taking flags)
    if let Ok(options) = def.get::<LuaTable>("options") {
        for pair in options.sequence_values::<LuaTable>() {
            let opt_def = pair?;
            let long: String = opt_def.get("long")?;
            let short: Option<String> = opt_def.get::<Option<String>>("short")?;
            let help: String = opt_def.get::<Option<String>>("help")?.unwrap_or_default();
            let default: Option<String> = opt_def.get::<Option<String>>("default")?;
            let required: bool = opt_def.get::<Option<bool>>("required")?.unwrap_or(false);

            let mut arg = Arg::new(long.clone())
                .long(long)
                .help(help)
                .action(ArgAction::Set)
                .required(required);

            if let Some(s) = short {
                if let Some(c) = s.chars().next() {
                    arg = arg.short(c);
                }
            }
            if let Some(d) = default {
                arg = arg.default_value(d);
            }
            sub = sub.arg(arg);
        }
    }

    Ok(sub)
}

#[cfg(test)]
mod tests {
    use crate::runtime;

    fn build_from_lua(lua_source: &str) -> crate::error::Result<clap::Command> {
        let lua = runtime::create_lua(&[], &[]).unwrap();
        let cli_def: mlua::prelude::LuaTable = lua.load(lua_source).eval().unwrap();
        super::build_command(&cli_def)
    }

    #[test]
    fn minimal_command_builds() {
        let cmd = build_from_lua(
            r#"
            return {
                name = "myapp",
                commands = { hello = { about = "say hi" } },
                routes = {},
            }
        "#,
        )
        .unwrap();
        assert_eq!(cmd.get_name(), "myapp");
    }

    #[test]
    fn subcommand_registered() {
        let cmd = build_from_lua(
            r#"
            return {
                name = "app",
                commands = {
                    greet = { about = "greet" },
                    bye = { about = "bye" },
                },
                routes = {},
            }
        "#,
        )
        .unwrap();
        let sub_names: Vec<&str> = cmd.get_subcommands().map(|s| s.get_name()).collect();
        assert!(sub_names.contains(&"greet"));
        assert!(sub_names.contains(&"bye"));
    }

    #[test]
    fn missing_commands_table_is_error() {
        let result = build_from_lua(
            r#"
            return { name = "broken" }
        "#,
        );
        assert!(result.is_err());
    }

    #[test]
    fn version_is_set() {
        let cmd = build_from_lua(
            r#"
            return {
                name = "v",
                version = "1.2.3",
                commands = { x = { about = "x" } },
                routes = {},
            }
        "#,
        )
        .unwrap();
        assert_eq!(cmd.get_version(), Some("1.2.3"));
    }

    #[test]
    fn parses_with_positional_arg() {
        let cmd = build_from_lua(
            r#"
            return {
                name = "app",
                commands = {
                    greet = {
                        about = "g",
                        args = { { name = "target", help = "who" } },
                    },
                },
                routes = {},
            }
        "#,
        )
        .unwrap();
        let matches = cmd.try_get_matches_from(["app", "greet", "world"]).unwrap();
        let (_, sub) = matches.subcommand().unwrap();
        assert_eq!(sub.get_one::<String>("target").unwrap(), "world");
    }

    #[test]
    fn option_with_default() {
        let cmd = build_from_lua(
            r#"
            return {
                name = "app",
                commands = {
                    serve = {
                        about = "s",
                        options = {
                            { long = "port", short = "p", help = "port", default = "3000" },
                        },
                    },
                },
                routes = {},
            }
        "#,
        )
        .unwrap();
        let matches = cmd.try_get_matches_from(["app", "serve"]).unwrap();
        let (_, sub) = matches.subcommand().unwrap();
        assert_eq!(sub.get_one::<String>("port").unwrap(), "3000");
    }
}
