mod builder;
mod dispatch;
mod error;
mod helpers;
mod runtime;

pub use error::{Result, SenlError};

use mlua::prelude::*;
use std::ffi::OsString;
use std::path::{Path, PathBuf};

/// A Lua-powered CLI application.
///
/// # Examples
///
/// ```no_run
/// use senl::SenlApp;
///
/// let exit_code = SenlApp::from_file("hello.lua")
///     .unwrap()
///     .run()
///     .unwrap();
/// std::process::exit(exit_code);
/// ```
pub struct SenlApp {
    source: AppSource,
    search_paths: Vec<String>,
    preloads: Vec<(String, String)>,
}

enum AppSource {
    File(PathBuf),
    Source { name: String, code: String },
}

impl SenlApp {
    /// Load a CLI app from a Lua file on disk.
    pub fn from_file(path: impl AsRef<Path>) -> Result<Self> {
        let path = path.as_ref().canonicalize()?;
        Ok(Self {
            source: AppSource::File(path),
            search_paths: Vec::new(),
            preloads: Vec::new(),
        })
    }

    /// Load a CLI app from an inline Lua source string.
    ///
    /// Useful with `include_str!` for compile-time embedding.
    pub fn from_source(name: impl Into<String>, code: impl Into<String>) -> Self {
        Self {
            source: AppSource::Source {
                name: name.into(),
                code: code.into(),
            },
            search_paths: Vec::new(),
            preloads: Vec::new(),
        }
    }

    /// Load a CLI app from a directory containing `init.lua`.
    ///
    /// The directory is automatically added to the Lua search path,
    /// so modules within it can be `require`d by the app.
    pub fn from_dir(dir: impl AsRef<Path>) -> Result<Self> {
        let dir = dir.as_ref().canonicalize()?;
        let entry = ["init.lua", "main.lua"]
            .iter()
            .map(|f| dir.join(f))
            .find(|p| p.exists())
            .ok_or_else(|| {
                SenlError::App(format!("no init.lua or main.lua in {}", dir.display()))
            })?;
        Ok(Self {
            source: AppSource::File(entry),
            search_paths: Vec::new(),
            preloads: Vec::new(),
        })
    }

    /// Add a directory to the Lua `package.path` search list.
    ///
    /// Modules in this directory can be `require`d by the app.
    #[must_use]
    pub fn with_lua_search_path(mut self, path: impl Into<String>) -> Self {
        self.search_paths.push(path.into());
        self
    }

    /// Register a Lua source string as a preloaded module.
    ///
    /// After registration, `require(name)` in Lua resolves to this source.
    ///
    /// # Examples
    ///
    /// ```no_run
    /// use senl::SenlApp;
    ///
    /// let exit_code = SenlApp::from_source("myapp", include_str!("../examples/hello.lua"))
    ///     .with_preload("mylib", "return { version = '1.0' }")
    ///     .run_with_args(["greet", "World"])
    ///     .unwrap();
    /// ```
    #[must_use]
    pub fn with_preload(mut self, name: impl Into<String>, source: impl Into<String>) -> Self {
        self.preloads.push((name.into(), source.into()));
        self
    }

    /// Register multiple Lua source files as preloaded modules.
    ///
    /// Each `(path, source)` entry is mapped to a module name under `base_name`:
    /// - `"init.lua"` → `base_name`
    /// - `"sub/init.lua"` → `"base_name.sub"`
    /// - `"sub/mod.lua"` → `"base_name.sub.mod"`
    ///
    /// # Examples
    ///
    /// ```no_run
    /// use senl::SenlApp;
    ///
    /// let files: &[(&str, &str)] = &[
    ///     ("init.lua", "return { version = '1.0' }"),
    ///     ("utils.lua", "return { helper = function() end }"),
    /// ];
    ///
    /// let exit_code = SenlApp::from_source("myapp", "return require('lua_sen').app { name = 'x', commands = {} }")
    ///     .with_preload_dir("mylib", files)
    ///     .run_with_args(["--help"])
    ///     .unwrap_or(0);
    /// ```
    #[must_use]
    pub fn with_preload_dir(mut self, base_name: &str, files: &[(&str, &str)]) -> Self {
        for (path, source) in files {
            let module_name = path_to_module_name(base_name, path);
            self.preloads.push((module_name, (*source).to_string()));
        }
        self
    }

