use crate::mir::definitions::call_unified::Callee;
use serde_json::json;

/// Emit MIR JSON for Python harness/PyVM.
/// The JSON schema matches tools/llvmlite_harness.py expectations and is
/// intentionally minimal for initial scaffolding.
///
/// Phase 15.5: Supports both v0 (legacy separate ops) and v1 (unified mir_call) formats

/// Helper: Create JSON v1 root with schema information
/// Includes version, capabilities, metadata for advanced MIR features
fn create_json_v1_root(functions: serde_json::Value) -> serde_json::Value {
    json!({
        "schema_version": "1.0",
        "capabilities": [
            "unified_call",      // Phase 15.5: Unified MirCall support
            "phi",              // SSA Phi functions
            "effects",          // Effect tracking for optimization
            "callee_typing"     // Type-safe call target resolution
        ],
        "metadata": {
            "generator": "nyash-rust",
            "phase": "15.5",
            "build_time": "Phase 15.5 Development",
            "features": ["mir_call_unification", "json_v1_schema"]
        },
        "functions": functions
    })
}

/// Helper: detect residual numeric-core boxcalls that should have been lowered by AotPrepNumericCoreBox.
/// Currently we only check for `boxcall` with `method:"mul_naive"` which should become
/// `call("NyNumericMatI64.mul_naive", ...)` when NYASH_AOT_NUMERIC_CORE=1 is effective.
#[allow(dead_code)]
fn has_numeric_core_boxcall(root: &serde_json::Value) -> bool {
    let funs = match root.get("functions") {
        Some(v) => v.as_array().cloned().unwrap_or_default(),
        None => return false,
    };
    for f in funs {
        let blocks = match f.get("blocks").and_then(|b| b.as_array()) {
            Some(b) => b,
            None => continue,
        };
        for b in blocks {
            let insts = match b.get("instructions").and_then(|i| i.as_array()) {
                Some(i) => i,
                None => continue,
            };
            for inst in insts {
                let op = inst.get("op").and_then(|v| v.as_str());
                let method = inst.get("method").and_then(|v| v.as_str());
                if op == Some("boxcall") && method == Some("mul_naive") {
                    return true;
                }
            }
        }
    }
    false
}

/// Helper: enforce numeric_core invariants when NYASH_AOT_NUMERIC_CORE=1 is set.
/// - Default: emit a warning if mul_naive boxcalls are still present.
/// - Strict: if NYASH_AOT_NUMERIC_CORE_STRICT=1, return Err to fail fast.
#[allow(dead_code)]
fn check_numeric_core_invariants(root: &serde_json::Value) -> Result<(), String> {
    let numeric_on = matches!(
        std::env::var("NYASH_AOT_NUMERIC_CORE").ok().as_deref(),
        Some("1")
    );
    if !numeric_on {
        return Ok(());
    }

    if !has_numeric_core_boxcall(root) {
        return Ok(());
    }

    let strict = matches!(
        std::env::var("NYASH_AOT_NUMERIC_CORE_STRICT")
            .ok()
            .as_deref(),
        Some("1")
    );

    eprintln!(
        "[mir_json/numeric_core] NYASH_AOT_NUMERIC_CORE=1 but MIR JSON still contains boxcall(\"mul_naive\"). \
AotPrepNumericCoreBox may not have run or did not match; inspect AotPrep logs or run tools/hakorune_emit_mir.sh with HAKO_SELFHOST_TRACE=1."
    );

    if strict {
        return Err(
            "NYASH_AOT_NUMERIC_CORE_STRICT=1: numeric_core invariants violated (mul_naive boxcall remains)"
                .to_string(),
        );
    }
    Ok(())
}

