use mlua::prelude::*;

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

/// Dispatch a parsed `ArgMatches` to the corresponding Lua handler.
///
/// Returns `(exit_code, output)`.
pub fn dispatch(
    lua: &Lua,
    cli_def: &LuaTable,
    matches: &clap::ArgMatches,
) -> Result<(i32, String)> {
    let (sub_name, sub_matches) = matches
        .subcommand()
        .ok_or_else(|| SenlError::App("no subcommand provided".into()))?;

    let args_table = lua.create_table()?;

    let commands: LuaTable = cli_def.get("commands")?;
    let cmd_def: LuaTable = commands.get(sub_name)?;

    // Positional args
    if let Ok(arg_defs) = cmd_def.get::<LuaTable>("args") {
        for pair in arg_defs.sequence_values::<LuaTable>() {
            let arg_def = pair?;
            let name: String = arg_def.get("name")?;
            if let Some(val) = sub_matches.get_one::<String>(&name) {
                args_table.set(name, val.as_str())?;
            }
        }
    }

    // Flags
    if let Ok(flag_defs) = cmd_def.get::<LuaTable>("flags") {
        for pair in flag_defs.sequence_values::<LuaTable>() {
            let flag_def = pair?;
            let long: String = flag_def.get("long")?;
            let val = sub_matches.get_flag(&long);
            args_table.set(long, val)?;
        }
    }

    // Options (value-taking flags)
    if let Ok(opt_defs) = cmd_def.get::<LuaTable>("options") {
        for pair in opt_defs.sequence_values::<LuaTable>() {
            let opt_def = pair?;
            let long: String = opt_def.get("long")?;
            if let Some(val) = sub_matches.get_one::<String>(&long) {
                args_table.set(long, val.as_str())?;
            }
        }
    }

    // Call the Lua handler
    let routes: LuaTable = cli_def.get("routes")?;
    let handler: LuaFunction = routes.get(sub_name)?;

    let ctx = lua.create_table()?;
    ctx.set("cmd", sub_name)?;
    ctx.set("args", args_table)?;

    let result: LuaTable = handler.call(ctx)?;
    let exit_code: i32 = result.get::<Option<i32>>("exit_code")?.unwrap_or(0);
    let output: String = result.get::<Option<String>>("output")?.unwrap_or_default();

    Ok((exit_code, output))
}

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

    /// Helper: build a Lua CLI def table, parse args, and dispatch.
    fn run_dispatch(lua_source: &str, args: &[&str]) -> crate::error::Result<(i32, String)> {
        let lua = runtime::create_lua(&[], &[]).unwrap();
        let cli_def: mlua::prelude::LuaTable = lua.load(lua_source).eval().unwrap();
        let cmd = builder::build_command(&cli_def)?;

        let name: String = cli_def.get("name").unwrap();
        let mut clap_args: Vec<&str> = vec![name.as_str()];
        clap_args.extend(args);
        let matches = cmd.try_get_matches_from(clap_args)?;

        super::dispatch(&lua, &cli_def, &matches)
    }

    #[test]
    fn dispatch_returns_output() {
        let src = r#"
            local sen = require("lua_sen")
            return {
                name = "t",
                commands = { ping = { about = "p" } },
                routes = { ping = function(ctx) return sen.ok("pong") end },
            }
        "#;
        let (code, out) = run_dispatch(src, &["ping"]).unwrap();
        assert_eq!(code, 0);
        assert_eq!(out, "pong");
    }

    #[test]
    fn dispatch_error_exit_code() {
        let src = r#"
            local sen = require("lua_sen")
            return {
                name = "t",
                commands = { fail = { about = "f" } },
                routes = { fail = function(ctx) return sen.err("bad", 42) end },
            }
        "#;
        let (code, out) = run_dispatch(src, &["fail"]).unwrap();
        assert_eq!(code, 42);
        assert_eq!(out, "bad");
    }

    #[test]
    fn dispatch_no_subcommand_is_error() {
        let src = r#"
            local sen = require("lua_sen")
            return {
                name = "t",
                commands = { x = { about = "x" } },
                routes = { x = function(ctx) return sen.ok("") end },
            }
        "#;
        // clap requires subcommand, so try_get_matches_from will fail
        let result = run_dispatch(src, &[]);
        assert!(result.is_err());
    }

    #[test]
    fn dispatch_passes_positional_arg() {
        let src = r#"
            local sen = require("lua_sen")
            return {
                name = "t",
                commands = {
                    greet = {
                        about = "g",
                        args = { { name = "who", help = "target" } },
                    },
                },
                routes = {
                    greet = function(ctx) return sen.ok("hi " .. ctx.args.who) end,
                },
            }
        "#;
        let (_, out) = run_dispatch(src, &["greet", "world"]).unwrap();
        assert_eq!(out, "hi world");
    }

    #[test]
    fn dispatch_passes_flag() {
        let src = r#"
            local sen = require("lua_sen")
            return {
                name = "t",
                commands = {
                    run = {
                        about = "r",
                        flags = { { long = "verbose", short = "v", help = "v" } },
                    },
                },
                routes = {
                    run = function(ctx)
                        if ctx.args.verbose then return sen.ok("loud") end
                        return sen.ok("quiet")
                    end,
                },
            }
        "#;
        let (_, out) = run_dispatch(src, &["run", "--verbose"]).unwrap();
        assert_eq!(out, "loud");
    }
}
