const std = @import("std");
const ast = @import("../../ast.zig");
const Node = ast.Node;
const types = @import("../types.zig");
const inst_mod = @import("../inst.zig");
const errors = @import("../../errors.zig");

const TypeId = types.TypeId;
const Ref = inst_mod.Ref;
const BlockId = inst_mod.BlockId;

const lower = @import("../lower.zig");
const Lowering = lower.Lowering;
const Scope = lower.Scope;
const ComptimeValue = Lowering.ComptimeValue;
const isTypeCategoryMatch = Lowering.isTypeCategoryMatch;

pub fn lowerIfExpr(self: *Lowering, ie: *const ast.IfExpr) Ref {
    // inline if: evaluate condition at compile time, only lower taken branch
    if (ie.is_comptime) {
        if (self.evalComptimeCondition(ie.condition)) |is_true| {
            if (is_true) {
                return self.lowerInlineBranch(ie.then_branch);
            } else if (ie.else_branch) |eb| {
                return self.lowerInlineBranch(eb);
            }
            return self.builder.constInt(0, .void);
        }
        // Condition couldn't be evaluated — fall through to runtime
    }

    // Check for constant-bool conditions (e.g., is_flags(T) → false) to avoid dead-code LLVM errors
    if (self.tryConstBoolCondition(ie.condition)) |is_true| {
        if (is_true) {
            // Condition always true: only lower then-branch
            if ((ie.is_inline or self.force_block_value) and ie.else_branch != null) {
                return self.lowerExpr(ie.then_branch);
            }
            self.lowerBlock(ie.then_branch);
            // If then-branch terminated (return/break), mark block as dead
            if (self.currentBlockHasTerminator()) {
                self.block_terminated = true;
                return .none;
            }
            return self.builder.constInt(0, .void);
        } else {
            // Condition always false: only lower else-branch (if any)
            if (ie.else_branch) |eb| {
                if (ie.is_inline or self.force_block_value) {
                    return self.lowerExpr(eb);
                }
                self.lowerBlock(eb);
                if (self.currentBlockHasTerminator()) {
                    self.block_terminated = true;
                    return .none;
                }
            }
            return self.builder.constInt(0, .void);
        }
    }

    // Optional binding: `if val := expr { ... }`
    // Clear target_type so the ternary's result type doesn't leak into the condition
    // (e.g., `if x != 0 then 1.0 else 2.0` — the `0` must be i64, not f32)
    const saved_cond_target = self.target_type;
    self.target_type = null;
    const opt_val = self.lowerExpr(ie.condition);
    self.target_type = saved_cond_target;
    const cond = if (ie.binding_name != null) blk: {
        // The condition is an optional — emit has_value check
        break :blk self.builder.emit(.{ .optional_has_value = .{ .operand = opt_val } }, .bool);
    } else opt_val;
    const has_else = ie.else_branch != null;
    // If-else produces a value when inline OR when in value position (force_block_value)
    var is_value = (ie.is_inline or self.force_block_value) and has_else;

    // Infer result type from then branch for value if-exprs
    // If then_branch is null/void, try else_branch (e.g., `if cond then null else val`)
    var result_type: TypeId = if (is_value) blk: {
        var t = self.inferExprType(ie.then_branch);
        if ((t == .void or t == .unresolved) and ie.else_branch != null) {
            t = self.inferExprType(ie.else_branch.?);
        }
        // Branch type not statically inferable (e.g. `null` / a bare enum
        // literal) — use the contextually expected type rather than a guess.
        if (t == .unresolved) {
            if (self.target_type) |tt| t = tt;
        }
        break :blk t;
    } else .void;

    // A value-position if/else whose branches yield no value (both are
    // `;`-terminated / void blocks) is really a statement-if — lowering it
    // as a value would build a `phi void`. Demote it.
    if (is_value and result_type == .void) {
        is_value = false;
        result_type = .void;
    }

    const then_bb = self.freshBlock("if.then");
    const else_bb: ?BlockId = if (has_else) self.freshBlock("if.else") else null;
    const merge_params: []const TypeId = if (is_value) &.{result_type} else &.{};
    const merge_bb = self.freshBlockWithParams("if.merge", merge_params);

    // Conditional branch
    self.builder.condBr(
        cond,
        then_bb,
        &.{},
        if (else_bb) |eb| eb else merge_bb,
        &.{},
    );

    // Then branch
    self.builder.switchToBlock(then_bb);
    // If binding: unwrap the optional and bind to the name
    if (ie.binding_name) |bind_name| {
        const opt_ty = self.inferExprType(ie.condition);
        const inner_ty = if (!opt_ty.isBuiltin()) blk: {
            const info = self.module.types.get(opt_ty);
            break :blk if (info == .optional) info.optional.child else opt_ty;
        } else opt_ty;
        const unwrapped = self.builder.emit(.{ .optional_unwrap = .{ .operand = opt_val } }, inner_ty);
        const slot = self.builder.alloca(inner_ty);
        self.builder.store(slot, unwrapped);
        if (self.scope) |scope| {
            scope.put(bind_name, .{ .ref = slot, .ty = inner_ty, .is_alloca = true });
        }
    }
    // Set target_type so null/undef in branches get the right type
    const saved_target = self.target_type;
    if (is_value and result_type != .void) self.target_type = result_type;
    if (is_value) {
        var v = self.lowerExpr(ie.then_branch);
        if (!self.currentBlockHasTerminator()) {
            const v_ty = self.builder.getRefType(v);
            if (v_ty != result_type and v_ty != .void and result_type != .void) {
                v = self.coerceToType(v, v_ty, result_type);
            }
            self.builder.br(merge_bb, &.{v});
        }
    } else {
        self.lowerBlock(ie.then_branch);
        if (!self.currentBlockHasTerminator()) {
            self.builder.br(merge_bb, &.{});
        }
    }

    // Else branch
    if (has_else) {
        self.builder.switchToBlock(else_bb.?);
        if (is_value) {
            var v = self.lowerExpr(ie.else_branch.?);
            if (!self.currentBlockHasTerminator()) {
                const v_ty = self.builder.getRefType(v);
                if (v_ty != result_type and v_ty != .void and result_type != .void) {
                    v = self.coerceToType(v, v_ty, result_type);
                }
                self.builder.br(merge_bb, &.{v});
            }
        } else {
            self.lowerBlock(ie.else_branch.?);
            if (!self.currentBlockHasTerminator()) {
                self.builder.br(merge_bb, &.{});
            }
        }
    }
    self.target_type = saved_target;

    // Continue at merge
    self.builder.switchToBlock(merge_bb);
    if (is_value) {
        return self.builder.blockParam(merge_bb, 0, result_type);
    }
    return self.builder.constInt(0, .void);
}

