use flate2::write::DeflateEncoder;
use flate2::Compression;
use serde_json::Value;
use std::io::Write;
use std::path::Path;

fn main() {
    println!("cargo:rerun-if-changed=data");

    let manifest_dir = std::env::var("CARGO_MANIFEST_DIR").unwrap();
    let out_dir = std::env::var("OUT_DIR").unwrap();
    let data_dir = Path::new(&manifest_dir).join("data");

    if !data_dir.exists() {
        panic!("data/ directory not found. It should contain extension_api JSON files named by version (e.g., 4.5.1.json).");
    }

    // Collect all .json files in data/, extract version, strip, compress
    let mut versions: Vec<(String, String)> = Vec::new(); // (version, output_filename)

    let mut entries: Vec<_> = std::fs::read_dir(&data_dir)
        .expect("Failed to read data/ directory")
        .filter_map(|e| e.ok())
        .filter(|e| e.path().extension().is_some_and(|ext| ext == "json"))
        .collect();
    entries.sort_by_key(|e| e.file_name());

    if entries.is_empty() {
        panic!("No .json files found in data/. Add extension_api JSON files named by version (e.g., 4.5.1.json).");
    }

    for entry in &entries {
        let path = entry.path();
        let stem = path.file_stem().unwrap().to_string_lossy().to_string();

        let contents = std::fs::read_to_string(&path)
            .unwrap_or_else(|e| panic!("Failed to read {}: {}", path.display(), e));
        let api: Value = serde_json::from_str(&contents)
            .unwrap_or_else(|e| panic!("Failed to parse {}: {}", path.display(), e));

        // Use the filename stem as the version identifier
        let version = stem.clone();

        let stripped = strip_api(&api);
        let json_bytes = serde_json::to_vec(&stripped).expect("Failed to serialize stripped API");

        let mut encoder = DeflateEncoder::new(Vec::new(), Compression::best());
        encoder.write_all(&json_bytes).unwrap();
        let compressed = encoder.finish().unwrap();

        let out_filename = format!("classdb_{}.deflate", version);
        let out_path = Path::new(&out_dir).join(&out_filename);
        std::fs::write(&out_path, &compressed)
            .unwrap_or_else(|e| panic!("Failed to write {}: {}", out_path.display(), e));

        eprintln!(
            "ClassDB v{}: {}B -> {}B stripped -> {}B compressed",
            version,
            contents.len(),
            json_bytes.len(),
            compressed.len()
        );

        versions.push((version, out_filename));
    }

    // Generate classdb_registry.rs with embedded data for all versions
    let mut registry_code = String::new();
    registry_code.push_str("/// Auto-generated by build.rs — do not edit.\n");

    // Generate individual static byte arrays
    for (version, filename) in &versions {
        let ident = version.replace('.', "_");
        registry_code.push_str(&format!(
            "static DATA_{}: &[u8] = include_bytes!(concat!(env!(\"OUT_DIR\"), \"/{}\"));\n",
            ident, filename
        ));
    }

    // Generate the VERSIONS array
    registry_code
        .push_str("\n/// All bundled ClassDB versions: (version_string, compressed_data).\n");
    registry_code.push_str("pub static VERSIONS: &[(&str, &[u8])] = &[\n");
    for (version, _) in &versions {
        let ident = version.replace('.', "_");
        registry_code.push_str(&format!("    (\"{}\", DATA_{}),\n", version, ident));
    }
    registry_code.push_str("];\n");

    let registry_path = Path::new(&out_dir).join("classdb_registry.rs");
    std::fs::write(&registry_path, &registry_code).expect("Failed to write classdb_registry.rs");
}

