use mlua::prelude::*;
use toml::Value as TomlValue;

pub fn create(lua: &Lua) -> LuaResult<LuaTable> {
    let t = lua.create_table()?;

    t.set(
        "parse",
        lua.create_function(|lua, s: String| {
            let value: TomlValue = s
                .parse()
                .map_err(|e: toml::de::Error| LuaError::external(e.to_string()))?;
            toml_to_lua(lua, &value)
        })?,
    )?;

    t.set(
        "encode",
        lua.create_function(|_, value: LuaValue| {
            let tv = lua_to_toml(&value)?;
            toml::to_string_pretty(&tv).map_err(|e| LuaError::external(e.to_string()))
        })?,
    )?;

    Ok(t)
}

const MAX_DEPTH: usize = 128;

fn toml_to_lua(lua: &Lua, value: &TomlValue) -> LuaResult<LuaValue> {
    toml_to_lua_inner(lua, value, 0)
}

fn toml_to_lua_inner(lua: &Lua, value: &TomlValue, depth: usize) -> LuaResult<LuaValue> {
    if depth > MAX_DEPTH {
        return Err(LuaError::external("TOML nesting too deep (limit: 128)"));
    }
    match value {
        TomlValue::String(s) => lua.create_string(s).map(LuaValue::String),
        TomlValue::Integer(i) => Ok(LuaValue::Integer(*i)),
        TomlValue::Float(f) => Ok(LuaValue::Number(*f)),
        TomlValue::Boolean(b) => Ok(LuaValue::Boolean(*b)),
        TomlValue::Datetime(dt) => lua.create_string(dt.to_string()).map(LuaValue::String),
        TomlValue::Array(arr) => {
            let table = lua.create_table()?;
            for (i, v) in arr.iter().enumerate() {
                table.set(i + 1, toml_to_lua_inner(lua, v, depth + 1)?)?;
            }
            Ok(LuaValue::Table(table))
        }
        TomlValue::Table(map) => {
            let table = lua.create_table()?;
            for (k, v) in map {
                table.set(k.as_str(), toml_to_lua_inner(lua, v, depth + 1)?)?;
            }
            Ok(LuaValue::Table(table))
        }
    }
}

fn lua_to_toml(value: &LuaValue) -> LuaResult<TomlValue> {
    lua_to_toml_inner(value, 0)
}

fn lua_to_toml_inner(value: &LuaValue, depth: usize) -> LuaResult<TomlValue> {
    use super::lua_table::{classify, TableKind};

    if depth > MAX_DEPTH {
        return Err(LuaError::external(
            "Lua table nesting too deep for TOML (limit: 128)",
        ));
    }
    match value {
        LuaValue::Boolean(b) => Ok(TomlValue::Boolean(*b)),
        LuaValue::Integer(i) => Ok(TomlValue::Integer(*i)),
        LuaValue::Number(n) => Ok(TomlValue::Float(*n)),
        LuaValue::String(s) => Ok(TomlValue::String(s.to_str()?.to_string())),
        LuaValue::Table(t) => match classify(t)? {
            TableKind::Array(len) => {
                let mut arr = Vec::with_capacity(len);
                for i in 1..=len {
                    let v: LuaValue = t.raw_get(i)?;
                    arr.push(lua_to_toml_inner(&v, depth + 1)?);
                }
                Ok(TomlValue::Array(arr))
            }
            TableKind::Map(pairs) => {
                let mut map = toml::map::Map::new();
                for (k, v) in pairs {
                    let key = match k {
                        LuaValue::String(s) => s.to_str()?.to_string(),
                        LuaValue::Integer(i) => i.to_string(),
                        _ => continue,
                    };
                    map.insert(key, lua_to_toml_inner(&v, depth + 1)?);
                }
                Ok(TomlValue::Table(map))
            }
        },
        _ => Err(LuaError::external(format!(
            "unsupported Lua type for TOML: {:?}",
            value.type_name()
        ))),
    }
}

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

    fn eval<T: mlua::FromLua>(code: &str) -> T {
        let lua = runtime::create_lua(&[], &[]).unwrap();
        lua.load(code).eval().unwrap()
    }

    #[test]
    fn parse_simple_table() {
        let s: String = eval(
            "local toml = require('senl.toml')\n\
             local t = toml.parse('name = \"test\"\\nvalue = 42\\n')\n\
             return t.name .. ',' .. tostring(t.value)",
        );
        assert_eq!(s, "test,42");
    }

    #[test]
    fn parse_nested_table() {
        let s: String = eval(
            "local toml = require('senl.toml')\n\
             local t = toml.parse('[server]\\nhost = \"localhost\"\\nport = 8080\\n')\n\
             return t.server.host .. ':' .. tostring(t.server.port)",
        );
        assert_eq!(s, "localhost:8080");
    }

    #[test]
    fn parse_array() {
        let n: i64 = eval(
            "local toml = require('senl.toml')\n\
             local t = toml.parse('items = [1, 2, 3]\\n')\n\
             return #t.items",
        );
        assert_eq!(n, 3);
    }

    #[test]
    fn encode_roundtrip() {
        let b: bool = eval(
            "local toml = require('senl.toml')\n\
             local t = toml.parse('key = \"value\"\\n')\n\
             local encoded = toml.encode(t)\n\
             local t2 = toml.parse(encoded)\n\
             return t2.key == 'value'",
        );
        assert!(b);
    }

    #[test]
    fn parse_invalid_returns_error() {
        let lua = runtime::create_lua(&[], &[]).unwrap();
        let result: mlua::Result<mlua::Value> = lua
            .load(r#"local toml = require("senl.toml"); return toml.parse("[invalid")"#)
            .eval();
        assert!(result.is_err());
    }

    #[test]
    fn encode_unsupported_type_returns_error() {
        let lua = runtime::create_lua(&[], &[]).unwrap();
        let result: mlua::Result<mlua::Value> = lua
            .load(
                r#"
                local toml = require("senl.toml")
                return toml.encode(print)
            "#,
            )
            .eval();
        assert!(result.is_err());
    }
}