/// Try to evaluate an AST condition as a compile-time constant bool.
/// Returns true/false if the condition is known at compile time, null otherwise.
pub fn tryConstBoolCondition(self: *Lowering, node: *const Node) ?bool {
    switch (node.data) {
        .bool_literal => |bl| return bl.value,
        .call => |c| {
            if (c.callee.data == .identifier) {
                const cname = c.callee.data.identifier.name;
                if (std.mem.eql(u8, cname, "is_flags")) {
                    // Resolve the type arg to check if it's actually a flags enum
                    if (c.args.len > 0) {
                        const ty = self.resolveTypeArg(c.args[0]);
                        if (!ty.isBuiltin()) {
                            const info = self.module.types.get(ty);
                            if (info == .@"enum") return info.@"enum".is_flags;
                        }
                    }
                    return false;
                }
                if (std.mem.eql(u8, cname, "type_eq") and c.args.len >= 2) {
                    const a = self.resolveTypeArg(c.args[0]);
                    const b = self.resolveTypeArg(c.args[1]);
                    return a == b;
                }
                if (std.mem.eql(u8, cname, "has_impl") and c.args.len >= 2) {
                    const ty = self.resolveTypeArg(c.args[1]);
                    return self.computeHasImpl(c.args[0], ty);
                }
            }
        },
        else => {},
    }
    return null;
}

pub fn lowerWhile(self: *Lowering, we: *const ast.WhileExpr) Ref {
    const header_bb = self.freshBlock("while.hdr");
    const body_bb = self.freshBlock("while.body");
    const exit_bb = self.freshBlock("while.exit");

    // Branch to header
    self.builder.br(header_bb, &.{});

    // Header: evaluate condition
    self.builder.switchToBlock(header_bb);
    const cond = self.lowerExpr(we.condition);
    self.builder.condBr(cond, body_bb, &.{}, exit_bb, &.{});

    // Body
    self.builder.switchToBlock(body_bb);

    // Save and set loop targets
    const old_break = self.break_target;
    const old_continue = self.continue_target;
    const old_defer_base = self.loop_defer_base;
    self.break_target = exit_bb;
    self.continue_target = header_bb;
    self.loop_defer_base = self.defer_stack.items.len;
    defer {
        self.break_target = old_break;
        self.continue_target = old_continue;
        self.loop_defer_base = old_defer_base;
    }

    self.lowerBlock(we.body);
    if (!self.currentBlockHasTerminator()) {
        self.builder.br(header_bb, &.{});
    }

    // Continue at exit
    self.builder.switchToBlock(exit_bb);
    return self.builder.constInt(0, .void);
}

/// View a `List(T)`-like struct (`{ items: [*]T, len, … }`) as its backing
/// `items` pointer + element type + `len`, so `for list: (x)` iterates the
/// elements. Null for anything that isn't such a struct.
pub fn listView(self: *Lowering, value: Ref, ty: TypeId) ?struct { data: Ref, data_ty: TypeId, len: Ref } {
    if (ty.isBuiltin()) return null;
    const info = self.module.types.get(ty);
    if (info != .@"struct") return null;
    const items_id = self.module.types.internString("items");
    const len_id = self.module.types.internString("len");
    var items_idx: ?u32 = null;
    var items_ty: TypeId = .unresolved;
    var len_idx: ?u32 = null;
    for (info.@"struct".fields, 0..) |f, i| {
        if (f.name == items_id and !f.ty.isBuiltin() and self.module.types.get(f.ty) == .many_pointer) {
            items_idx = @intCast(i);
            items_ty = f.ty;
        } else if (f.name == len_id) {
            len_idx = @intCast(i);
        }
    }
    if (items_idx == null or len_idx == null) return null;
    return .{
        .data = self.builder.emit(.{ .struct_get = .{ .base = value, .field_index = items_idx.? } }, items_ty),
        .data_ty = items_ty,
        .len = self.builder.emit(.{ .struct_get = .{ .base = value, .field_index = len_idx.? } }, .i64),
    };
}