fn strip_api(api: &Value) -> Value {
    let mut result = serde_json::Map::new();

    if let Some(classes) = api.get("classes").and_then(|v| v.as_array()) {
        let stripped_classes: Vec<Value> = classes.iter().map(strip_class).collect();
        result.insert("classes".into(), Value::Array(stripped_classes));
    }

    if let Some(builtins) = api.get("builtin_classes").and_then(|v| v.as_array()) {
        let stripped: Vec<Value> = builtins.iter().map(strip_builtin_class).collect();
        result.insert("builtin_classes".into(), Value::Array(stripped));
    }

    if let Some(funcs) = api.get("utility_functions").and_then(|v| v.as_array()) {
        let stripped: Vec<Value> = funcs.iter().map(strip_utility_function).collect();
        result.insert("utility_functions".into(), Value::Array(stripped));
    }

    if let Some(enums) = api.get("global_enums").and_then(|v| v.as_array()) {
        let stripped: Vec<Value> = enums.iter().map(strip_enum).collect();
        result.insert("global_enums".into(), Value::Array(stripped));
    }

    if let Some(singletons) = api.get("singletons").and_then(|v| v.as_array()) {
        let stripped: Vec<Value> = singletons
            .iter()
            .map(|s| {
                let mut m = serde_json::Map::new();
                if let Some(name) = s.get("name") {
                    m.insert("name".into(), name.clone());
                }
                if let Some(ty) = s.get("type") {
                    m.insert("type".into(), ty.clone());
                }
                Value::Object(m)
            })
            .collect();
        result.insert("singletons".into(), Value::Array(stripped));
    }

    Value::Object(result)
}

fn strip_class(cls: &Value) -> Value {
    let mut c = serde_json::Map::new();

    c.insert(
        "name".into(),
        cls.get("name")
            .cloned()
            .unwrap_or(Value::String(String::new())),
    );
    c.insert(
        "inherits".into(),
        cls.get("inherits")
            .cloned()
            .unwrap_or(Value::String(String::new())),
    );

    if let Some(methods) = cls.get("methods").and_then(|v| v.as_array()) {
        let stripped: Vec<Value> = methods.iter().map(strip_method).collect();
        c.insert("methods".into(), Value::Array(stripped));
    }

    if let Some(props) = cls.get("properties").and_then(|v| v.as_array()) {
        let stripped: Vec<Value> = props
            .iter()
            .map(|p| {
                let mut m = serde_json::Map::new();
                if let Some(name) = p.get("name") {
                    m.insert("name".into(), name.clone());
                }
                if let Some(ty) = p.get("type") {
                    m.insert("type".into(), ty.clone());
                }
                Value::Object(m)
            })
            .collect();
        c.insert("properties".into(), Value::Array(stripped));
    }

    if let Some(signals) = cls.get("signals").and_then(|v| v.as_array()) {
        let stripped: Vec<Value> = signals
            .iter()
            .map(|s| {
                let mut m = serde_json::Map::new();
                if let Some(name) = s.get("name") {
                    m.insert("name".into(), name.clone());
                }
                if let Some(args) = s.get("arguments").and_then(|v| v.as_array()) {
                    let stripped_args: Vec<Value> = args.iter().map(strip_argument).collect();
                    m.insert("arguments".into(), Value::Array(stripped_args));
                }
                Value::Object(m)
            })
            .collect();
        c.insert("signals".into(), Value::Array(stripped));
    }

    if let Some(constants) = cls.get("constants").and_then(|v| v.as_array()) {
        let stripped: Vec<Value> = constants
            .iter()
            .map(|k| {
                let mut m = serde_json::Map::new();
                if let Some(name) = k.get("name") {
                    m.insert("name".into(), name.clone());
                }
                if let Some(val) = k.get("value") {
                    m.insert("value".into(), val.clone());
                }
                Value::Object(m)
            })
            .collect();
        c.insert("constants".into(), Value::Array(stripped));
    }

    if let Some(enums) = cls.get("enums").and_then(|v| v.as_array()) {
        let stripped: Vec<Value> = enums.iter().map(strip_enum).collect();
        c.insert("enums".into(), Value::Array(stripped));
    }

    Value::Object(c)
}