/// Helper: Emit unified mir_call JSON (v1 format)
/// Supports all 6 Callee types in a single unified JSON structure
fn emit_unified_mir_call(
    dst: Option<u32>,
    callee: &Callee,
    args: &[u32],
    effects: &[&str],
) -> serde_json::Value {
    let mut call_obj = json!({
        "op": "mir_call",
        "dst": dst,
        "mir_call": {
            "args": args,
            "effects": effects,
            "flags": {}
        }
    });

    // Generate Callee-specific mir_call structure
    match callee {
        Callee::Global(name) => {
            call_obj["mir_call"]["callee"] = json!({
                "type": "Global",
                "name": name
            });
        }
        Callee::Method {
            box_name,
            method,
            receiver,
            certainty,
            ..
        } => {
            call_obj["mir_call"]["callee"] = json!({
                "type": "Method",
                "box_name": box_name,
                "method": method,
                "receiver": receiver.map(|v| v.as_u32()),
                "certainty": match certainty { crate::mir::definitions::call_unified::TypeCertainty::Known => "Known", crate::mir::definitions::call_unified::TypeCertainty::Union => "Union" }
            });
        }
        Callee::Constructor { box_type } => {
            call_obj["mir_call"]["callee"] = json!({
                "type": "Constructor",
                "box_type": box_type
            });
        }
        Callee::Closure {
            params,
            captures,
            me_capture,
        } => {
            let captures_json: Vec<_> = captures
                .iter()
                .map(|(name, vid)| json!([name, vid.as_u32()]))
                .collect();
            call_obj["mir_call"]["callee"] = json!({
                "type": "Closure",
                "params": params,
                "captures": captures_json,
                "me_capture": me_capture.map(|v| v.as_u32())
            });
        }
        Callee::Value(vid) => {
            call_obj["mir_call"]["callee"] = json!({
                "type": "Value",
                "function_value": vid.as_u32()
            });
        }
        Callee::Extern(name) => {
            call_obj["mir_call"]["callee"] = json!({
                "type": "Extern",
                "name": name
            });
        }
    }

    call_obj
}