/// Lowered prep for one position of a multi-iterable `for` header. Every
/// position gets its own i64 cursor slot (ranges start at their `start`,
/// collections at 0); all cursors advance by 1 per iteration, and ONLY the
/// first position's bound terminates the loop (first-iterable-wins).
const IterPrep = struct {
    is_range: bool,
    slot: Ref,
    // Collection-only fields:
    data: Ref = Ref.none,
    data_ty: TypeId = .unresolved,
    elem_ty: TypeId = .unresolved,
    is_array: bool = false,
    storage: ?Ref = null, // array's own alloca when addressable (not deref'd)
};

/// `for it1, it2, ... (c1, c2, ...) { }` — parallel iteration. The first
/// iterable's length/bound drives the loop; the others follow by position.
/// Consequences of first-iterable-wins: a non-first range's end is never
/// lowered (its side effects do not run), and a shorter non-first collection
/// is read past its length on mismatch — the first iterable is the
/// authoritative one.
pub fn lowerFor(self: *Lowering, fe: *const ast.ForExpr) Ref {
    if (fe.is_inline) return self.lowerInlineRangeFor(fe);

    // A pack has no runtime value to iterate (Decision 1) — point the user
    // at `inline for`.
    for (fe.iterables) |it| {
        if (!it.is_range and it.expr.data == .identifier and self.isPackName(it.expr.data.identifier.name)) {
            return self.diagPackAsValue(it.expr.data.identifier.name, it.expr.span, .runtime_iter);
        }
    }

    var preps = std.ArrayList(IterPrep).empty;
    defer preps.deinit(self.alloc);
    var limit: Ref = Ref.none; // exclusive bound of position 0

    for (fe.iterables, 0..) |it, i| {
        if (it.is_range) {
            var start_ref = self.lowerExpr(it.expr);
            if (it.start_exclusive) start_ref = self.builder.add(start_ref, self.builder.constInt(1, .i64), .i64);
            const slot = self.builder.alloca(.i64);
            self.builder.store(slot, start_ref);
            if (i == 0) {
                // Parser guarantees the first iterable is bounded.
                var end_ref = self.lowerExpr(it.range_end.?);
                if (it.end_inclusive) end_ref = self.builder.add(end_ref, self.builder.constInt(1, .i64), .i64);
                limit = end_ref;
            }
            preps.append(self.alloc, .{ .is_range = true, .slot = slot }) catch unreachable;
        } else {
            var data = self.lowerExpr(it.expr);
            var data_ty = self.inferExprType(it.expr);

            // `*List` / `*[]T` etc. — deref to the collection value. Tracked
            // because a deref'd iterable's identifier binding holds the
            // POINTER, so its alloca is not the collection's storage.
            var was_deref = false;
            const ptr_info = if (data_ty.isBuiltin()) null else self.module.types.get(data_ty);
            if (ptr_info != null and ptr_info.? == .pointer) {
                data = self.builder.load(data, ptr_info.?.pointer.pointee);
                data_ty = ptr_info.?.pointer.pointee;
                was_deref = true;
            }

            // A `List(T)`-like struct iterates its `items[0..len]`;
            // arrays/slices use their intrinsic length.
            var len: Ref = Ref.none;
            if (self.listView(data, data_ty)) |lv| {
                data = lv.data;
                data_ty = lv.data_ty;
                len = lv.len;
            } else if (i == 0) {
                len = self.builder.emit(.{ .length = .{ .operand = data } }, .i64);
            }

            const elem_ty = self.getElementType(data_ty);
            if (elem_ty == .unresolved) {
                // Not a collection. The common trip: `for f(n) { }` — the
                // trailing parens are the CAPTURE, so the iterable is `f`.
                if (self.diagnostics) |d| {
                    if (data_ty == .unresolved) {
                        d.addFmt(.err, it.expr.span, "cannot iterate this expression — if the parens were call arguments, a call iterable also needs a capture (`for f(n) (x) {{ }}`) or parentheses (`for (f(n)) {{ }}`)", .{});
                    } else {
                        d.addFmt(.err, it.expr.span, "cannot iterate a value of type '{s}' — if the parens were call arguments, a call iterable also needs a capture (`for f(n) (x) {{ }}`) or parentheses (`for (f(n)) {{ }}`)", .{self.module.types.typeName(data_ty)});
                    }
                }
                return self.builder.constInt(0, .void);
            }
            const is_array = !data_ty.isBuiltin() and self.module.types.get(data_ty) == .array;
            const storage = if (is_array and !was_deref) self.getExprAlloca(it.expr) else null;
            const slot = self.builder.alloca(.i64);
            self.builder.store(slot, self.builder.constInt(0, .i64));
            if (i == 0) limit = len;
            preps.append(self.alloc, .{
                .is_range = false,
                .slot = slot,
                .data = data,
                .data_ty = data_ty,
                .elem_ty = elem_ty,
                .is_array = is_array,
                .storage = storage,
            }) catch unreachable;
        }
    }

    const header_bb = self.freshBlock("for.hdr");
    const body_bb = self.freshBlock("for.body");
    const inc_bb = self.freshBlock("for.inc");
    const exit_bb = self.freshBlock("for.exit");

    self.builder.br(header_bb, &.{});

    // Header: first cursor against the first bound.
    self.builder.switchToBlock(header_bb);
    const cur0 = self.builder.load(preps.items[0].slot, .i64);
    const cmp = self.builder.cmpLt(cur0, limit);
    self.builder.condBr(cmp, body_bb, &.{}, exit_bb, &.{});

    // Body: bind one capture per position (when captures are present).
    self.builder.switchToBlock(body_bb);

    var body_scope = Scope.init(self.alloc, self.scope);
    const old_scope = self.scope;
    self.scope = &body_scope;

    for (fe.captures, 0..) |cap, i| {
        const prep = preps.items[i];
        const cur = if (i == 0) cur0 else self.builder.load(prep.slot, .i64);
        if (prep.is_range) {
            body_scope.put(cap.name, .{ .ref = cur, .ty = .i64, .is_alloca = false });
            continue;
        }
        const bind_ty = if (cap.by_ref) self.module.types.ptrTo(prep.elem_ty) else prep.elem_ty;
        const elem = if (cap.by_ref) blk: {
            // A slice value carries its backing pointer, so GEP on it writes
            // through. An array is a value — GEP needs its storage (alloca)
            // or mutations would hit a copy.
            const base = if (prep.is_array) (prep.storage orelse prep.data) else prep.data;
            break :blk self.builder.emit(.{ .index_gep = .{ .lhs = base, .rhs = cur } }, bind_ty);
        } else blk: {
            // By-value over an array with addressable storage: GEP + load ONE
            // element. `index_get` on the array VALUE spills the whole array
            // to a temp on every iteration — O(N²) bytes copied per loop.
            if (prep.storage) |storage| {
                const elem_ptr = self.builder.emit(.{ .index_gep = .{ .lhs = storage, .rhs = cur } }, self.module.types.ptrTo(prep.elem_ty));
                break :blk self.builder.load(elem_ptr, prep.elem_ty);
            }
            break :blk self.builder.emit(.{ .index_get = .{ .lhs = prep.data, .rhs = cur } }, bind_ty);
        };
        body_scope.put(cap.name, .{ .ref = elem, .ty = bind_ty, .is_alloca = false, .is_ref_capture = cap.by_ref });
    }

    // Save and set loop targets
    const old_break = self.break_target;
    const old_continue = self.continue_target;
    const old_defer_base = self.loop_defer_base;
    self.break_target = exit_bb;
    self.continue_target = inc_bb; // continue → increment, not header
    self.loop_defer_base = self.defer_stack.items.len;

    self.lowerBlock(fe.body);

    self.break_target = old_break;
    self.continue_target = old_continue;
    self.loop_defer_base = old_defer_base;
    self.scope = old_scope;
    body_scope.deinit();

    // Fall through to increment block
    if (!self.currentBlockHasTerminator()) {
        self.builder.br(inc_bb, &.{});
    }

    // Increment block: advance every cursor and jump back to header.
    self.builder.switchToBlock(inc_bb);
    {
        const one = self.builder.constInt(1, .i64);
        for (preps.items) |prep| {
            const cur = self.builder.load(prep.slot, .i64);
            const next = self.builder.add(cur, one, .i64);
            self.builder.store(prep.slot, next);
        }
        self.builder.br(header_bb, &.{});
    }

    // Continue at exit
    self.builder.switchToBlock(exit_bb);
    return self.builder.constInt(0, .void);
}

