use mlua::prelude::*;
use serde_yaml_ng::Value as YamlValue;

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

    t.set(
        "parse",
        lua.create_function(|lua, s: String| {
            let value: YamlValue =
                serde_yaml_ng::from_str(&s).map_err(|e| LuaError::external(e.to_string()))?;
            yaml_to_lua(lua, &value)
        })?,
    )?;

    t.set(
        "encode",
        lua.create_function(|_, value: LuaValue| {
            let yaml = lua_to_yaml(&value)?;
            serde_yaml_ng::to_string(&yaml).map_err(|e| LuaError::external(e.to_string()))
        })?,
    )?;

    Ok(t)
}

const MAX_DEPTH: usize = 128;

fn yaml_to_lua(lua: &Lua, value: &YamlValue) -> LuaResult<LuaValue> {
    yaml_to_lua_inner(lua, value, 0)
}

fn yaml_to_lua_inner(lua: &Lua, value: &YamlValue, depth: usize) -> LuaResult<LuaValue> {
    if depth > MAX_DEPTH {
        return Err(LuaError::external("YAML nesting too deep (limit: 128)"));
    }
    match value {
        YamlValue::Null => Ok(LuaValue::Nil),
        YamlValue::Bool(b) => Ok(LuaValue::Boolean(*b)),
        YamlValue::Number(n) => {
            if let Some(i) = n.as_i64() {
                Ok(LuaValue::Integer(i))
            } else if let Some(f) = n.as_f64() {
                Ok(LuaValue::Number(f))
            } else {
                Ok(LuaValue::Nil)
            }
        }
        YamlValue::String(s) => lua.create_string(s).map(LuaValue::String),
        YamlValue::Sequence(seq) => {
            let table = lua.create_table()?;
            for (i, v) in seq.iter().enumerate() {
                table.set(i + 1, yaml_to_lua_inner(lua, v, depth + 1)?)?;
            }
            Ok(LuaValue::Table(table))
        }
        YamlValue::Mapping(map) => {
            let table = lua.create_table()?;
            for (k, v) in map {
                let key = match k {
                    YamlValue::String(s) => s.clone(),
                    YamlValue::Number(n) => n.to_string(),
                    YamlValue::Bool(b) => b.to_string(),
                    _ => continue,
                };
                table.set(key, yaml_to_lua_inner(lua, v, depth + 1)?)?;
            }
            Ok(LuaValue::Table(table))
        }
        YamlValue::Tagged(tagged) => yaml_to_lua_inner(lua, &tagged.value, depth),
    }
}

fn lua_to_yaml(value: &LuaValue) -> LuaResult<YamlValue> {
    lua_to_yaml_inner(value, 0)
}

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

    if depth > MAX_DEPTH {
        return Err(LuaError::external(
            "Lua table nesting too deep for YAML (limit: 128)",
        ));
    }
    match value {
        LuaValue::Nil => Ok(YamlValue::Null),
        LuaValue::Boolean(b) => Ok(YamlValue::Bool(*b)),
        LuaValue::Integer(i) => Ok(YamlValue::Number(serde_yaml_ng::Number::from(*i))),
        LuaValue::Number(n) => Ok(YamlValue::Number(serde_yaml_ng::Number::from(*n))),
        LuaValue::String(s) => Ok(YamlValue::String(s.to_str()?.to_string())),
        LuaValue::Table(t) => match classify(t)? {
            TableKind::Array(len) => {
                let mut seq = Vec::with_capacity(len);
                for i in 1..=len {
                    let v: LuaValue = t.raw_get(i)?;
                    seq.push(lua_to_yaml_inner(&v, depth + 1)?);
                }
                Ok(YamlValue::Sequence(seq))
            }
            TableKind::Map(pairs) => {
                let mut map = serde_yaml_ng::Mapping::new();
                for (k, v) in pairs {
                    let key = match &k {
                        LuaValue::String(s) => YamlValue::String(s.to_str()?.to_string()),
                        LuaValue::Integer(i) => YamlValue::Number(serde_yaml_ng::Number::from(*i)),
                        _ => continue,
                    };
                    map.insert(key, lua_to_yaml_inner(&v, depth + 1)?);
                }
                Ok(YamlValue::Mapping(map))
            }
        },
        _ => Err(LuaError::external(format!(
            "unsupported Lua type for YAML: {:?}",
            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_mapping() {
        let s: String = eval(
            r#"
            local yaml = require("senl.yaml")
            local t = yaml.parse("name: test\nvalue: 42\n")
            return t.name .. "," .. tostring(t.value)
        "#,
        );
        assert_eq!(s, "test,42");
    }

    #[test]
    fn parse_sequence() {
        let n: i64 = eval(
            r#"
            local yaml = require("senl.yaml")
            local t = yaml.parse("- 1\n- 2\n- 3\n")
            return #t
        "#,
        );
        assert_eq!(n, 3);
    }

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

    #[test]
    fn parse_boolean_and_null() {
        let s: String = eval(
            r#"
            local yaml = require("senl.yaml")
            local t = yaml.parse("flag: true\nnone: null\n")
            local flag_ok = type(t.flag) == "boolean" and t.flag == true
            local none_ok = t.none == nil
            return tostring(flag_ok) .. "," .. tostring(none_ok)
        "#,
        );
        assert_eq!(s, "true,true");
    }

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