pub fn emit_mir_json_for_harness(
    module: &nyash_rust::mir::MirModule,
    path: &std::path::Path,
) -> Result<(), String> {
    use nyash_rust::mir::{BinaryOp as B, CompareOp as C, MirInstruction as I, MirType};
    let mut funs = Vec::new();
    for (name, f) in &module.functions {
        let mut blocks = Vec::new();
        let mut ids: Vec<_> = f.blocks.keys().copied().collect();
        ids.sort();
        for bid in ids {
            if let Some(bb) = f.blocks.get(&bid) {
                let mut insts = Vec::new();
                // Phase 131-13: Emit all instructions in MIR order (SSOT principle)
                // No reordering except PHI consolidation at block start (LLVM constraint)

                // Step 1: Emit all PHI instructions first (LLVM requirement)
                for inst in &bb.instructions {
                    if let I::Phi { dst, inputs, .. } = inst {
                        let incoming: Vec<_> = inputs
                            .iter()
                            .map(|(b, v)| json!([v.as_u32(), b.as_u32()]))
                            .collect();
                        // Phase 131-11-F: Add dst_type hint from metadata for all PHI instructions
                        let mut phi_inst =
                            json!({"op":"phi","dst": dst.as_u32(), "incoming": incoming});
                        if let Some(dst_type) = f.metadata.value_types.get(dst) {
                            let type_json = match dst_type {
                                MirType::Integer => json!("i64"),
                                MirType::String => json!({"kind": "string"}),
                                MirType::Box(bt) => json!({"kind": "handle", "box_type": bt}),
                                MirType::Bool => json!("i1"),
                                MirType::Void => json!("void"),
                                _ => json!(null),
                            };
                            if !type_json.is_null() {
                                phi_inst["dst_type"] = type_json;
                            }
                        }
                        insts.push(phi_inst);
                    }
                }

                // Step 2: Emit all non-PHI instructions in MIR order (no reordering!)
                for inst in &bb.instructions {
                    match inst {
                        I::Phi { .. } => {
                            // Already emitted in step 1
                            continue;
                        }
                        I::Copy { dst, src } => {
                            insts.push(
                                json!({"op":"copy","dst": dst.as_u32(), "src": src.as_u32()}),
                            );
                        }
                        I::UnaryOp { dst, op, operand } => {
                            let kind = match op {
                                nyash_rust::mir::UnaryOp::Neg => "neg",
                                nyash_rust::mir::UnaryOp::Not => "not",
                                nyash_rust::mir::UnaryOp::BitNot => "bitnot",
                            };
                            insts.push(json!({"op":"unop","kind": kind, "src": operand.as_u32(), "dst": dst.as_u32()}));
                        }
                        I::Const { dst, value } => {
                            match value {
                                nyash_rust::mir::ConstValue::Integer(i) => {
                                    insts.push(json!({"op":"const","dst": dst.as_u32(), "value": {"type": "i64", "value": i}}));
                                }
                                nyash_rust::mir::ConstValue::Float(fv) => {
                                    insts.push(json!({"op":"const","dst": dst.as_u32(), "value": {"type": "f64", "value": fv}}));
                                }
                                nyash_rust::mir::ConstValue::Bool(b) => {
                                    insts.push(json!({"op":"const","dst": dst.as_u32(), "value": {"type": "i64", "value": if *b {1} else {0}}}));
                                }
                                nyash_rust::mir::ConstValue::String(s) => {
                                    // String constants are exported as StringBox handle by default
                                    insts.push(json!({
                                        "op":"const",
                                        "dst": dst.as_u32(),
                                        "value": {
                                            "type": {"kind":"handle","box_type":"StringBox"},
                                            "value": s
                                        }
                                    }));
                                }
                                nyash_rust::mir::ConstValue::Null
                                | nyash_rust::mir::ConstValue::Void => {
                                    insts.push(json!({"op":"const","dst": dst.as_u32(), "value": {"type": "void", "value": 0}}));
                                }
                            }
                        }
                        I::TypeOp { dst, op, value, ty } => {
                            let op_s = match op {
                                nyash_rust::mir::TypeOpKind::Check => "check",
                                nyash_rust::mir::TypeOpKind::Cast => "cast",
                            };
                            let ty_s = match ty {
                                MirType::Integer => "Integer".to_string(),
                                MirType::Float => "Float".to_string(),
                                MirType::Bool => "Bool".to_string(),
                                MirType::String => "String".to_string(),
                                MirType::Void => "Void".to_string(),
                                MirType::Box(name) => name.clone(),
                                _ => "Unknown".to_string(),
                            };
                            insts.push(json!({
                                "op":"typeop",
                                "operation": op_s,
                                "src": value.as_u32(),
                                "dst": dst.as_u32(),
                                "target_type": ty_s,
                            }));
                        }
                        I::BinOp { dst, op, lhs, rhs } => {
                            let op_s = match op {
                                B::Add => "+",
                                B::Sub => "-",
                                B::Mul => "*",
                                B::Div => "/",
                                B::Mod => "%",
                                B::BitAnd => "&",
                                B::BitOr => "|",
                                B::BitXor => "^",
                                B::Shl => "<<",
                                B::Shr => ">>",
                                B::And => "&",
                                B::Or => "|",
                            };
                            let mut obj = json!({"op":"binop","operation": op_s, "lhs": lhs.as_u32(), "rhs": rhs.as_u32(), "dst": dst.as_u32()});
                            // Phase 131-15-P1: dst_type only when type is KNOWN (not Unknown)
                            // Operand TypeFacts take priority over dst_type hint in Python
                            if matches!(op, B::Add) {
                                let dst_type = f.metadata.value_types.get(dst);
                                match dst_type {
                                    Some(MirType::Box(bt)) if bt == "StringBox" => {
                                        obj["dst_type"] =
                                            json!({"kind":"handle","box_type":"StringBox"});
                                    }
                                    Some(MirType::Integer) => {
                                        // Explicitly mark as i64 for integer addition
                                        obj["dst_type"] = json!("i64");
                                    }
                                    Some(MirType::Unknown) | None => {
                                        // Unknown: DO NOT emit dst_type
                                        // Let Python side infer from operand TypeFacts
                                    }
                                    _ => {
                                        // Other known types: use conservative i64
                                        obj["dst_type"] = json!("i64");
                                    }
                                }
                            }
                            insts.push(obj);
                        }
                        I::Compare { dst, op, lhs, rhs } => {
                            let op_s = match op {
                                C::Lt => "<",
                                C::Le => "<=",
                                C::Gt => ">",
                                C::Ge => ">=",
                                C::Eq => "==",
                                C::Ne => "!=",
                            };
                            let mut obj = json!({"op":"compare","operation": op_s, "lhs": lhs.as_u32(), "rhs": rhs.as_u32(), "dst": dst.as_u32()});
                            // cmp_kind hint for string equality
                            if matches!(op, C::Eq | C::Ne) {
                                let lhs_is_str = match f.metadata.value_types.get(lhs) {
                                    Some(MirType::String) => true,
                                    Some(MirType::Box(bt)) if bt == "StringBox" => true,
                                    _ => false,
                                };
                                let rhs_is_str = match f.metadata.value_types.get(rhs) {
                                    Some(MirType::String) => true,
                                    Some(MirType::Box(bt)) if bt == "StringBox" => true,
                                    _ => false,
                                };
                                if lhs_is_str && rhs_is_str {
                                    obj["cmp_kind"] = json!("string");
                                }
                            }
                            insts.push(obj);
                        }
                        I::Call {
                            dst,
                            func,
                            callee,
                            args,
                            effects,
                            ..
                        } => {
                            // Phase 15.5: Unified Call support with environment variable control
                            let use_unified = match std::env::var("NYASH_MIR_UNIFIED_CALL")
                                .ok()
                                .as_deref()
                                .map(|s| s.to_ascii_lowercase())
                            {
                                Some(s) if s == "0" || s == "false" || s == "off" => false,
                                _ => true,
                            };

                            if use_unified && callee.is_some() {
                                // v1: Unified mir_call format
                                let effects_str: Vec<&str> =
                                    if effects.is_io() { vec!["IO"] } else { vec![] };
                                let args_u32: Vec<u32> = args.iter().map(|v| v.as_u32()).collect();
                                let unified_call = emit_unified_mir_call(
                                    dst.map(|v| v.as_u32()),
                                    callee.as_ref().unwrap(),
                                    &args_u32,
                                    &effects_str,
                                );
                                insts.push(unified_call);
                            } else if !use_unified && callee.is_some() {
                                // v0: When unified is OFF but callee exists, emit proper v0 format
                                use nyash_rust::mir::definitions::Callee;
                                match callee.as_ref().unwrap() {
                                    Callee::Method {
                                        method, receiver, ..
                                    } => {
                                        // Emit as boxcall for compatibility
                                        let box_val = receiver.unwrap_or(*func);
                                        let args_a: Vec<_> =
                                            args.iter().map(|v| json!(v.as_u32())).collect();
                                        let mut obj = json!({
                                            "op":"boxcall",
                                            "box": box_val.as_u32(),
                                            "method": method,
                                            "args": args_a,
                                            "dst": dst.map(|d| d.as_u32())
                                        });
                                        // Add dst_type hints for known methods
                                        let m = method.as_str();
                                        let dst_ty = if m == "substring"
                                            || m == "dirname"
                                            || m == "join"
                                            || m == "read_all"
                                            || m == "read"
                                        {
                                            Some(json!({"kind":"handle","box_type":"StringBox"}))
                                        } else if m == "length" || m == "lastIndexOf" {
                                            Some(json!("i64"))
                                        } else {
                                            None
                                        };
                                        if let Some(t) = dst_ty {
                                            obj["dst_type"] = t;
                                        }
                                        insts.push(obj);
                                        if let Some(d) = dst.map(|v| v.as_u32()) {}
                                    }
                                    _ => {
                                        // Other callee types: emit generic call
                                        let args_a: Vec<_> =
                                            args.iter().map(|v| json!(v.as_u32())).collect();
                                        insts.push(json!({"op":"call","func": func.as_u32(), "args": args_a, "dst": dst.map(|d| d.as_u32())}));
                                    }
                                }
                            } else {
                                // v0: Legacy call format (no callee info)
                                let args_a: Vec<_> =
                                    args.iter().map(|v| json!(v.as_u32())).collect();
                                insts.push(json!({"op":"call","func": func.as_u32(), "args": args_a, "dst": dst.map(|d| d.as_u32())}));
                            }
                        }
                        I::ExternCall {
                            dst,
                            iface_name,
                            method_name,
                            args,
                            ..
                        } => {
                            let args_a: Vec<_> = args.iter().map(|v| json!(v.as_u32())).collect();
                            let func_name = if iface_name == "env.console" {
                                format!("nyash.console.{}", method_name)
                            } else {
                                format!("{}.{}", iface_name, method_name)
                            };
                            let mut obj = json!({
                                "op": "externcall",
                                "func": func_name,
                                "args": args_a,
                                "dst": dst.map(|d| d.as_u32()),
                            });
                            // Minimal dst_type hints for known externs
                            if iface_name == "env.console" {
                                // console.* returns i64 status (ignored by user code)
                                if dst.is_some() {
                                    obj["dst_type"] = json!("i64");
                                }
                            }
                            insts.push(obj);
                        }
                        I::BoxCall {
                            dst,
                            box_val,
                            method,
                            args,
                            ..
                        } => {
                            let args_a: Vec<_> = args.iter().map(|v| json!(v.as_u32())).collect();
                            // Minimal dst_type hints
                            let mut obj = json!({
                                "op":"boxcall","box": box_val.as_u32(), "method": method, "args": args_a, "dst": dst.map(|d| d.as_u32())
                            });
                            let m = method.as_str();
                            let dst_ty = if m == "substring"
                                || m == "dirname"
                                || m == "join"
                                || m == "read_all"
                                || m == "read"
                            {
                                Some(json!({"kind":"handle","box_type":"StringBox"}))
                            } else if m == "length" || m == "lastIndexOf" {
                                Some(json!("i64"))
                            } else {
                                None
                            };
                            if let Some(t) = dst_ty {
                                obj["dst_type"] = t;
                            }
                            insts.push(obj);
                            if let Some(d) = dst.map(|v| v.as_u32()) {}
                        }
                        I::NewBox {
                            dst,
                            box_type,
                            args,
                        } => {
                            let args_a: Vec<_> = args.iter().map(|v| json!(v.as_u32())).collect();
                            insts.push(json!({"op":"newbox","type": box_type, "args": args_a, "dst": dst.as_u32()}));
                        }
                        I::Branch {
                            condition,
                            then_bb,
                            else_bb,
                        } => {
                            insts.push(json!({"op":"branch","cond": condition.as_u32(), "then": then_bb.as_u32(), "else": else_bb.as_u32()}));
                        }
                        I::Jump { target } => {
                            insts.push(json!({"op":"jump","target": target.as_u32()}));
                        }
                        I::Return { value } => {
                            insts.push(json!({"op":"ret","value": value.map(|v| v.as_u32())}));
                        }
                        _ => { /* skip non-essential ops for initial harness */ }
                    }
                }
                // Phase 131-13: Terminator emitted inline (no delayed copies)
                if let Some(term) = &bb.terminator {
                    match term {
                        I::Return { value } => insts.push(json!({"op":"ret","value": value.map(|v| v.as_u32())})),
                        I::Jump { target } => insts.push(json!({"op":"jump","target": target.as_u32()})),
                        I::Branch { condition, then_bb, else_bb } => insts.push(json!({"op":"branch","cond": condition.as_u32(), "then": then_bb.as_u32(), "else": else_bb.as_u32()})),
                        _ => {}
                    }
                }
                blocks.push(json!({"id": bid.as_u32(), "instructions": insts}));
            }
        }
        // Export parameter value-ids so a VM can bind arguments
        let params: Vec<_> = f.params.iter().map(|v| v.as_u32()).collect();

        // Phase 131-11-F: Build metadata JSON from MIR metadata (SSOT)
        let metadata_json = json!({
            "value_types": f.metadata.value_types.iter().map(|(k, v)| {
                let type_str = match v {
                    MirType::Integer => json!("i64"),
                    MirType::String => json!({"kind": "string"}),
                    MirType::Box(bt) => json!({"kind": "handle", "box_type": bt}),
                    MirType::Bool => json!("i1"),
                    MirType::Void => json!("void"),
                    MirType::Unknown => json!(null),
                    _ => json!(null),
                };
                (k.as_u32().to_string(), type_str)
            }).collect::<serde_json::Map<String, serde_json::Value>>()
        });

        funs.push(json!({
            "name": name,
            "params": params,
            "blocks": blocks,
            "metadata": metadata_json
        }));
    }

    // Phase 15.5: JSON v1 schema with environment variable control
    let use_v1_schema = std::env::var("NYASH_JSON_SCHEMA_V1").unwrap_or_default() == "1"
        || match std::env::var("NYASH_MIR_UNIFIED_CALL")
            .ok()
            .as_deref()
            .map(|s| s.to_ascii_lowercase())
        {
            Some(s) if s == "0" || s == "false" || s == "off" => false,
            _ => true,
        };

    // Phase 155: Extract CFG information for hako_check
    let cfg_info = nyash_rust::mir::extract_cfg_info(module);

    let root = if use_v1_schema {
        let mut root = create_json_v1_root(json!(funs));
        // Add CFG data to v1 schema
        if let Some(obj) = root.as_object_mut() {
            obj.insert("cfg".to_string(), cfg_info);
        }
        root
    } else {
        // v0 legacy format - also add CFG
        json!({"functions": funs, "cfg": cfg_info})
    };

    // NOTE: numeric_core strict validation is applied on the AotPrep output
    // (tools/hakorune_emit_mir.sh) rather than at raw MIR emit time. This keeps
    // pre-AotPrep MIR emission usable even when BoxCall(MatI64, mul_naive) is
    // still present.

    std::fs::write(path, serde_json::to_string_pretty(&root).unwrap())
        .map_err(|e| format!("write mir json: {}", e))
}