/// Comptime-unrolled `inline for`. Iterables are comptime ranges and/or
/// PACKS, mirroring the runtime multi-iterable contract: position 0 drives
/// the iteration count (a pack's arity, or a bounded range's span) and
/// trailing range bounds are ignored. Per iteration the body is lowered
/// once; a range capture binds as an `int_val` comptime constant (so
/// `xs[i]` substitutes the concrete per-position argument), and a pack
/// capture binds as an AST alias for the synthesized `xs[<i>]`
/// (`Binding.pack_elem`), inheriting full pack-element semantics —
/// substitution, typing, and the interface-only constraint check.
///
///   inline for 0..xs.len (i) { xs[i].show(); }      // index form
///   inline for xs (x) { x.show(); }                 // element form
///   inline for xs, 0.. (x, i) { ... }               // element + index
pub fn lowerInlineRangeFor(self: *Lowering, fe: *const ast.ForExpr) Ref {
    const IterClass = union(enum) {
        range: i64, // comptime start value
        pack: []const u8, // pack name
    };
    var classes = std.ArrayList(IterClass).empty;
    defer classes.deinit(self.alloc);
    var count: i64 = 0;

    for (fe.iterables, 0..) |it, idx| {
        if (it.is_range) {
            var start = self.evalComptimeInt(it.expr) orelse {
                if (self.diagnostics) |d| d.addFmt(.err, it.expr.span, "inline for: range start is not a compile-time integer", .{});
                return self.builder.constInt(0, .void);
            };
            if (it.start_exclusive) start += 1;
            if (idx == 0) {
                const end_node = it.range_end orelse {
                    if (self.diagnostics) |d| d.addFmt(.err, it.expr.span, "inline for: the first range must be bounded — `inline for 0..N (i) {{ }}`", .{});
                    return self.builder.constInt(0, .void);
                };
                var end = self.evalComptimeInt(end_node) orelse {
                    if (self.diagnostics) |d| d.addFmt(.err, end_node.span, "inline for: range end is not a compile-time integer", .{});
                    return self.builder.constInt(0, .void);
                };
                if (it.end_inclusive) end += 1;
                count = end - start;
            }
            classes.append(self.alloc, .{ .range = start }) catch unreachable;
        } else if (it.expr.data == .identifier and self.isPackName(it.expr.data.identifier.name)) {
            const name = it.expr.data.identifier.name;
            const len: i64 = if (self.pack_param_count) |ppc| @intCast(ppc.get(name) orelse 0) else 0;
            if (idx == 0) {
                count = len;
            } else if (len < count) {
                if (self.diagnostics) |d| d.addFmt(.err, it.expr.span, "inline for: pack '{s}' has {} element{s} but the unroll is {} iterations", .{
                    name, len, if (len == 1) @as([]const u8, "") else @as([]const u8, "s"), count,
                });
                return self.builder.constInt(0, .void);
            }
            classes.append(self.alloc, .{ .pack = name }) catch unreachable;
        } else {
            if (self.diagnostics) |d| d.addFmt(.err, it.expr.span, "inline for: each iterable must be a comptime range or a pack — `inline for 0..N (i) {{ }}` / `inline for xs (x) {{ }}`", .{});
            return self.builder.constInt(0, .void);
        }
    }

    // `(*x)` on a pack element: there is no storage to borrow — an element
    // is an AST-substituted call argument.
    for (fe.captures, 0..) |cap, ci| {
        if (cap.by_ref and ci < classes.items.len and classes.items[ci] == .pack) {
            const sp = cap.span orelse fe.iterables[ci].expr.span;
            if (self.diagnostics) |d| d.addFmt(.err, sp, "a pack element cannot be captured by reference", .{});
            return self.builder.constInt(0, .void);
        }
    }

    const CursorSave = struct { name: []const u8, had_prev: bool, prev: ComptimeValue };

    var i: i64 = 0;
    while (i < count) : (i += 1) {
        var body_scope = Scope.init(self.alloc, self.scope);
        const old_scope = self.scope;
        self.scope = &body_scope;

        var saves = std.ArrayList(CursorSave).empty;
        defer saves.deinit(self.alloc);

        for (fe.captures, 0..) |cap, ci| {
            if (cap.name.len == 0) continue;
            switch (classes.items[ci]) {
                .range => |start| {
                    // Bind the cursor both as a runtime value (constInt, for
                    // uses like `print(i)`) and as a comptime constant (for
                    // `xs[i]` substitution).
                    const v = start + i;
                    body_scope.put(cap.name, .{ .ref = self.builder.constInt(v, .i64), .ty = .i64, .is_alloca = false });
                    var save = CursorSave{ .name = cap.name, .had_prev = false, .prev = undefined };
                    if (self.comptime_constants.get(cap.name)) |p| {
                        save.had_prev = true;
                        save.prev = p;
                    }
                    saves.append(self.alloc, save) catch {};
                    self.comptime_constants.put(cap.name, .{ .int_val = v }) catch {};
                },
                .pack => |pack_name| {
                    const span = fe.iterables[ci].expr.span;
                    const id_node = self.alloc.create(Node) catch break;
                    id_node.* = .{ .span = span, .data = .{ .identifier = .{ .name = pack_name } } };
                    const idx_node = self.alloc.create(Node) catch break;
                    idx_node.* = .{ .span = span, .data = .{ .int_literal = .{ .value = i } } };
                    const elem_node = self.alloc.create(Node) catch break;
                    elem_node.* = .{ .span = span, .data = .{ .index_expr = .{ .object = id_node, .index = idx_node } } };
                    const elem_ty = self.inferExprType(elem_node);
                    body_scope.put(cap.name, .{ .ref = Ref.none, .ty = elem_ty, .is_alloca = false, .pack_elem = elem_node });
                },
            }
        }

        self.lowerBlock(fe.body);

        for (saves.items) |save| {
            if (save.had_prev) {
                self.comptime_constants.put(save.name, save.prev) catch {};
            } else {
                _ = self.comptime_constants.remove(save.name);
            }
        }

        self.scope = old_scope;
        body_scope.deinit();

        if (self.currentBlockHasTerminator()) break;
    }

    return self.builder.constInt(0, .void);
}