    /// Run the app using `std::env::args()`, skipping only argv\[0\].
    ///
    /// Use this for standalone binaries where `from_file`/`from_source`/`from_dir`
    /// already know the app source, and argv\[1..\] are the subcommand + args.
    pub fn run(self) -> Result<i32> {
        let args: Vec<String> = std::env::args().collect();
        let user_args: Vec<&str> = args.iter().skip(1).map(|s| s.as_str()).collect();
        self.run_inner(&user_args)
    }

    /// Run the app with explicit arguments (not including argv\[0\]).
    ///
    /// # Examples
    ///
    /// ```no_run
    /// use senl::SenlApp;
    ///
    /// let exit_code = SenlApp::from_source("demo", include_str!("../examples/hello.lua"))
    ///     .run_with_args(["greet", "World"])
    ///     .unwrap();
    /// ```
    pub fn run_with_args<I, T>(self, args: I) -> Result<i32>
    where
        I: IntoIterator<Item = T>,
        T: Into<OsString> + AsRef<str>,
    {
        let args: Vec<String> = args.into_iter().map(|a| a.as_ref().to_string()).collect();
        let refs: Vec<&str> = args.iter().map(|s| s.as_str()).collect();
        self.run_inner(&refs)
    }

    fn run_inner(self, user_args: &[&str]) -> Result<i32> {
        let lua = runtime::create_lua(&self.search_paths, &self.preloads)?;

        // Load app source and get CLI definition table
        let (name, cli_def) = match &self.source {
            AppSource::File(path) => {
                runtime::add_app_dir_to_path(&lua, path)?;
                let src = std::fs::read_to_string(path)?;
                let file_name = path
                    .file_name()
                    .map(|f| f.to_string_lossy().to_string())
                    .unwrap_or_else(|| "app".into());
                let def: LuaTable = lua
                    .load(&src)
                    .set_name(&file_name)
                    .eval()
                    .map_err(|e| SenlError::App(format!("failed to load {file_name}: {e}")))?;
                let name: String = def.get("name")?;
                (name, def)
            }
            AppSource::Source { name, code } => {
                let def: LuaTable = lua
                    .load(code.as_str())
                    .set_name(name.as_str())
                    .eval()
                    .map_err(|e| SenlError::App(format!("failed to load {name}: {e}")))?;
                let cli_name: String = def.get("name")?;
                (cli_name, def)
            }
        };

        // Build clap command and parse
        let cmd = builder::build_command(&cli_def)?;
        let mut clap_args: Vec<&str> = Vec::with_capacity(user_args.len() + 1);
        clap_args.push(name.as_str());
        clap_args.extend(user_args);
        let matches = cmd.try_get_matches_from(clap_args)?;

        // Dispatch to Lua handler
        let (exit_code, output) = dispatch::dispatch(&lua, &cli_def, &matches)?;

        if !output.is_empty() {
            println!("{output}");
        }

        Ok(exit_code)
    }
}

/// Convert a file path to a Lua module name under `base_name`.
///
/// - `"init.lua"` → `base_name`
/// - `"sub/init.lua"` → `"base_name.sub"`
/// - `"sub/mod.lua"` → `"base_name.sub.mod"`
fn path_to_module_name(base_name: &str, path: &str) -> String {
    let stripped = path
        .strip_suffix(".lua")
        .unwrap_or(path)
        .replace(['/', '\\'], ".");

    match stripped.strip_suffix(".init") {
        Some("") | None if stripped == "init" => base_name.to_string(),
        Some(prefix) => format!("{base_name}.{prefix}"),
        None => format!("{base_name}.{stripped}"),
    }
}

/// A standalone Lua runtime with senl helpers registered.
///
/// Use this for headless (non-CLI) scenarios where you need the
/// senl Lua environment without the clap-based CLI framework.
///
/// # Examples
///
/// ```
/// use senl::Runtime;
///
/// let rt = Runtime::builder()
///     .with_preload("mymod", "return { value = 42 }")
///     .build()
///     .unwrap();
///
/// let val: i32 = rt.eval("return require('mymod').value").unwrap();
/// assert_eq!(val, 42);
/// ```
pub struct Runtime {
    lua: Lua,
}

