use mlua::prelude::*;
use serde_json::Value as JsonValue;

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

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

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

    t.set(
        "encode_pretty",
        lua.create_function(|_, value: LuaValue| {
            let json = lua_to_json(&value)?;
            serde_json::to_string_pretty(&json).map_err(|e| LuaError::external(e.to_string()))
        })?,
    )?;

    t.set(
        "read_file",
        lua.create_function(|lua, path: String| {
            let content = std::fs::read_to_string(&path).map_err(LuaError::external)?;
            let value: JsonValue =
                serde_json::from_str(&content).map_err(|e| LuaError::external(e.to_string()))?;
            json_to_lua(lua, &value)
        })?,
    )?;

    t.set(
        "write_file",
        lua.create_function(|_, (path, value): (String, LuaValue)| {
            let json = lua_to_json(&value)?;
            let content = serde_json::to_string_pretty(&json)
                .map_err(|e| LuaError::external(e.to_string()))?;
            std::fs::write(&path, content).map_err(LuaError::external)?;
            Ok(true)
        })?,
    )?;

    Ok(t)
}

const MAX_DEPTH: usize = 128;

fn json_to_lua(lua: &Lua, value: &JsonValue) -> LuaResult<LuaValue> {
    json_to_lua_inner(lua, value, 0)
}

fn json_to_lua_inner(lua: &Lua, value: &JsonValue, depth: usize) -> LuaResult<LuaValue> {
    if depth > MAX_DEPTH {
        return Err(LuaError::external("JSON nesting too deep (limit: 128)"));
    }
    match value {
        JsonValue::Null => Ok(LuaValue::Nil),
        JsonValue::Bool(b) => Ok(LuaValue::Boolean(*b)),
        JsonValue::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)
            }
        }
        JsonValue::String(s) => lua.create_string(s).map(LuaValue::String),
        JsonValue::Array(arr) => {
            let table = lua.create_table()?;
            for (i, v) in arr.iter().enumerate() {
                table.set(i + 1, json_to_lua_inner(lua, v, depth + 1)?)?;
            }
            Ok(LuaValue::Table(table))
        }
        JsonValue::Object(map) => {
            let table = lua.create_table()?;
            for (k, v) in map {
                table.set(k.as_str(), json_to_lua_inner(lua, v, depth + 1)?)?;
            }
            Ok(LuaValue::Table(table))
        }
    }
}

fn lua_to_json(value: &LuaValue) -> LuaResult<JsonValue> {
    lua_to_json_inner(value, 0)
}

fn lua_to_json_inner(value: &LuaValue, depth: usize) -> LuaResult<JsonValue> {
    if depth > MAX_DEPTH {
        return Err(LuaError::external(
            "Lua table nesting too deep for JSON (limit: 128)",
        ));
    }
    match value {
        LuaValue::Nil => Ok(JsonValue::Null),
        LuaValue::Boolean(b) => Ok(JsonValue::Bool(*b)),
        LuaValue::Integer(i) => Ok(JsonValue::Number((*i).into())),
        LuaValue::Number(n) => serde_json::Number::from_f64(*n)
            .map(JsonValue::Number)
            .ok_or_else(|| LuaError::external(format!("cannot convert {n} to JSON number"))),
        LuaValue::String(s) => Ok(JsonValue::String(s.to_str()?.to_string())),
        LuaValue::Table(t) => lua_table_to_json(t, depth),
        _ => Err(LuaError::external("unsupported type for JSON conversion")),
    }
}

fn lua_table_to_json(table: &LuaTable, depth: usize) -> LuaResult<JsonValue> {
    use super::lua_table::{classify, TableKind};

    match classify(table)? {
        TableKind::Array(len) => {
            let mut arr = Vec::with_capacity(len);
            for i in 1..=len {
                let v: LuaValue = table.raw_get(i)?;
                arr.push(lua_to_json_inner(&v, depth + 1)?);
            }
            Ok(JsonValue::Array(arr))
        }
        TableKind::Map(pairs) => {
            let mut map = serde_json::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(),
                    LuaValue::Number(n) => n.to_string(),
                    _ => continue,
                };
                map.insert(key, lua_to_json_inner(&v, depth + 1)?);
            }
            Ok(JsonValue::Object(map))
        }
    }
}

#[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_object() {
        let s: String = eval(
            r#"
            local json = require("senl.json")
            local t = json.parse('{"a":1,"b":"hello"}')
            return tostring(t.a) .. "," .. t.b
        "#,
        );
        assert_eq!(s, "1,hello");
    }

    #[test]
    fn parse_array() {
        let n: i64 = eval(
            r#"
            local json = require("senl.json")
            local arr = json.parse('[10,20,30]')
            return #arr
        "#,
        );
        assert_eq!(n, 3);
    }

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

    #[test]
    fn encode_roundtrip() {
        let s: String = eval(
            r#"
            local json = require("senl.json")
            local original = '{"name":"test","values":[1,2,3]}'
            local t = json.parse(original)
            local encoded = json.encode(t)
            local t2 = json.parse(encoded)
            return t2.name .. "," .. tostring(#t2.values)
        "#,
        );
        assert_eq!(s, "test,3");
    }

    #[test]
    fn encode_empty_table_as_object() {
        let s: String = eval(
            r#"
            local json = require("senl.json")
            return json.encode({})
        "#,
        );
        assert_eq!(s, "{}");
    }

    #[test]
    fn encode_nested_structure() {
        let s: String = eval(
            r#"
            local json = require("senl.json")
            return json.encode({items = {1, 2}, meta = {ok = true}})
        "#,
        );
        let v: serde_json::Value = serde_json::from_str(&s).unwrap();
        assert!(v["items"].is_array());
        assert_eq!(v["meta"]["ok"], true);
    }

    #[test]
    fn encode_pretty_has_newlines() {
        let s: String = eval(
            r#"
            local json = require("senl.json")
            return json.encode_pretty({a = 1})
        "#,
        );
        assert!(s.contains('\n'));
    }

    #[test]
    fn parse_null_becomes_nil() {
        let b: bool = eval(
            r#"
            local json = require("senl.json")
            local t = json.parse('{"x":null}')
            return t.x == nil
        "#,
        );
        assert!(b);
    }

    #[test]
    fn parse_boolean() {
        let s: String = eval(
            r#"
            local json = require("senl.json")
            local t = json.parse('{"flag":true}')
            return type(t.flag)
        "#,
        );
        assert_eq!(s, "boolean");
    }
}