pub fn lowerMatch(self: *Lowering, me: *const ast.MatchExpr) Ref {
    // inline if match: evaluate at compile time, only lower the matching arm
    if (me.is_comptime) {
        if (self.evalComptimeMatch(me)) |arm_body| {
            return self.lowerInlineBranch(arm_body);
        }
        // Couldn't evaluate — fall through to runtime
    }

    const is_type_match = isTypeCategoryMatch(me);
    var subject = self.lowerExpr(me.subject);
    var subject_ty = self.inferExprType(me.subject);
    // A pointer subject (e.g. a `for xs: (*x)` element capture) — deref to
    // the pointed-to union/enum so tag/payload extraction works.
    if (!subject_ty.isBuiltin()) {
        const sinfo = self.module.types.get(subject_ty);
        if (sinfo == .pointer and !sinfo.pointer.pointee.isBuiltin()) {
            const pinfo = self.module.types.get(sinfo.pointer.pointee);
            if (pinfo == .tagged_union or pinfo == .@"enum") {
                subject = self.builder.load(subject, sinfo.pointer.pointee);
                subject_ty = sinfo.pointer.pointee;
            }
        }
    }
    const is_optional_match = blk: {
        if (!subject_ty.isBuiltin()) {
            const info = self.module.types.get(subject_ty);
            break :blk info == .optional;
        }
        break :blk false;
    };
    // An error-set subject (`catch e == { case .X: ... }` / `if e == { ... }`):
    // the value IS its u32 tag id, and `case .X` matches the global tag id
    // of `X`. Used by ERR E1.5's catch match-body form.
    const is_error_set_match = blk: {
        if (!subject_ty.isBuiltin()) {
            break :blk self.module.types.get(subject_ty) == .error_set;
        }
        break :blk false;
    };

    // Determine if the match produces a value (has non-void arms)
    // For type-category matches (inside any_to_string), only produce value when force_block_value
    // For regular enum/optional matches, always produce value if arms are non-void
    var inferred_result = self.inferMatchResultType(me);
    // Arms not statically inferable (bare enum literals etc.): only a
    // value-position match (`force_block_value`) needs a concrete result —
    // use the contextually expected type. A statement match with non-value
    // arms is a side-effect (void); don't let a leaked `target_type` turn
    // it into a value match.
    if (inferred_result == .unresolved) {
        inferred_result = if (self.force_block_value) (self.target_type orelse .unresolved) else .void;
    }
    const is_value = if (is_type_match) self.force_block_value else (self.force_block_value or (inferred_result != .void and inferred_result != .unresolved));
    const result_type: TypeId = if (is_value) inferred_result else .void;
    // A fully-diverging match (`result_type == .noreturn` — every arm
    // `return`s / `raise`s / etc.) produces no value, so it builds no
    // merge phi; its arms terminate and the merge block is unreachable.
    const has_value_merge = is_value and result_type != .void and result_type != .noreturn;
    const merge_params: []const TypeId = if (has_value_merge) &.{result_type} else &.{};
    const merge_bb = self.freshBlockWithParams("match.merge", merge_params);

    // Build arm blocks
    var default_bb: ?BlockId = null;
    var arm_blocks = std.ArrayList(BlockId).empty;
    defer arm_blocks.deinit(self.alloc);
    for (me.arms) |_| {
        arm_blocks.append(self.alloc, self.freshBlock("match.arm")) catch unreachable;
    }

    // Build case list and pre-collect type tags per arm
    var cases = std.ArrayList(inst_mod.SwitchBranch.Case).empty;
    defer cases.deinit(self.alloc);
    var arm_tag_values = std.ArrayList([]const u64).empty;
    defer arm_tag_values.deinit(self.alloc);

    for (me.arms, 0..) |arm, i| {
        if (arm.pattern == null) {
            default_bb = arm_blocks.items[i];
            arm_tag_values.append(self.alloc, &.{}) catch unreachable;
            continue;
        }
        const pat = arm.pattern.?;

        if (is_type_match) {
            // Type-category match: resolve category name to tag values
            const name = switch (pat.data) {
                .identifier => |id| id.name,
                .type_expr => |te| te.name,
                else => "",
            };
            // E4 single-hop visibility + ambiguity gate: a SPECIFIC 2-flat-hop
            // type name in a type-match arm (`case COnly:`) is not bare-visible
            // (consistent with annotations / 0763); ≥2 direct flat same-name
            // authors are ambiguous (loud diagnostic, 0755/0767). A category
            // keyword (`int`, `struct`, …) is not a type author anywhere → the
            // gate is a no-op (`.proceed`) and `resolveTypeCategoryTags` expands
            // it. A source-keyed specific TYPE author — including the querying
            // source's OWN author over a same-name flat import (own-wins, 0754) —
            // matches on ITS TypeId, NOT whichever same-name author a global
            // `findByName` (inside `resolveTypeCategoryTags`) would pick.
            const tag_values = switch (self.headTypeGate(name, pat.span)) {
                .ambiguous, .not_visible => {
                    arm_tag_values.append(self.alloc, &.{}) catch unreachable;
                    continue;
                },
                .resolved => |tid| blk_tv: {
                    const tv = self.alloc.alloc(u64, 1) catch unreachable;
                    tv[0] = tid.index();
                    break :blk_tv tv;
                },
                .proceed => self.resolveTypeCategoryTags(name),
            };
            arm_tag_values.append(self.alloc, tag_values) catch unreachable;
            for (tag_values) |tag| {
                cases.append(self.alloc, .{
                    .value = @intCast(tag),
                    .target = arm_blocks.items[i],
                    .args = &.{},
                }) catch unreachable;
            }
        } else if (is_optional_match) {
            // Optional match: .some → 1 (has_value=true), .none → 0
            arm_tag_values.append(self.alloc, &.{}) catch unreachable;
            const pat_name = switch (pat.data) {
                .enum_literal => |el| el.name,
                .identifier => |id| id.name,
                else => "",
            };
            const case_val: u64 = if (std.mem.eql(u8, pat_name, "some")) 1 else 0;
            cases.append(self.alloc, .{
                .value = @intCast(case_val),
                .target = arm_blocks.items[i],
                .args = &.{},
            }) catch unreachable;
        } else {
            // Enum/value match: resolve variant name to actual tag value
            arm_tag_values.append(self.alloc, &.{}) catch unreachable;
            const case_val: u64 = blk: {
                const pat_name = switch (pat.data) {
                    .enum_literal => |el| el.name,
                    .identifier => |id| id.name,
                    .int_literal => |il| break :blk @intCast(il.value),
                    .bool_literal => |bl| break :blk @as(u64, if (bl.value) 1 else 0),
                    else => break :blk @as(u64, @intCast(i)),
                };
                // Look up variant value in the subject's type
                if (!subject_ty.isBuiltin()) {
                    const ty_info = self.module.types.get(subject_ty);
                    if (ty_info == .tagged_union) {
                        for (ty_info.tagged_union.fields, 0..) |f, vi| {
                            const vname = self.module.types.strings.get(f.name);
                            if (std.mem.eql(u8, vname, pat_name)) {
                                if (ty_info.tagged_union.explicit_tag_values) |vals| {
                                    if (vi < vals.len) break :blk @intCast(@as(u64, @bitCast(vals[vi])));
                                }
                                break :blk @intCast(vi);
                            }
                        }
                        if (self.diagnostics) |diags| {
                            const ty_name = self.formatTypeName(subject_ty);
                            diags.addFmt(.err, pat.span, "no variant '{s}' on type '{s}'", .{ pat_name, ty_name });
                        }
                    } else if (ty_info == .@"enum") {
                        for (ty_info.@"enum".variants, 0..) |v, vi| {
                            const vname = self.module.types.strings.get(v);
                            if (std.mem.eql(u8, vname, pat_name)) {
                                if (ty_info.@"enum".explicit_values) |vals| {
                                    if (vi < vals.len) break :blk @intCast(@as(u64, @bitCast(vals[vi])));
                                }
                                break :blk @intCast(vi);
                            }
                        }
                        if (self.diagnostics) |diags| {
                            const ty_name = self.formatTypeName(subject_ty);
                            diags.addFmt(.err, pat.span, "no variant '{s}' on type '{s}'", .{ pat_name, ty_name });
                        }
                    } else if (ty_info == .error_set) {
                        // `case .X` matches the global tag id of `X`.
                        break :blk @intCast(self.module.types.internTag(pat_name));
                    }
                }
                break :blk @intCast(i);
            };
            cases.append(self.alloc, .{
                .value = @intCast(case_val),
                .target = arm_blocks.items[i],
                .args = &.{},
            }) catch unreachable;
        }
    }

    // If no default arm, create an unreachable default
    if (default_bb == null) {
        default_bb = self.freshBlock("match.unr");
    }

    // Switch on the subject (for type match, subject is either a
    // bare TypeId (i64) or an Any-shaped Type value — unbox in the
    // latter case so the switch sees the i64 type id).
    const tag = if (is_type_match) tag_blk: {
        if (subject_ty == .any) {
            break :tag_blk self.builder.emit(.{ .unbox_any = .{ .operand = subject } }, .i64);
        }
        break :tag_blk subject;
    } else if (is_optional_match) self.builder.emit(.{ .optional_has_value = .{ .operand = subject } }, .bool) else if (is_error_set_match) subject else blk: {
        // Determine actual tag type from union info (e.g. u32 for SDL_Event)
        const tag_ty: TypeId = tt: {
            if (!subject_ty.isBuiltin()) {
                const ty_info = self.module.types.get(subject_ty);
                if (ty_info == .tagged_union) break :tt ty_info.tagged_union.tag_type;
            }
            break :tt .i32;
        };
        break :blk self.builder.enumTag(subject, tag_ty);
    };
    self.builder.switchBr(tag, cases.items, default_bb.?, &.{});

    // Lower each arm's body
    for (me.arms, 0..) |arm, i| {
        self.builder.switchToBlock(arm_blocks.items[i]);

        // For type-match arms with empty tag lists, the arm is unreachable
        // (no switch case targets it). Skip lowering to avoid invalid IR
        // from runtime cast/dispatch with no matching types.
        if (is_type_match and arm.pattern != null and arm_tag_values.items[i].len == 0) {
            self.builder.emitUnreachable();
            continue;
        }

        var arm_scope = Scope.init(self.alloc, self.scope);
        const old_scope = self.scope;
        self.scope = &arm_scope;

        if (arm.capture) |capture_name| {
            if (is_optional_match) {
                // For optional match, unwrap the optional value
                const opt_info = self.module.types.get(subject_ty);
                const child_ty = if (opt_info == .optional) opt_info.optional.child else .i64;
                const unwrapped = self.builder.emit(.{ .optional_unwrap = .{ .operand = subject } }, child_ty);
                arm_scope.put(capture_name, .{ .ref = unwrapped, .ty = child_ty, .is_alloca = false });
            } else {
                // Resolve actual variant index and payload type from the subject's type
                var variant_idx: u32 = @intCast(i);
                var payload_ty: TypeId = .unresolved;
                if (arm.pattern) |arm_pat| {
                    const pat_name = switch (arm_pat.data) {
                        .enum_literal => |el| el.name,
                        .identifier => |id| id.name,
                        else => "",
                    };
                    if (!subject_ty.isBuiltin()) {
                        const ty_info = self.module.types.get(subject_ty);
                        if (ty_info == .tagged_union) {
                            for (ty_info.tagged_union.fields, 0..) |f, vi| {
                                const vname = self.module.types.strings.get(f.name);
                                if (std.mem.eql(u8, vname, pat_name)) {
                                    variant_idx = @intCast(vi);
                                    payload_ty = f.ty;
                                    break;
                                }
                            }
                        }
                    }
                }
                const payload = self.builder.emit(.{ .enum_payload = .{
                    .base = subject,
                    .field_index = variant_idx,
                } }, payload_ty);
                arm_scope.put(capture_name, .{ .ref = payload, .ty = payload_ty, .is_alloca = false });
            }
        }

        // Set match arm context for runtime type dispatch
        const saved_match_tags = self.current_match_tags;
        if (is_type_match) {
            self.current_match_tags = arm_tag_values.items[i];
        }

        if (has_value_merge) {
            // Lower the arm body against the merge's result type so literals
            // (and negated literals) in the arm pick the right width — the
            // phi operands must all match `result_type`.
            const saved_arm_target = self.target_type;
            self.target_type = result_type;
            const maybe_v = self.lowerBlockValue(arm.body);
            self.target_type = saved_arm_target;
            self.current_match_tags = saved_match_tags;
            self.scope = old_scope;
            arm_scope.deinit();
            // Only materialize a value + branch to the merge when the arm
            // body did NOT diverge. A diverging arm (e.g. `return x`) has
            // already terminated its block; emitting the fallback const
            // here would land AFTER the terminator .
            if (!self.currentBlockHasTerminator()) {
                var v = maybe_v orelse if (result_type == .string or !result_type.isBuiltin())
                    self.builder.constUndef(result_type)
                else
                    self.builder.constInt(0, result_type);
                const v_ty = self.builder.getRefType(v);
                v = self.coerceToType(v, v_ty, result_type);
                self.builder.br(merge_bb, &.{v});
            }
        } else {
            self.lowerBlock(arm.body);
            self.current_match_tags = saved_match_tags;
            self.scope = old_scope;
            arm_scope.deinit();
            if (!self.currentBlockHasTerminator()) {
                self.builder.br(merge_bb, &.{});
            }
        }
    }

    // Emit default block if no explicit else arm
    if (default_bb != null) {
        var found_default = false;
        for (me.arms) |arm| {
            if (arm.pattern == null) { found_default = true; break; }
        }
        if (!found_default) {
            self.builder.switchToBlock(default_bb.?);
            if (is_type_match) {
                // For type-category matches, unrecognized tags should skip to merge
                // (e.g., optional types not covered by any_to_string categories)
                if (has_value_merge) {
                    const default_val = self.builder.constUndef(result_type);
                    self.builder.br(merge_bb, &.{default_val});
                } else {
                    self.builder.br(merge_bb, &.{});
                }
            } else {
                // For non-exhaustive matches (union/enum with unhandled variants),
                // fall through to merge instead of unreachable
                const is_exhaustive = blk: {
                    if (!subject_ty.isBuiltin()) {
                        const ty_info = self.module.types.get(subject_ty);
                        if (ty_info == .tagged_union) {
                            break :blk cases.items.len >= ty_info.tagged_union.fields.len;
                        } else if (ty_info == .@"enum") {
                            break :blk cases.items.len >= ty_info.@"enum".variants.len;
                        }
                    }
                    break :blk false;
                };
                if (is_exhaustive) {
                    self.builder.emitUnreachable();
                } else if (has_value_merge) {
                    const default_val = self.builder.constUndef(result_type);
                    self.builder.br(merge_bb, &.{default_val});
                } else {
                    self.builder.br(merge_bb, &.{});
                }
            }
        }
    }

    self.builder.switchToBlock(merge_bb);
    if (has_value_merge) {
        return self.builder.blockParam(merge_bb, 0, result_type);
    }
    return self.builder.constInt(0, .void);
}