impl Runtime {
    /// Create a runtime with default settings (no extra preloads or search paths).
    pub fn new() -> Result<Self> {
        Self::builder().build()
    }

    /// Start building a runtime with custom configuration.
    pub fn builder() -> RuntimeBuilder {
        RuntimeBuilder::default()
    }

    /// Access the underlying `mlua::Lua` instance.
    pub fn lua(&self) -> &Lua {
        &self.lua
    }

    /// Evaluate a Lua expression and return the result.
    pub fn eval<T: FromLua>(&self, code: &str) -> Result<T> {
        Ok(self.lua.load(code).eval()?)
    }
}

/// Builder for configuring a [`Runtime`].
#[derive(Default)]
pub struct RuntimeBuilder {
    search_paths: Vec<String>,
    preloads: Vec<(String, String)>,
}

impl RuntimeBuilder {
    /// Add a directory to the Lua `package.path` search list.
    pub fn with_search_path(mut self, path: impl Into<String>) -> Self {
        self.search_paths.push(path.into());
        self
    }

    /// Register a Lua source string as a preloaded module.
    pub fn with_preload(mut self, name: impl Into<String>, source: impl Into<String>) -> Self {
        self.preloads.push((name.into(), source.into()));
        self
    }

    /// Register multiple Lua source files as preloaded modules.
    ///
    /// See [`SenlApp::with_preload_dir`] for path mapping rules.
    #[must_use]
    pub fn with_preload_dir(mut self, base_name: &str, files: &[(&str, &str)]) -> Self {
        for (path, source) in files {
            let module_name = path_to_module_name(base_name, path);
            self.preloads.push((module_name, (*source).to_string()));
        }
        self
    }

