use mlua::prelude::*;
use std::path::Path;

use crate::error::Result;

// Embedded lua_sen framework modules
const LUA_SEN_INIT: &str = include_str!("../lua/lua_sen/init.lua");
const LUA_SEN_CLI: &str = include_str!("../lua/lua_sen/cli.lua");
const LUA_SEN_ROUTER: &str = include_str!("../lua/lua_sen/router.lua");
const LUA_SEN_RESPONSE: &str = include_str!("../lua/lua_sen/response.lua");

/// Initialize a Lua runtime with the embedded lua_sen framework.
///
/// Registers the built-in `lua_sen` modules and any user-provided
/// preload modules into `package.preload`, then registers Rust
/// helper modules into `package.loaded`.
pub fn create_lua(search_paths: &[String], preloads: &[(String, String)]) -> Result<Lua> {
    let lua = Lua::new();
    register_embedded_modules(&lua)?;
    register_user_preloads(&lua, preloads)?;
    crate::helpers::register(&lua)?;

    if !search_paths.is_empty() {
        add_search_paths(&lua, search_paths)?;
    }

    Ok(lua)
}

/// Add filesystem search paths to `package.path`.
pub(crate) fn add_search_paths(lua: &Lua, paths: &[String]) -> LuaResult<()> {
    let pkg: LuaTable = lua.globals().get::<LuaTable>("package")?;
    let current: String = pkg.get::<String>("path")?;

    let mut prefix = String::new();
    for p in paths {
        prefix.push_str(p);
        prefix.push_str("/?.lua;");
        prefix.push_str(p);
        prefix.push_str("/?/init.lua;");
    }
    prefix.push_str(&current);
    pkg.set("path", prefix)?;
    Ok(())
}

/// Add the directory containing the app file to `package.path`.
pub(crate) fn add_app_dir_to_path(lua: &Lua, app_path: &Path) -> LuaResult<()> {
    if let Some(parent) = app_path.parent() {
        let dir = parent.to_string_lossy().to_string();
        add_search_paths(lua, &[dir])?;
    }
    Ok(())
}

/// Register embedded lua_sen modules into `package.preload`.
fn register_embedded_modules(lua: &Lua) -> LuaResult<()> {
    let modules: &[(&str, &str)] = &[
        ("lua_sen", LUA_SEN_INIT),
        ("lua_sen.cli", LUA_SEN_CLI),
        ("lua_sen.router", LUA_SEN_ROUTER),
        ("lua_sen.response", LUA_SEN_RESPONSE),
    ];
    preload_modules(lua, modules)
}

/// Load Lua source chunks into `package.preload`.
fn preload_modules(lua: &Lua, modules: &[(&str, &str)]) -> LuaResult<()> {
    let preload: LuaTable = lua
        .globals()
        .get::<LuaTable>("package")?
        .get::<LuaTable>("preload")?;

    for (name, source) in modules {
        let chunk = lua.load(*source).set_name(*name).into_function()?;
        preload.set(*name, chunk)?;
    }

    Ok(())
}

/// Register user-provided Lua source modules into `package.preload`.
fn register_user_preloads(lua: &Lua, preloads: &[(String, String)]) -> LuaResult<()> {
    if preloads.is_empty() {
        return Ok(());
    }

    let preload_table: LuaTable = lua
        .globals()
        .get::<LuaTable>("package")?
        .get::<LuaTable>("preload")?;

    for (name, source) in preloads {
        let chunk = lua
            .load(source.as_str())
            .set_name(name.as_str())
            .into_function()?;
        preload_table.set(name.as_str(), chunk)?;
    }

    Ok(())
}

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

    #[test]
    fn embedded_require_works() {
        let lua = create_lua(&[], &[]).unwrap();
        let result: LuaTable = lua.load(r#"return require("lua_sen")"#).eval().unwrap();
        // lua_sen exports `app`, `router`, `ok`, `err`, `silent`
        assert!(result.contains_key("app").unwrap());
        assert!(result.contains_key("ok").unwrap());
        assert!(result.contains_key("err").unwrap());
    }

    #[test]
    fn submodule_require_works() {
        let lua = create_lua(&[], &[]).unwrap();
        let result: LuaTable = lua.load(r#"return require("lua_sen.cli")"#).eval().unwrap();
        assert!(result.contains_key("app").unwrap());
    }

    #[test]
    fn search_paths_added() {
        let lua = create_lua(&["/tmp/test_lua".to_string()], &[]).unwrap();
        let pkg: LuaTable = lua.globals().get::<LuaTable>("package").unwrap();
        let path: String = pkg.get::<String>("path").unwrap();
        assert!(path.contains("/tmp/test_lua/?.lua"));
    }

    #[test]
    fn user_preloads_registered() {
        let preloads = vec![(
            "mymod".to_string(),
            "return { greeting = 'hello' }".to_string(),
        )];
        let lua = create_lua(&[], &preloads).unwrap();
        let result: LuaTable = lua.load(r#"return require("mymod")"#).eval().unwrap();
        let greeting: String = result.get("greeting").unwrap();
        assert_eq!(greeting, "hello");
    }

    #[test]
    fn user_preloads_hierarchical() {
        let preloads = vec![
            ("mypkg".to_string(), "return { name = 'mypkg' }".to_string()),
            (
                "mypkg.sub".to_string(),
                "return { name = 'mypkg.sub' }".to_string(),
            ),
        ];
        let lua = create_lua(&[], &preloads).unwrap();
        let root: LuaTable = lua.load(r#"return require("mypkg")"#).eval().unwrap();
        let sub: LuaTable = lua.load(r#"return require("mypkg.sub")"#).eval().unwrap();
        assert_eq!(root.get::<String>("name").unwrap(), "mypkg");
        assert_eq!(sub.get::<String>("name").unwrap(), "mypkg.sub");
    }
}