fn strip_builtin_class(bc: &Value) -> Value {
    let mut b = serde_json::Map::new();

    b.insert(
        "name".into(),
        bc.get("name")
            .cloned()
            .unwrap_or(Value::String(String::new())),
    );

    if let Some(methods) = bc.get("methods").and_then(|v| v.as_array()) {
        let stripped: Vec<Value> = methods
            .iter()
            .map(|m| {
                let mut method = serde_json::Map::new();
                if let Some(name) = m.get("name") {
                    method.insert("name".into(), name.clone());
                }
                if let Some(is_static) = m.get("is_static") {
                    method.insert("is_static".into(), is_static.clone());
                }
                if let Some(rt) = m.get("return_type") {
                    method.insert("return_type".into(), rt.clone());
                }
                if let Some(args) = m.get("arguments").and_then(|v| v.as_array()) {
                    let stripped_args: Vec<Value> = args.iter().map(strip_argument).collect();
                    method.insert("arguments".into(), Value::Array(stripped_args));
                }
                Value::Object(method)
            })
            .collect();
        b.insert("methods".into(), Value::Array(stripped));
    }

    // Preserve operators for type inference
    if let Some(operators) = bc.get("operators").and_then(|v| v.as_array()) {
        let stripped: Vec<Value> = operators
            .iter()
            .map(|op| {
                let mut o = serde_json::Map::new();
                if let Some(name) = op.get("name") {
                    o.insert("name".into(), name.clone());
                }
                if let Some(right_type) = op.get("right_type") {
                    o.insert("right_type".into(), right_type.clone());
                }
                if let Some(return_type) = op.get("return_type") {
                    o.insert("return_type".into(), return_type.clone());
                }
                Value::Object(o)
            })
            .collect();
        b.insert("operators".into(), Value::Array(stripped));
    }

    Value::Object(b)
}

fn strip_method(m: &Value) -> Value {
    let mut method = serde_json::Map::new();

    if let Some(name) = m.get("name") {
        method.insert("name".into(), name.clone());
    }
    if let Some(is_static) = m.get("is_static") {
        method.insert("is_static".into(), is_static.clone());
    }
    if let Some(is_virtual) = m.get("is_virtual") {
        method.insert("is_virtual".into(), is_virtual.clone());
    }

    if let Some(rv) = m.get("return_value") {
        if let Some(ty) = rv.get("type") {
            method.insert("return_type".into(), ty.clone());
        }
    }

    if let Some(args) = m.get("arguments").and_then(|v| v.as_array()) {
        let stripped_args: Vec<Value> = args.iter().map(strip_argument).collect();
        method.insert("arguments".into(), Value::Array(stripped_args));
    }

    Value::Object(method)
}

fn strip_utility_function(uf: &Value) -> Value {
    let mut u = serde_json::Map::new();

    if let Some(name) = uf.get("name") {
        u.insert("name".into(), name.clone());
    }
    if let Some(is_vararg) = uf.get("is_vararg") {
        u.insert("is_vararg".into(), is_vararg.clone());
    }
    if let Some(rt) = uf.get("return_type") {
        u.insert("return_type".into(), rt.clone());
    }
    if let Some(args) = uf.get("arguments").and_then(|v| v.as_array()) {
        let stripped_args: Vec<Value> = args.iter().map(strip_argument).collect();
        u.insert("arguments".into(), Value::Array(stripped_args));
    }

    Value::Object(u)
}

fn strip_enum(e: &Value) -> Value {
    let mut en = serde_json::Map::new();

    if let Some(name) = e.get("name") {
        en.insert("name".into(), name.clone());
    }
    if let Some(values) = e.get("values").and_then(|v| v.as_array()) {
        let stripped: Vec<Value> = values
            .iter()
            .map(|v| {
                let mut val = serde_json::Map::new();
                if let Some(name) = v.get("name") {
                    val.insert("name".into(), name.clone());
                }
                if let Some(value) = v.get("value") {
                    val.insert("value".into(), value.clone());
                }
                Value::Object(val)
            })
            .collect();
        en.insert("values".into(), Value::Array(stripped));
    }

    Value::Object(en)
}

fn strip_argument(arg: &Value) -> Value {
    let mut a = serde_json::Map::new();
    if let Some(name) = arg.get("name") {
        a.insert("name".into(), name.clone());
    }
    if let Some(ty) = arg.get("type") {
        a.insert("type".into(), ty.clone());
    }
    Value::Object(a)
}