    /// Build the runtime.
    pub fn build(self) -> Result<Runtime> {
        let lua = runtime::create_lua(&self.search_paths, &self.preloads)?;
        Ok(Runtime { lua })
    }
}

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

    // ── path_to_module_name ────────────────────────────

    #[test]
    fn module_name_init() {
        assert_eq!(path_to_module_name("mylib", "init.lua"), "mylib");
    }

    #[test]
    fn module_name_flat_file() {
        assert_eq!(path_to_module_name("mylib", "utils.lua"), "mylib.utils");
    }

    #[test]
    fn module_name_nested_init() {
        assert_eq!(path_to_module_name("mylib", "eval/init.lua"), "mylib.eval");
    }

    #[test]
    fn module_name_nested_file() {
        assert_eq!(
            path_to_module_name("mylib", "eval/stats.lua"),
            "mylib.eval.stats"
        );
    }

    #[test]
    fn module_name_deeply_nested() {
        assert_eq!(
            path_to_module_name("ef", "eval/metrics/accuracy.lua"),
            "ef.eval.metrics.accuracy"
        );
    }

    #[test]
    fn module_name_backslash_separator() {
        assert_eq!(
            path_to_module_name("mylib", "sub\\mod.lua"),
            "mylib.sub.mod"
        );
    }

    // ── with_preload (E2E) ─────────────────────────────

    #[test]
    fn preload_module_available_via_require() {
        let lua_app = r#"
            local sen = require("lua_sen")
            local mymod = require("mymod")
            return {
                name = "test",
                commands = {
                    info = { about = "show info" },
                },
                routes = {
                    info = function(ctx)
                        return sen.ok(mymod.version)
                    end,
                },
            }
        "#;

        let exit_code = SenlApp::from_source("test", lua_app)
            .with_preload("mymod", "return { version = '1.2.3' }")
            .run_with_args(["info"])
            .unwrap();

        assert_eq!(exit_code, 0);
    }

    // ── with_preload_dir (E2E) ─────────────────────────

    #[test]
    fn preload_dir_registers_multiple_modules() {
        let lua_app = r#"
            local sen = require("lua_sen")
            local pkg = require("mypkg")
            local sub = require("mypkg.helper")
            return {
                name = "test",
                commands = {
                    check = { about = "check modules" },
                },
                routes = {
                    check = function(ctx)
                        return sen.ok(pkg.name .. "+" .. sub.name)
                    end,
                },
            }
        "#;

        let files: &[(&str, &str)] = &[
            ("init.lua", "return { name = 'pkg' }"),
            ("helper.lua", "return { name = 'helper' }"),
        ];

        let exit_code = SenlApp::from_source("test", lua_app)
            .with_preload_dir("mypkg", files)
            .run_with_args(["check"])
            .unwrap();

        assert_eq!(exit_code, 0);
    }

    // ── options (E2E) ──────────────────────────────────

    #[test]
    fn option_with_default_value() {
        let lua_app = r#"
            local sen = require("lua_sen")
            return {
                name = "test",
                commands = {
                    run = {
                        about = "run with options",
                        options = {
                            { long = "endpoint", short = "e", help = "endpoint", default = "localhost:8000" },
                        },
                    },
                },
                routes = {
                    run = function(ctx)
                        return sen.ok(ctx.args.endpoint)
                    end,
                },
            }
        "#;

        let exit_code = SenlApp::from_source("test", lua_app)
            .run_with_args(["run"])
            .unwrap();
        assert_eq!(exit_code, 0);
    }

    #[test]
    fn option_with_explicit_value() {
        let lua_app = r#"
            local sen = require("lua_sen")
            return {
                name = "test",
                commands = {
                    run = {
                        about = "run with options",
                        options = {
                            { long = "format", short = "f", help = "output format" },
                        },
                    },
                },
                routes = {
                    run = function(ctx)
                        if ctx.args.format == "json" then
                            return sen.ok("json")
                        end
                        return sen.err("unexpected format")
                    end,
                },
            }
        "#;

        let exit_code = SenlApp::from_source("test", lua_app)
            .run_with_args(["run", "--format", "json"])
            .unwrap();
        assert_eq!(exit_code, 0);
    }

    #[test]
    fn option_with_short_flag() {
        let lua_app = r#"
            local sen = require("lua_sen")
            return {
                name = "test",
                commands = {
                    run = {
                        about = "run",
                        options = {
                            { long = "output", short = "o", help = "output path" },
                        },
                    },
                },
                routes = {
                    run = function(ctx)
                        if ctx.args.output == "/tmp/out" then
                            return sen.ok("ok")
                        end
                        return sen.err("wrong value")
                    end,
                },
            }
        "#;

        let exit_code = SenlApp::from_source("test", lua_app)
            .run_with_args(["run", "-o", "/tmp/out"])
            .unwrap();
        assert_eq!(exit_code, 0);
    }

    #[test]
    fn option_missing_without_default_is_nil() {
        let lua_app = r#"
            local sen = require("lua_sen")
            return {
                name = "test",
                commands = {
                    run = {
                        about = "run",
                        options = {
                            { long = "tag", help = "optional tag" },
                        },
                    },
                },
                routes = {
                    run = function(ctx)
                        if ctx.args.tag == nil then
                            return sen.ok("nil")
                        end
                        return sen.err("should be nil")
                    end,
                },
            }
        "#;

        let exit_code = SenlApp::from_source("test", lua_app)
            .run_with_args(["run"])
            .unwrap();
        assert_eq!(exit_code, 0);
    }

    #[test]
    fn option_required_missing_returns_error() {
        let lua_app = r#"
            local sen = require("lua_sen")
            return {
                name = "test",
                commands = {
                    run = {
                        about = "run",
                        options = {
                            { long = "config", help = "config path", required = true },
                        },
                    },
                },
                routes = {
                    run = function(ctx)
                        return sen.ok("ok")
                    end,
                },
            }
        "#;

        let result = SenlApp::from_source("test", lua_app).run_with_args(["run"]);
        assert!(result.is_err());
    }

    // ── options + flags + args 混在 ────────────────────

    #[test]
    fn mixed_args_flags_options() {
        let lua_app = r#"
            local sen = require("lua_sen")
            return {
                name = "test",
                commands = {
                    deploy = {
                        about = "deploy",
                        args = {
                            { name = "target", help = "deploy target" },
                        },
                        flags = {
                            { long = "dry-run", short = "n", help = "dry run" },
                        },
                        options = {
                            { long = "env", short = "e", help = "environment", default = "staging" },
                        },
                    },
                },
                routes = {
                    deploy = function(ctx)
                        local parts = {}
                        parts[#parts+1] = ctx.args.target
                        parts[#parts+1] = ctx.args.env
                        if ctx.args["dry-run"] then
                            parts[#parts+1] = "dry"
                        end
                        return sen.ok(table.concat(parts, ","))
                    end,
                },
            }
        "#;

        let exit_code = SenlApp::from_source("test", lua_app)
            .run_with_args(["deploy", "prod", "--env", "production", "-n"])
            .unwrap();
        assert_eq!(exit_code, 0);
    }

    // ── Lua DSL builder 経由の options ─────────────────

    #[test]
    fn lua_builder_option_with_default() {
        let lua_app = r#"
            local sen = require("lua_sen")
            local app = sen.app("test", "test app")
                :command("serve", "start server")
                    :option("p", "port", "listen port", { default = "3000" })
                    :done()
            app:route("serve", function(ctx)
                if ctx.args.port == "3000" then
                    return { exit_code = 0, output = "" }
                end
                return { exit_code = 1, output = "wrong" }
            end)
            return app:build()
        "#;

        let exit_code = SenlApp::from_source("test", lua_app)
            .run_with_args(["serve"])
            .unwrap();
        assert_eq!(exit_code, 0);
    }

    #[test]
    fn lua_builder_option_explicit_value() {
        let lua_app = r#"
            local sen = require("lua_sen")
            local app = sen.app("test", "test app")
                :command("serve", "start server")
                    :option("p", "port", "listen port", { default = "3000" })
                    :done()
            app:route("serve", function(ctx)
                if ctx.args.port == "8080" then
                    return { exit_code = 0, output = "" }
                end
                return { exit_code = 1, output = "wrong: " .. tostring(ctx.args.port) }
            end)
            return app:build()
        "#;

        let exit_code = SenlApp::from_source("test", lua_app)
            .run_with_args(["serve", "--port", "8080"])
            .unwrap();
        assert_eq!(exit_code, 0);
    }

    // ── Runtime ────────────────────────────────────────

    #[test]
    fn runtime_new_default() {
        let rt = Runtime::new().unwrap();
        let val: i32 = rt.eval("return 1 + 2").unwrap();
        assert_eq!(val, 3);
    }

    #[test]
    fn runtime_lua_sen_available() {
        let rt = Runtime::new().unwrap();
        let result: String = rt
            .eval(r#"local sen = require("lua_sen"); return type(sen.app)"#)
            .unwrap();
        assert_eq!(result, "function");
    }

    #[test]
    fn runtime_builder_with_preload() {
        let rt = Runtime::builder()
            .with_preload("testmod", "return { answer = 42 }")
            .build()
            .unwrap();
        let val: i32 = rt.eval("return require('testmod').answer").unwrap();
        assert_eq!(val, 42);
    }

    #[test]
    fn runtime_builder_with_preload_dir() {
        let rt = Runtime::builder()
            .with_preload_dir(
                "mylib",
                &[
                    ("init.lua", "return { name = 'mylib' }"),
                    ("math.lua", "return { pi = 3.14 }"),
                ],
            )
            .build()
            .unwrap();

        let name: String = rt.eval("return require('mylib').name").unwrap();
        assert_eq!(name, "mylib");

        let pi: f64 = rt.eval("return require('mylib.math').pi").unwrap();
        assert!((pi - 3.14).abs() < f64::EPSILON);
    }

    #[test]
    fn runtime_helpers_available() {
        let rt = Runtime::new().unwrap();
        let result: String = rt
            .eval(r#"local env = require("senl.env"); return type(env.get)"#)
            .unwrap();
        assert_eq!(result, "function");
    }

    #[test]
    fn runtime_lua_access() {
        let rt = Runtime::new().unwrap();
        let lua = rt.lua();
        let globals = lua.globals();
        assert!(globals.contains_key("require").unwrap());
    }
}