/// Variant for the bin crate's local MIR type
pub fn emit_mir_json_for_harness_bin(
    module: &crate::mir::MirModule,
    path: &std::path::Path,
) -> Result<(), String> {
    use crate::mir::{BinaryOp as B, CompareOp as C, MirInstruction as I, MirType};
    let mut funs = Vec::new();
    for (name, f) in &module.functions {
        let mut blocks = Vec::new();
        let mut ids: Vec<_> = f.blocks.keys().copied().collect();
        ids.sort();
        for bid in ids {
            if let Some(bb) = f.blocks.get(&bid) {
                let mut insts = Vec::new();
                // Phase 131-13: Emit all instructions in MIR order (SSOT principle)
                // No reordering except PHI consolidation at block start (LLVM constraint)

                // Step 1: Emit all PHI instructions first (LLVM requirement)
                for inst in &bb.instructions {
                    if let I::Phi { dst, inputs, .. } = inst {
                        let incoming: Vec<_> = inputs
                            .iter()
                            .map(|(b, v)| json!([v.as_u32(), b.as_u32()]))
                            .collect();
                        // Phase 131-11-F: Add dst_type hint from metadata for all PHI instructions
                        let mut phi_inst =
                            json!({"op":"phi","dst": dst.as_u32(), "incoming": incoming});
                        if let Some(dst_type) = f.metadata.value_types.get(dst) {
                            let type_json = match dst_type {
                                MirType::Integer => json!("i64"),
                                MirType::String => json!({"kind": "string"}),
                                MirType::Box(bt) => json!({"kind": "handle", "box_type": bt}),
                                MirType::Bool => json!("i1"),
                                MirType::Void => json!("void"),
                                _ => json!(null),
                            };
                            if !type_json.is_null() {
                                phi_inst["dst_type"] = type_json;
                            }
                        }
                        insts.push(phi_inst);
                    }
                }

                // Step 2: Emit all non-PHI instructions in MIR order (no reordering!)
                for inst in &bb.instructions {
                    match inst {
                        I::Phi { .. } => {
                            // Already emitted in step 1
                            continue;
                        }
                        I::Copy { dst, src } => {
                            insts.push(
                                json!({"op":"copy","dst": dst.as_u32(), "src": src.as_u32()}),
                            );
                        }
                        I::Const { dst, value } => match value {
                            crate::mir::ConstValue::Integer(i) => {
                                insts.push(json!({"op":"const","dst": dst.as_u32(), "value": {"type": "i64", "value": i}}));
                            }
                            crate::mir::ConstValue::Float(fv) => {
                                insts.push(json!({"op":"const","dst": dst.as_u32(), "value": {"type": "f64", "value": fv}}));
                            }
                            crate::mir::ConstValue::Bool(b) => {
                                insts.push(json!({"op":"const","dst": dst.as_u32(), "value": {"type": "i64", "value": if *b {1} else {0}}}));
                            }
                            crate::mir::ConstValue::String(s) => {
                                insts.push(json!({
                                    "op":"const",
                                    "dst": dst.as_u32(),
                                    "value": {
                                        "type": {"kind":"handle","box_type":"StringBox"},
                                        "value": s
                                    }
                                }));
                            }
                            crate::mir::ConstValue::Null | crate::mir::ConstValue::Void => {
                                insts.push(json!({"op":"const","dst": dst.as_u32(), "value": {"type": "void", "value": 0}}));
                            }
                        },
                        I::BinOp { dst, op, lhs, rhs } => {
                            let op_s = match op {
                                B::Add => "+",
                                B::Sub => "-",
                                B::Mul => "*",
                                B::Div => "/",
                                B::Mod => "%",
                                B::BitAnd => "&",
                                B::BitOr => "|",
                                B::BitXor => "^",
                                B::Shl => "<<",
                                B::Shr => ">>",
                                B::And => "&",
                                B::Or => "|",
                            };
                            let mut obj = json!({"op":"binop","operation": op_s, "lhs": lhs.as_u32(), "rhs": rhs.as_u32(), "dst": dst.as_u32()});
                            // Phase 131-15-P1: dst_type only when type is KNOWN (not Unknown)
                            // Operand TypeFacts take priority over dst_type hint in Python
                            if matches!(op, B::Add) {
                                let dst_type = f.metadata.value_types.get(dst);
                                match dst_type {
                                    Some(MirType::Box(bt)) if bt == "StringBox" => {
                                        obj["dst_type"] =
                                            json!({"kind":"handle","box_type":"StringBox"});
                                    }
                                    Some(MirType::Integer) => {
                                        // Explicitly mark as i64 for integer addition
                                        obj["dst_type"] = json!("i64");
                                    }
                                    Some(MirType::Unknown) | None => {
                                        // Unknown: DO NOT emit dst_type
                                        // Let Python side infer from operand TypeFacts
                                    }
                                    _ => {
                                        // Other known types: use conservative i64
                                        obj["dst_type"] = json!("i64");
                                    }
                                }
                            }
                            insts.push(obj);
                        }
                        I::Compare { dst, op, lhs, rhs } => {
                            let op_s = match op {
                                C::Eq => "==",
                                C::Ne => "!=",
                                C::Lt => "<",
                                C::Le => "<=",
                                C::Gt => ">",
                                C::Ge => ">=",
                            };
                            insts.push(json!({"op":"compare","operation": op_s, "lhs": lhs.as_u32(), "rhs": rhs.as_u32(), "dst": dst.as_u32()}));
                        }
                        I::Call {
                            dst,
                            func,
                            callee,
                            args,
                            effects,
                            ..
                        } => {
                            // Phase 15.5: Unified Call support with environment variable control
                            let use_unified = match std::env::var("NYASH_MIR_UNIFIED_CALL")
                                .ok()
                                .as_deref()
                                .map(|s| s.to_ascii_lowercase())
                            {
                                Some(s) if s == "0" || s == "false" || s == "off" => false,
                                _ => true,
                            };

                            if use_unified && callee.is_some() {
                                // v1: Unified mir_call format
                                let effects_str: Vec<&str> =
                                    if effects.is_io() { vec!["IO"] } else { vec![] };
                                let args_u32: Vec<u32> = args.iter().map(|v| v.as_u32()).collect();
                                let unified_call = emit_unified_mir_call(
                                    dst.map(|v| v.as_u32()),
                                    callee.as_ref().unwrap(),
                                    &args_u32,
                                    &effects_str,
                                );
                                insts.push(unified_call);
                            } else if !use_unified && callee.is_some() {
                                // v0: When unified is OFF but callee exists, emit proper v0 format
                                use Callee;
                                match callee.as_ref().unwrap() {
                                    Callee::Method {
                                        method, receiver, ..
                                    } => {
                                        // Emit as boxcall for compatibility
                                        let box_val = receiver.unwrap_or(*func);
                                        let args_a: Vec<_> =
                                            args.iter().map(|v| json!(v.as_u32())).collect();
                                        let mut obj = json!({
                                            "op":"boxcall",
                                            "box": box_val.as_u32(),
                                            "method": method,
                                            "args": args_a,
                                            "dst": dst.map(|d| d.as_u32())
                                        });
                                        // Add dst_type hints for known methods
                                        let m = method.as_str();
                                        let dst_ty = if m == "substring"
                                            || m == "dirname"
                                            || m == "join"
                                            || m == "read_all"
                                            || m == "read"
                                        {
                                            Some(json!({"kind":"handle","box_type":"StringBox"}))
                                        } else if m == "length" || m == "lastIndexOf" {
                                            Some(json!("i64"))
                                        } else {
                                            None
                                        };
                                        if let Some(t) = dst_ty {
                                            obj["dst_type"] = t;
                                        }
                                        insts.push(obj);
                                        if let Some(d) = dst.map(|v| v.as_u32()) {}
                                    }
                                    _ => {
                                        // Other callee types: emit generic call
                                        let args_a: Vec<_> =
                                            args.iter().map(|v| json!(v.as_u32())).collect();
                                        insts.push(json!({"op":"call","func": func.as_u32(), "args": args_a, "dst": dst.map(|d| d.as_u32())}));
                                    }
                                }
                            } else {
                                // v0: Legacy call format (no callee info)
                                let args_a: Vec<_> =
                                    args.iter().map(|v| json!(v.as_u32())).collect();
                                insts.push(json!({"op":"call","func": func.as_u32(), "args": args_a, "dst": dst.map(|d| d.as_u32())}));
                            }
                        }
                        I::ExternCall {
                            dst,
                            iface_name,
                            method_name,
                            args,
                            ..
                        } => {
                            let args_a: Vec<_> = args.iter().map(|v| json!(v.as_u32())).collect();
                            let mut obj = json!({
                                "op":"externcall","func": format!("{}.{}", iface_name, method_name), "args": args_a,
                                "dst": dst.map(|d| d.as_u32()),
                            });
                            if iface_name == "env.console" {
                                if dst.is_some() {
                                    obj["dst_type"] = json!("i64");
                                }
                            }
                            insts.push(obj);
                            if let Some(d) = dst.map(|v| v.as_u32()) {}
                        }
                        I::BoxCall {
                            dst,
                            box_val,
                            method,
                            args,
                            ..
                        } => {
                            let args_a: Vec<_> = args.iter().map(|v| json!(v.as_u32())).collect();
                            let mut obj = json!({
                                "op":"boxcall","box": box_val.as_u32(), "method": method, "args": args_a, "dst": dst.map(|d| d.as_u32())
                            });
                            let m = method.as_str();
                            let dst_ty = if m == "substring"
                                || m == "dirname"
                                || m == "join"
                                || m == "read_all"
                                || m == "read"
                            {
                                Some(json!({"kind":"handle","box_type":"StringBox"}))
                            } else if m == "length" || m == "lastIndexOf" {
                                Some(json!("i64"))
                            } else {
                                None
                            };
                            if let Some(t) = dst_ty {
                                obj["dst_type"] = t;
                            }
                            insts.push(obj);
                            if let Some(d) = dst.map(|v| v.as_u32()) {}
                        }
                        I::NewBox {
                            dst,
                            box_type,
                            args,
                        } => {
                            let args_a: Vec<_> = args.iter().map(|v| json!(v.as_u32())).collect();
                            insts.push(json!({"op":"newbox","type": box_type, "args": args_a, "dst": dst.as_u32()}));
                        }
                        I::Branch {
                            condition,
                            then_bb,
                            else_bb,
                        } => {
                            insts.push(json!({"op":"branch","cond": condition.as_u32(), "then": then_bb.as_u32(), "else": else_bb.as_u32()}));
                        }
                        I::Jump { target } => {
                            insts.push(json!({"op":"jump","target": target.as_u32()}));
                        }
                        I::Return { value } => {
                            insts.push(json!({"op":"ret","value": value.map(|v| v.as_u32())}));
                        }
                        _ => {}
                    }
                }
                // Phase 131-13: Terminator emitted inline (no delayed copies)
                if let Some(term) = &bb.terminator {
                    match term {
                    I::Return { value } => insts.push(json!({"op":"ret","value": value.map(|v| v.as_u32())})),
                    I::Jump { target } => insts.push(json!({"op":"jump","target": target.as_u32()})),
                    I::Branch { condition, then_bb, else_bb } => insts.push(json!({"op":"branch","cond": condition.as_u32(), "then": then_bb.as_u32(), "else": else_bb.as_u32()})),
                    _ => {} }
                }
                blocks.push(json!({"id": bid.as_u32(), "instructions": insts}));
            }
        }
        let params: Vec<_> = f.params.iter().map(|v| v.as_u32()).collect();

        // Phase 131-11-F: Build metadata JSON from MIR metadata (SSOT)
        let metadata_json = json!({
            "value_types": f.metadata.value_types.iter().map(|(k, v)| {
                let type_str = match v {
                    MirType::Integer => json!("i64"),
                    MirType::String => json!({"kind": "string"}),
                    MirType::Box(bt) => json!({"kind": "handle", "box_type": bt}),
                    MirType::Bool => json!("i1"),
                    MirType::Void => json!("void"),
                    MirType::Unknown => json!(null),
                    _ => json!(null),
                };
                (k.as_u32().to_string(), type_str)
            }).collect::<serde_json::Map<String, serde_json::Value>>()
        });

        funs.push(json!({
            "name": name,
            "params": params,
            "blocks": blocks,
            "metadata": metadata_json
        }));
    }

    // Phase 155: Extract CFG information for hako_check
    let cfg_info = crate::mir::extract_cfg_info(module);

    let root = json!({"functions": funs, "cfg": cfg_info});

    // NOTE: numeric_core strict validation is applied on the AotPrep output
    // (tools/hakorune_emit_mir.sh) rather than at raw MIR emit time. This keeps
    // pre-AotPrep MIR emission usable even when BoxCall(MatI64, mul_naive) is
    // still present.

    std::fs::write(path, serde_json::to_string_pretty(&root).unwrap())
        .map_err(|e| format!("write mir json: {}", e))
}