pub fn lowerBreak(self: *Lowering, span: ast.Span) Ref {
    if (self.break_target) |target| {
        // Leaving the loop body's scope: run the defers registered since the
        // loop began (LIFO) before the jump — same as the fall-through exit.
        self.emitLoopExitDefers();
        self.builder.br(target, &.{});
    } else if (self.diagnostics) |d| {
        d.addFmt(.err, span, "`break` outside a loop", .{});
    }
    return Ref.none;
}

pub fn lowerContinue(self: *Lowering, span: ast.Span) Ref {
    if (self.continue_target) |target| {
        self.emitLoopExitDefers();
        self.builder.br(target, &.{});
    } else if (self.diagnostics) |d| {
        d.addFmt(.err, span, "`continue` outside a loop", .{});
    }
    return Ref.none;
}

// ── Block plumbing ──────────────────────────────────────────────

pub fn freshBlock(self: *Lowering, prefix: []const u8) BlockId {
    return self.freshBlockWithParams(prefix, &.{});
}

pub fn freshBlockWithParams(self: *Lowering, prefix: []const u8, params: []const TypeId) BlockId {
    var buf: [64]u8 = undefined;
    const name = std.fmt.bufPrint(&buf, "{s}.{d}", .{ prefix, self.block_counter }) catch prefix;
    self.block_counter += 1;
    const name_id = self.module.types.internString(name);
    return self.builder.appendBlock(name_id, params);
}

pub fn currentBlockHasTerminator(self: *Lowering) bool {
    const func = self.builder.module.getFunctionMut(self.builder.func.?);
    const block_idx = self.builder.current_block orelse return true;
    const block = &func.blocks.items[block_idx.index()];
    if (block.insts.items.len > 0) {
        const last_op = block.insts.items[block.insts.items.len - 1].op;
        return switch (last_op) {
            .ret, .ret_void, .br, .cond_br, .switch_br, .@"unreachable" => true,
            else => false,
        };
    }
    return false;
}

pub fn ensureTerminator(self: *Lowering, ret_ty: TypeId) void {
    if (self.currentBlockHasTerminator()) return;
    if (ret_ty == .noreturn) {
        // A `-> noreturn` function never returns; if control reaches the
        // end of the body it's genuinely unreachable (the body is expected
        // to diverge — call another noreturn, loop forever, etc.).
        self.builder.emitUnreachable();
    } else if (ret_ty == .void) {
        self.builder.retVoid();
    } else {
        // Use const_undef for complex types (string, struct, etc.)
        const default_val = if (ret_ty == .string or !ret_ty.isBuiltin())
            self.builder.constUndef(ret_ty)
        else
            self.builder.constInt(0, ret_ty);
        self.builder.ret(default_val, ret_ty);
    }
}
