const std = @import("std");
const ast = @import("../../ast.zig");
const Node = ast.Node;
const types = @import("../types.zig");
const mod_mod = @import("../module.zig");
const type_bridge = @import("../type_bridge.zig");
const program_index_mod = @import("../program_index.zig");
const resolver_mod = @import("../resolver.zig");
const StructTemplate = program_index_mod.StructTemplate;
const TemplateParam = program_index_mod.TemplateParam;

const TypeId = types.TypeId;
const StringId = types.StringId;
const Module = mod_mod.Module;


const lower = @import("../lower.zig");
const Lowering = lower.Lowering;
const VisibleStructAuthor = Lowering.VisibleStructAuthor;
const structDeclOfRaw = Lowering.structDeclOfRaw;

/// Register a struct declaration's fields and methods in the IR type table.
/// Register a `Foo :: error { A, B }` declaration as an error-set type.
/// Rejects an empty set here (sema gate) since type_bridge has no
/// diagnostics; non-empty sets are interned via type_bridge.
pub fn registerErrorSetDecl(self: *Lowering, node: *const Node) void {
    const esd = node.data.error_set_decl;
    if (esd.tag_names.len == 0) {
        if (self.diagnostics) |diags| {
            diags.addFmt(.err, node.span, "error set '{s}' must declare at least one tag", .{esd.name});
        }
        return;
    }
    _ = type_bridge.resolveAstType(node, &self.module.types, &self.program_index.type_alias_map, &self.program_index.module_const_map);
}

/// The `nominal_id` stamped on a nominal `TypeInfo` (0 for non-nominal /
/// structural). Reading it back lets a re-registration preserve the slot's
/// existing key when refreshing a forward-stubbed body.
pub fn nominalIdOf(info: types.TypeInfo) u32 {
    return switch (info) {
        .@"struct" => |s| s.nominal_id,
        .@"enum" => |e| e.nominal_id,
        .@"union" => |u| u.nominal_id,
        .tagged_union => |u| u.nominal_id,
        .error_set => |e| e.nominal_id,
        else => 0,
    };
}

/// Return `info` with its nominal arm's `nominal_id` set to `nid` (a no-op for
/// non-nominal infos). Used to build the key-matching body for
/// `updatePreservingKey` after a shadow author interned at a nonzero id.
pub fn stampNominalId(info: types.TypeInfo, nid: u32) types.TypeInfo {
    var out = info;
    switch (out) {
        .@"struct" => |*s| s.nominal_id = nid,
        .@"enum" => |*e| e.nominal_id = nid,
        .@"union" => |*u| u.nominal_id = nid,
        .tagged_union => |*u| u.nominal_id = nid,
        .error_set => |*e| e.nominal_id = nid,
        else => {},
    }
    return out;
}

/// Reserve a GENUINE same-name STRUCT shadow author's DISTINCT nominal slot
/// BEFORE any field resolves, so a self / forward / mutual reference to a shadow
/// name (`next: *Box`; `peer: *Node` where Node is a shadow declared later)
/// binds to ITS nominal TypeId via `type_decl_tids` instead of the global
/// findByName first-author fallback. Called only from the
/// `scanDecls` genuine-shadow pass, which has already established that ≥2
/// distinct struct decls author this name; ALL of them reserve — the FIRST at
/// id 0, the rest at fresh nonzero ids — so none falls through to the name-only
/// `findByName` (which, once a shadow is interned, no longer uniquely identifies
/// the first author). Idempotent per decl key: an already-reserved decl returns
/// before re-invoking `shadowNominalId`, so the shadow id is computed once.
/// Generic templates resolve lazily on instantiation and are skipped.
pub fn reserveShadowStructSlot(self: *Lowering, sd: *const ast.StructDecl) void {
    if (sd.type_params.len > 0) return;
    const table = &self.module.types;
    const decl_key: *const anyopaque = @ptrCast(sd);
    if (table.type_decl_tids.contains(decl_key)) return;
    const name_id = table.internString(sd.name);
    const nominal_id = self.shadowNominalId(name_id); // 0 for the first author, nonzero for the rest
    const reserved = table.internNominal(.{ .@"struct" = .{ .name = name_id, .fields = &.{} } }, nominal_id);
    table.type_decl_tids.put(decl_key, reserved) catch {};
}

/// Reserve a GENUINE same-name ENUM shadow author's DISTINCT nominal slot
/// up-front — the enum twin of `reserveShadowStructSlot` (E6a). The reserved
/// slot's KIND MUST match what `buildEnumInfo` will produce (a payload enum →
/// `.tagged_union`, a payload-less enum → `.enum`), because `internNamedTypeDecl`
/// later refreshes the body via `updatePreservingKey`, whose key-stability
/// assert compares the FULL info tag — a struct/enum/tagged_union mismatch would
/// trip it. The empty body and placeholder `tag_type` are not part of the intern
/// key (name + nominal id only), so the real body fills in freely.
pub fn reserveShadowEnumSlot(self: *Lowering, ed: *const ast.EnumDecl) void {
    const table = &self.module.types;
    const decl_key: *const anyopaque = @ptrCast(ed);
    if (table.type_decl_tids.contains(decl_key)) return;
    const name_id = table.internString(ed.name);
    const nominal_id = self.shadowNominalId(name_id);
    const empty: types.TypeInfo = if (ed.variant_types.len > 0)
        .{ .tagged_union = .{ .name = name_id, .fields = &.{}, .tag_type = .i64 } }
    else
        .{ .@"enum" = .{ .name = name_id, .variants = &.{} } };
    const reserved = table.internNominal(empty, nominal_id);
    table.type_decl_tids.put(decl_key, reserved) catch {};
}

/// Reserve a GENUINE same-name UNION shadow author's DISTINCT nominal slot
/// up-front — the union twin of `reserveShadowStructSlot` (E6a).
pub fn reserveShadowUnionSlot(self: *Lowering, ud: *const ast.UnionDecl) void {
    const table = &self.module.types;
    const decl_key: *const anyopaque = @ptrCast(ud);
    if (table.type_decl_tids.contains(decl_key)) return;
    const name_id = table.internString(ud.name);
    const nominal_id = self.shadowNominalId(name_id);
    const reserved = table.internNominal(.{ .@"union" = .{ .name = name_id, .fields = &.{} } }, nominal_id);
    table.type_decl_tids.put(decl_key, reserved) catch {};
}

/// A top-level NAMED type decl the genuine-shadow scan tracks, KIND-tagged so
/// same-name authors of DIFFERENT kinds (a `struct Foo` and an `enum Foo`) are
/// NOT mistaken for one shadow group. Carries the stable decl pointer (the
/// `decl_key` / raw-facts identity) so the scan de-dups by decl identity, and
/// dispatches the per-kind reservation. Later E6 sub-steps add their kind here.
const ShadowTypeDecl = union(enum) {
    @"struct": *const ast.StructDecl,
    @"enum": *const ast.EnumDecl,
    @"union": *const ast.UnionDecl,

    pub fn key(self: ShadowTypeDecl) *const anyopaque {
        return switch (self) {
            inline else => |p| @ptrCast(p),
        };
    }
    pub fn name(self: ShadowTypeDecl) []const u8 {
        return switch (self) {
            inline else => |p| p.name,
        };
    }
    pub fn isGeneric(self: ShadowTypeDecl) bool {
        return switch (self) {
            .@"struct" => |p| p.type_params.len > 0,
            else => false,
        };
    }
};

/// Classify a top-level node as the NAMED type decl it authors — a bare
/// `struct`/`enum`/`union` node, or a `const_decl` whose value is one — so the
/// genuine-shadow scan enumerates all three kinds uniformly. Null when the node
/// is not a struct/enum/union author. The shared infra E6b/E6c extend by adding
/// their kind here.
pub fn topLevelTypeDecl(decl: *const Node) ?ShadowTypeDecl {
    return switch (decl.data) {
        .struct_decl => .{ .@"struct" = &decl.data.struct_decl },
        .enum_decl => .{ .@"enum" = &decl.data.enum_decl },
        .union_decl => .{ .@"union" = &decl.data.union_decl },
        .const_decl => |cd| switch (cd.value.data) {
            .struct_decl => .{ .@"struct" = &cd.value.data.struct_decl },
            .enum_decl => .{ .@"enum" = &cd.value.data.enum_decl },
            .union_decl => .{ .@"union" = &cd.value.data.union_decl },
            else => null,
        },
        else => null,
    };
}

/// Dispatch a genuine-shadow reservation to the matching per-kind reserver.
pub fn reserveShadowSlot(self: *Lowering, td: ShadowTypeDecl) void {
    switch (td) {
        .@"struct" => |sd| self.reserveShadowStructSlot(sd),
        .@"enum" => |ed| self.reserveShadowEnumSlot(ed),
        .@"union" => |ud| self.reserveShadowUnionSlot(ud),
    }
}

/// Register (or re-register) a top-level NAMED type decl under a per-source
/// nominal identity (E2), returning its TypeId. `decl_key` is the decl's
/// stable pointer (the import raw-facts identity); `info` carries the full
/// body; `nominal_id` is the slot's identity (0 for a single / first author,
/// nonzero for a later same-name shadow) — computed once by the caller
/// (`registerStructDecl`), which reuses the id reserved up-front in `scanDecls`
/// for a genuine shadow (so its fields' self / forward / mutual refs already
/// resolved against it). This stamps the id and records the `decl_key → TypeId`
/// map (`type_decl_tids`, the `fn_decl_fids` analogue).
///
/// A `nominal_id == 0` author adopts any forward-reference stub (`findByName`
/// orelse intern) — BYTE-IDENTICAL to pre-E2 registration. For a genuinely
/// multi-authored name, the FIRST source keeps id 0 and later sources get
/// fresh ids → DISTINCT TypeIds, so the authors no longer collapse last-wins
///. Idempotent per `decl_key`: a re-registration — OR an up-front
/// shadow reservation — reuses the recorded slot, refreshing its body via
/// `updatePreservingKey` (key-stable because a struct's intern key is its
/// name + nominal id, not its fields).
pub fn internNamedTypeDecl(self: *Lowering, decl_key: *const anyopaque, name_id: types.StringId, info: types.TypeInfo, nominal_id: u32) TypeId {
    const table = &self.module.types;
    // Slot already recorded (re-registration, or a reserve-before-fields shadow
    // reservation) → reuse its slot + nominal id, refresh the body.
    if (table.type_decl_tids.get(decl_key)) |existing_id| {
        table.updatePreservingKey(existing_id, stampNominalId(info, nominalIdOf(table.get(existing_id))));
        return existing_id;
    }
    const id = if (nominal_id == 0)
        (table.findByName(name_id) orelse table.internNominal(info, 0))
    else
        table.internNominal(info, nominal_id);
    const stamped = stampNominalId(info, nominal_id);
    // A self / mutual `*Name` field in an enum/union body forward-creates a
    // STRUCT placeholder under `Name` (the stateless resolver has no kind
    // context — `type_resolver.resolveNamed` always stubs a struct), which the
    // `findByName` above then returns. Adopting a wrong-kind stub needs a
    // re-key, NOT the in-place `updatePreservingKey` body-fill — whose
    // kind-stability assert trips on struct→enum/union.
    if (adoptsForwardStructStub(table.get(id), stamped))
        table.replaceKeyedInfo(id, stamped)
    else
        table.updatePreservingKey(id, stamped);
    table.type_decl_tids.put(decl_key, id) catch {};
    return id;
}

/// TRUE when `existing` is a forward-reference STRUCT placeholder (empty
/// fields — the stateless resolver's stub for an as-yet-unregistered name) and
/// `incoming` is a NON-struct nominal (enum / union / tagged_union): the one
/// case where `internNamedTypeDecl` must re-key the slot rather than fill its
/// body in place. A struct adopting its own struct stub is same-kind and stays
/// on `updatePreservingKey`; a fresh-interned slot has no stub to adopt.
pub fn adoptsForwardStructStub(existing: types.TypeInfo, incoming: types.TypeInfo) bool {
    if (existing != .@"struct" or existing.@"struct".fields.len != 0) return false;
    return switch (incoming) {
        .@"enum", .@"union", .tagged_union => true,
        else => false,
    };
}

/// The `nominal_id` to register a NAMED type author of `name_id` under. 0
/// unless `name_id` is authored as a named type by ≥2 distinct modules (a real
/// same-name shadow per the import facts): the FIRST source to register keeps
/// 0, each later source gets a fresh monotonic id. Gating on the import facts
/// keeps the single-author path at id 0 (byte-identical) even when one logical
/// type is re-registered from several `current_source_file` contexts.
pub fn shadowNominalId(self: *Lowering, name_id: types.StringId) u32 {
    if (!self.nameHasMultipleTypeAuthors(self.module.types.getString(name_id))) return 0;
    const src = self.current_source_file orelse self.main_file orelse "";
    const gop = self.nominal_name_authors.getOrPut(name_id) catch return 0;
    if (!gop.found_existing) {
        gop.value_ptr.* = src;
        return 0;
    }
    if (std.mem.eql(u8, gop.value_ptr.*, src)) return 0;
    self.next_nominal_id += 1;
    return self.next_nominal_id;
}

/// TRUE iff `name` is authored AS A NAMED TYPE (struct / enum / union /
/// error-set / protocol / runtime class) by ≥2 DISTINCT modules in the import
/// raw facts — the authoritative same-name-shadow signal (the only case where
/// distinct `nominal_id`s are needed). Module distinctness is by LEXICALLY
/// NORMALIZED path: one logical file reached through several spellings
/// (`testpkg/../allocators.sx` vs `allocators.sx`) is cached — and so parsed —
/// twice, landing two `module_decls` entries with two decl pointers for the
/// SAME source; normalizing collapses them to one author, NOT a false shadow.
/// False when the facts are unwired (comptime / registration host with no
/// `module_decls`): the single-author path applies, correct there.
pub fn nameHasMultipleTypeAuthors(self: *Lowering, name: []const u8) bool {
    const decls = self.program_index.module_decls orelse return false;
    var first_norm: ?[]const u8 = null;
    defer if (first_norm) |f| self.alloc.free(f);
    var it = decls.iterator();
    while (it.next()) |entry| {
        const m = entry.value_ptr;
        const ref = m.names.get(name) orelse continue;
        if (rawNamedTypePtr(ref) == null) continue;
        const norm = std.fs.path.resolvePosix(self.alloc, &.{entry.key_ptr.*}) catch continue;
        if (first_norm) |f| {
            defer self.alloc.free(norm);
            if (!std.mem.eql(u8, f, norm)) return true;
        } else {
            first_norm = norm;
        }
    }
    return false;
}

/// The opaque decl-pointer identity of a NAMED-type `RawDeclRef`, or null when
/// the ref is not a named type (fn / value-const / namespace alias). Used to
/// de-dup same-name authors by decl identity.
pub fn rawNamedTypePtr(ref: resolver_mod.RawDeclRef) ?*const anyopaque {
    return switch (ref) {
        .struct_decl => |d| @ptrCast(d),
        .enum_decl => |d| @ptrCast(d),
        .union_decl => |d| @ptrCast(d),
        .error_set_decl => |d| @ptrCast(d),
        .protocol_decl => |d| @ptrCast(d),
        .runtime_class_decl => |d| @ptrCast(d),
        .fn_decl, .const_decl, .var_decl, .namespace_decl => null,
    };
}

/// Build an owned generic-struct template (type params, field names, field
/// type nodes) for `sd`, pinned to its declaring `source_file`. The returned
/// template is heap-owned via `self.alloc`; callers register it under a bare
/// or namespace-qualified key. Null on OOM.
pub fn buildGenericStructTemplate(self: *Lowering, sd: *const ast.StructDecl, source_file: ?[]const u8) ?StructTemplate {
    const owned_name = self.alloc.dupe(u8, sd.name) catch return null;

    const tps = self.alloc.alloc(TemplateParam, sd.type_params.len) catch return null;
    for (sd.type_params, 0..) |tp, i| {
        const is_type_param = tp.is_variadic or (if (tp.constraint.data == .type_expr) blk: {
            const cname = tp.constraint.data.type_expr.name;
            // "Type" or a protocol name → type param
            break :blk std.mem.eql(u8, cname, "Type") or
                self.program_index.protocol_decl_map.contains(cname) or
                self.program_index.protocol_ast_map.contains(cname);
        } else false);
        tps[i] = .{
            .name = self.alloc.dupe(u8, tp.name) catch return null,
            // $T: Type, $T: Lerpable, $T: Type/Eq — all are type params.
            // `..$Ts: []Type` (variadic) is a type-pack param. Only value
            // params like $N: u32 are non-type.
            .is_type_param = is_type_param,
            .is_variadic = tp.is_variadic,
            // Capture a value param's declared type name (`$K: u32` →
            // "u32") so instantiation can range-check the folded arg.
            .value_type = if (!is_type_param and tp.constraint.data == .type_expr)
                (self.alloc.dupe(u8, tp.constraint.data.type_expr.name) catch null)
            else
                null,
        };
    }

    const fnames = self.alloc.alloc([]const u8, sd.field_names.len) catch return null;
    for (sd.field_names, 0..) |fn_str, i| {
        fnames[i] = self.alloc.dupe(u8, fn_str) catch return null;
    }

    // Field type nodes are *Node pointers into the AST; copy the slice of
    // pointers (the nodes themselves are heap-allocated).
    const ftype_nodes = self.alloc.dupe(*const Node, sd.field_types) catch return null;

    return .{
        .name = owned_name,
        .type_params = tps,
        .field_names = fnames,
        .field_type_nodes = ftype_nodes,
        .source_file = source_file,
        .decl = sd,
    };
}

/// Select the generic struct template AUTHORED by namespace `alias`'s target
/// module (the `importer → alias → NamespaceTarget` edge), not the bare
/// last-wins `struct_template_map`. A qualified head `ns.Box(..)` must
/// instantiate ns's OWN `Box`, even when another module's same-name `Box` won
/// the bare map. Null when the alias is unknown in the current source or its
/// module authors no such generic struct — the caller then falls back to the
/// legacy bare lookup.
pub fn qualifiedStructTemplate(self: *Lowering, alias: []const u8, member: []const u8) ?StructTemplate {
    const target = self.namespaceAliasTarget(alias, null) orelse return null;
    for (target.own_decls) |decl| {
        // A top-level struct is authored either as a bare `struct_decl` node
        // or a `const_decl` whose value is one (`Box :: struct($T){...}`).
        const sd: *const ast.StructDecl = switch (decl.data) {
            .struct_decl => |*s| s,
            .const_decl => |cd| if (cd.value.data == .struct_decl) &cd.value.data.struct_decl else continue,
            else => continue,
        };
        if (!std.mem.eql(u8, sd.name, member)) continue;
        if (sd.type_params.len == 0) continue;
        return self.buildGenericStructTemplate(sd, decl.source_file orelse target.target_module_path);
    }
    return null;
}

/// TRUE iff `alias` is a KNOWN namespace in the current source but its target
/// module authors NO member named `member` at all. A qualified generic head
/// `a.Box(..)` whose namespace lacks `Box` must diagnose the missing member —
/// never silently fall back to the bare last-wins `struct_template_map` (which
/// would instantiate an unrelated module's same-name `Box`, E4 finding #2).
/// FALSE when `alias` is not a namespace at all (leave the caller's existing
/// non-namespace handling), or when the namespace DOES author `member` (a
/// generic struct → `qualifiedStructTemplate` already selected it; any other
/// kind → the type-fn / named-type arms handle it).
pub fn qualifiedMemberMissing(self: *Lowering, alias: []const u8, member: []const u8) bool {
    const target = self.namespaceAliasTarget(alias, null) orelse return false;
    for (target.own_decls) |decl| {
        const dn = decl.data.declName() orelse continue;
        if (std.mem.eql(u8, dn, member)) return false;
    }
    return true;
}

/// The `*ConstDecl` a raw author wraps when it is a const ALIAS of another
/// name — `BoxAlias :: Box;` (identifier RHS) or `Box :: r.Box;` (namespace-
/// member RHS). Null for every other shape, including const-wrapped struct /
/// fn DEFINITIONS, which are authors in their own right.
fn constAliasOfRaw(ref: resolver_mod.RawDeclRef) ?*const ast.ConstDecl {
    return switch (ref) {
        .const_decl => |cd| switch (cd.value.data) {
            .identifier, .field_access => cd,
            else => null,
        },
        else => null,
    };
}

/// The single author of `name` as seen from `from` — own wins, else exactly
/// one flat-import author. Null when absent or when ≥2 flat authors compete
/// (the use site then diagnoses the unresolved head; no silent pick).
fn singleVisibleAuthor(self: *Lowering, name: []const u8, from: []const u8) ?resolver_mod.RawAuthor {
    var res = self.resolver();
    const set = res.collectVisibleAuthors(name, from, .user_bare_flat);
    defer if (set.flat.len > 0) self.alloc.free(set.flat);
    if (set.own) |o| return o;
    if (set.flat.len == 1) return set.flat[0];
    return null;
}

/// Resolve `name`, as seen from `from`, to a generic-struct template by
/// following const ALIAS declarations (issue 0120). Entry for the head
/// selector's bare tail: the FIRST hop must be alias-shaped — a direct
/// struct author is the template map's business, never this path's. Each
/// hop resolves from the ALIAS AUTHOR's source, so visibility is the
/// author's, not the use site's (a consumer one flat hop from a facade
/// reaches the facade's `Box :: r.Box;` without seeing `r` itself).
pub fn aliasedStructTemplate(self: *Lowering, name: []const u8, from: []const u8) ?StructTemplate {
    const author = singleVisibleAuthor(self, name, from) orelse return null;
    if (constAliasOfRaw(author.raw) == null) return null;
    return followToTemplate(self, author, 8);
}

/// One alias hop: a generic-struct author terminates the chain with its
/// rebuilt source-pinned template; an alias author recurses via
/// `followAliasChain`.
fn followToTemplate(self: *Lowering, author: resolver_mod.RawAuthor, depth: u8) ?StructTemplate {
    const terminal = followAliasChain(self, author, depth) orelse return null;
    const sd = structDeclOfRaw(terminal.raw) orelse return null;
    if (sd.type_params.len == 0) return null;
    return self.buildGenericStructTemplate(sd, terminal.source);
}

/// Walk a chain of const ALIAS decls to its terminal author. Each hop
/// resolves the RHS from the hop AUTHOR's own source — a bare identifier
/// via the visible-author walk, `ns.X` through the author's namespace edge
/// into the target module's own member. A non-alias author terminates the
/// chain (callers unwrap it by domain: `structDeclOfRaw` / `fnDeclOfRaw`).
/// The depth cap breaks alias cycles (`A :: B; B :: A;`).
pub fn followAliasChain(self: *Lowering, author: resolver_mod.RawAuthor, depth: u8) ?resolver_mod.RawAuthor {
    if (depth == 0) return null;
    const cd = constAliasOfRaw(author.raw) orelse return author;
    const next: ?resolver_mod.RawAuthor = switch (cd.value.data) {
        .identifier => |id| singleVisibleAuthor(self, id.name, author.source),
        .field_access => |fa| blk: {
            if (fa.object.data != .identifier) break :blk null;
            const target = switch (self.namespaceAliasVerdictFrom(fa.object.data.identifier.name, author.source)) {
                .target => |t| t,
                .none, .ambiguous => break :blk null,
            };
            var res = self.resolver();
            const member_set = res.collectNamespaceAuthors(target, fa.field);
            break :blk member_set.own;
        },
        else => null,
    };
    return followAliasChain(self, next orelse return null, depth - 1);
}

/// The fn decl a const ALIAS chain terminates at, or null when `cd` is not
/// an alias of a function. Entry for fn-alias registration (issue 0121):
/// `cd` itself seeds the chain (it IS the first alias hop), `from` is its
/// declaring source.
pub fn aliasedFnDecl(self: *Lowering, cd: *const ast.ConstDecl, from: []const u8) ?*const ast.FnDecl {
    const terminal = followAliasChain(self, .{ .raw = .{ .const_decl = cd }, .source = from }, 9) orelse return null;
    return Lowering.fnDeclOfRaw(terminal.raw);
}

/// The bare-VISIBLE single generic-struct author of `name` (its `StructDecl` +
/// defining source) when that author is NOT the one the global last-wins
/// `struct_template_map` already holds — the E4 non-transitive selection for a
/// bare generic head / alias / static-method head whose visible author (own or
/// a single 1-hop flat import) is shadowed in the global map by a NON-visible
/// (≥2-flat-hop) same-name template (finding #1). Exposing the decl (not just a
/// rebuilt template) lets a static-method head source-pin the METHOD body too,
/// not only the type layout. Null — caller uses the global map unchanged
/// (byte-identical) — when: no source context; the single visible author IS the
/// canonical map author (the common single-author case, matched by source
/// file); or the visible picture is not a clean single generic-struct author
/// (own non-generic shadow, or ≥2 flat authors whose ambiguity `headTypeLeak`
/// has already diagnosed + poisoned before this is consulted).
pub fn bareVisibleStructDecl(self: *Lowering, name: []const u8) ?VisibleStructAuthor {
    if (self.emitting_default_context) return null;
    const from = self.current_source_file orelse return null;
    const canon = self.program_index.struct_template_map.get(name) orelse return null;
    const canon_src = canon.source_file orelse "";

    var res_walk = self.resolver();
    const set = res_walk.collectVisibleAuthors(name, from, .user_bare_flat);
    defer if (set.flat.len > 0) self.alloc.free(set.flat);

    // Own author wins — must be a generic struct to count.
    if (set.own) |own| {
        const sd = structDeclOfRaw(own.raw) orelse return null; // alias / fn / other → skip
        if (sd.type_params.len == 0) return null;
        if (std.mem.eql(u8, from, canon_src)) return null;
        return .{ .sd = sd, .source = from };
    }

    // Single flat-import generic-struct author.
    var picked: ?*const ast.StructDecl = null;
    var picked_src: []const u8 = "";
    for (set.flat) |fa| {
        const sd = structDeclOfRaw(fa.raw) orelse continue;
        if (sd.type_params.len == 0) continue;
        if (picked != null) return null; // ≥2 visible authors
        picked = sd;
        picked_src = fa.source;
    }
    const sd = picked orelse return null;
    if (std.mem.eql(u8, picked_src, canon_src)) return null;
    return .{ .sd = sd, .source = picked_src };
}

/// The rebuilt, source-pinned generic struct TEMPLATE of the single bare-VISIBLE
/// author (`bareVisibleStructDecl`) — instantiate this INSTEAD of the global
/// last-wins map entry. Null under the same conditions `bareVisibleStructDecl`
/// returns null (caller keeps the global map, byte-identical).
pub fn bareVisibleStructTemplate(self: *Lowering, name: []const u8) ?StructTemplate {
    const v = self.bareVisibleStructDecl(name) orelse return null;
    return self.buildGenericStructTemplate(v.sd, v.source);
}

/// Instantiate a generic struct template and register the result under an
/// alias name (`Vec3 :: Vec(3, f32)` / `ABox :: a.Box(i64)`). Shared by the
/// `.call` and `.parameterized_type_expr` const-decl alias branches and the
/// qualified-head selection that precedes the bare `struct_template_map`
/// fallback in each.
pub fn registerGenericStructAlias(self: *Lowering, alias_name: []const u8, tmpl: *const StructTemplate, args: []const *const Node) void {
    const inst_id = self.instantiateGenericStruct(tmpl, args);
    const alias_name_id = self.module.types.internString(alias_name);
    const inst_info = self.module.types.get(inst_id);
    if (inst_info != .@"struct") return;
    const alias_info: types.TypeInfo = .{ .@"struct" = .{
        .name = alias_name_id,
        .fields = inst_info.@"struct".fields,
    } };
    const alias_id = if (self.module.types.findByName(alias_name_id)) |existing| existing else self.module.types.intern(alias_info);
    self.module.types.updatePreservingKey(alias_id, alias_info);
    // A generic-struct instantiation alias IS a type author: route it through
    // the unified writer so it lands in `type_aliases_by_source` and the
    // bare-TYPE gate treats it like any other alias.
    self.putTypeAlias(self.current_source_file, alias_name, alias_id);
    // CP-3: the alias display name (`ABox`) is the struct type name a receiver
    // typed `x: ABox` reports, so method dispatch on it looks up the instance
    // maps under `ABox`. Mirror the mangled instance's template/bindings/author
    // onto the alias name so an alias-typed receiver is a first-class dispatch
    // instance (runs the selected author's body + bindings), not a dead end.
    const inst_name = self.formatTypeName(inst_id);
    if (self.struct_instance_author.get(inst_name)) |author_decl| {
        const tmpl_name = self.struct_instance_template.get(inst_name) orelse return;
        const bindings = self.struct_instance_bindings.getPtr(inst_name) orelse return;
        self.struct_instance_template.put(self.alloc.dupe(u8, alias_name) catch return, tmpl_name) catch {};
        self.struct_instance_bindings.put(self.alloc.dupe(u8, alias_name) catch return, bindings.*) catch {};
        self.struct_instance_author.put(self.alloc.dupe(u8, alias_name) catch return, author_decl) catch {};
    }
}

pub fn registerStructDecl(self: *Lowering, sd: *const ast.StructDecl, source_file: ?[]const u8) void {
    const table = &self.module.types;
    const name_id = table.internString(sd.name);

    // Generic structs: store as owned template, don't resolve fields yet
    if (sd.type_params.len > 0) {
        const tmpl = self.buildGenericStructTemplate(sd, source_file) orelse return;
        self.program_index.struct_template_map.put(tmpl.name, tmpl) catch {};

        // S1.1 (additive): key the template by DeclId in parallel. Nothing
        // reads this for selection yet; `struct_template_map` stays the live
        // consumer. A template whose decl is not in the table (comptime /
        // block-local registration with facts unwired) keeps only the
        // name-keyed entry.
        if (self.program_index.decl_table) |dt| {
            if (dt.declIdForStructDecl(sd)) |id| {
                self.program_index.struct_template_by_decl.put(id, tmpl) catch {};
            }
        }

        // Register methods under "TemplateName.method" in fn_ast_map
        for (sd.methods) |method_node| {
            if (method_node.data == .fn_decl) {
                const method_fd = &method_node.data.fn_decl;
                const qualified = std.fmt.allocPrint(self.alloc, "{s}.{s}", .{ sd.name, method_fd.name }) catch continue;
                self.program_index.fn_ast_map.put(qualified, method_fd) catch {};
            }
        }
        return;
    }

    // Per-decl nominal identity (E2). EACH author of a GENUINE same-name STRUCT
    // shadow already reserved its distinct slot up-front in `scanDecls` (the
    // first at id 0, the rest at nonzero ids), so a self / forward / mutual
    // reference to the shadow name bound to ITS nominal TypeId via
    // `type_decl_tids`, not the global findByName first-author fallback (issue
    // 0105 / F1): reuse that reserved id. A single-author name (or a phantom
    // over-counted by the raw import facts) was NOT reserved — it keeps id 0 and
    // the legacy post-field registration, byte-identical to pre-F1.
    // `shadowNominalId` here only fires for the non-scanDecls registration paths
    // (comptime `lowerDecls`, block-local), where module facts are unwired so it
    // returns 0.
    const decl_key: *const anyopaque = @ptrCast(sd);
    const nominal_id: u32 = if (table.type_decl_tids.get(decl_key)) |id| nominalIdOf(table.get(id)) else self.shadowNominalId(name_id);

    // Build field list, expanding #using entries
    var fields = std.ArrayList(types.TypeInfo.StructInfo.Field).empty;
    var field_idx: usize = 0;
    var using_idx: usize = 0;
    const total_explicit = sd.field_names.len;
    while (field_idx < total_explicit or using_idx < sd.using_entries.len) {
        // Insert #using fields at their declared positions
        while (using_idx < sd.using_entries.len and sd.using_entries[using_idx].insert_index == fields.items.len) {
            const ue = sd.using_entries[using_idx];
            const used_name_id = table.internString(ue.type_name);
            if (table.findByName(used_name_id)) |used_ty| {
                const used_info = table.get(used_ty);
                if (used_info == .@"struct") {
                    for (used_info.@"struct".fields) |f| {
                        fields.append(self.alloc, f) catch unreachable;
                    }
                }
            }
            using_idx += 1;
        }
        if (field_idx < total_explicit) {
            const field_ty = self.resolveType(sd.field_types[field_idx]);
            fields.append(self.alloc, .{
                .name = table.internString(sd.field_names[field_idx]),
                .ty = field_ty,
            }) catch unreachable;
            field_idx += 1;
        } else break;
    }
    // Append remaining #using entries after all explicit fields
    while (using_idx < sd.using_entries.len) {
        const ue = sd.using_entries[using_idx];
        const used_name_id = table.internString(ue.type_name);
        if (table.findByName(used_name_id)) |used_ty| {
            const used_info = table.get(used_ty);
            if (used_info == .@"struct") {
                for (used_info.@"struct".fields) |f| {
                    fields.append(self.alloc, f) catch unreachable;
                }
            }
        }
        using_idx += 1;
    }

    // Qualify inline __anon type names: __anon → StructName.field_name
    for (sd.field_names, 0..) |fname, fi| {
        if (fi < fields.items.len) {
            const field_ty = fields.items[fi].ty;
            if (!field_ty.isBuiltin()) {
                self.qualifyAnonType(table, field_ty, sd.name, fname);
            }
        }
    }

    // Register under the per-decl nominal identity computed above. A non-first
    // shadow author's slot was already reserved before fields resolved, so this
    // fills it (key-stable updatePreservingKey); a first / single author adopts
    // any forward-reference stub. Same-name structs in DIFFERENT sources get
    // distinct TypeIds instead of last-wins clobbering the first.
    const info: types.TypeInfo = .{ .@"struct" = .{ .name = name_id, .fields = fields.items } };
    _ = self.internNamedTypeDecl(decl_key, name_id, info, nominal_id);

    // Store field defaults for struct literal lowering
    if (sd.field_defaults.len > 0) {
        var has_any_default = false;
        for (sd.field_defaults) |d| {
            if (d != null) { has_any_default = true; break; }
        }
        if (has_any_default) {
            self.struct_defaults_map.put(sd.name, sd.field_defaults) catch {};
        }
    }

    // Register struct methods as StructName.method in fn_ast_map
    for (sd.methods) |method_node| {
        if (method_node.data == .fn_decl) {
            const method_fd = &method_node.data.fn_decl;
            // Build qualified name: StructName.method
            const qualified = std.fmt.allocPrint(self.alloc, "{s}.{s}", .{ sd.name, method_fd.name }) catch continue;
            self.program_index.fn_ast_map.put(qualified, method_fd) catch {};
            // Declare extern stub (body is lowered lazily on demand)
            self.declareFunction(method_fd, qualified);
        }
    }

    // Register struct-level constants (e.g., GRAVITY :f32: 9.81)
    for (sd.constants) |const_node| {
        if (const_node.data == .const_decl) {
            const cd = const_node.data.const_decl;
            const qualified = std.fmt.allocPrint(self.alloc, "{s}.{s}", .{ sd.name, cd.name }) catch continue;
            const ty: ?TypeId = if (cd.type_annotation) |ta| self.resolveType(ta) else null;
            self.struct_const_map.put(qualified, .{ .value = cd.value, .ty = ty }) catch {};
        }
    }
}

/// Register a top-level ENUM decl under a per-decl nominal identity (E6a) —
/// the enum twin of `registerStructDecl`. A GENUINE same-name shadow already
/// reserved its DISTINCT slot up-front in `scanDecls` (the first at id 0, the
/// rest at nonzero ids), so a forward / self / mutual reference to the shadow
/// name already bound to ITS nominal TypeId via `type_decl_tids`: reuse that
/// reserved id. A single-author name (or one over-counted by the raw facts but
/// not a genuine scanned shadow) was NOT reserved — it keeps id 0 and the legacy
/// post-build registration, byte-identical to pre-E6a. The body is built once by
/// the shared `type_bridge.buildEnumInfo`; `internNamedTypeDecl` interns it under
/// the computed nominal id and records `decl_key → TypeId` so `namedRefTid`
/// resolves bare references to this exact author.
pub fn registerEnumDecl(self: *Lowering, ed: *const ast.EnumDecl) void {
    const table = &self.module.types;
    const name_id = table.internString(ed.name);
    const decl_key: *const anyopaque = @ptrCast(ed);
    const nominal_id: u32 = if (table.type_decl_tids.get(decl_key)) |id| nominalIdOf(table.get(id)) else self.shadowNominalId(name_id);
    // Pass `self` (the visibility-aware `Lowering` resolver) as the `inner`
    // recursion hook — the same seam `resolveCompound` uses — so a payload type
    // NAME resolves in the enum's OWN module visibility context (own author wins
    // over a namespaced same-name import), not via a global `findByName`
    // first-match (issue 0132's class).
    const info = type_bridge.buildEnumInfo(ed, table, self);
    _ = self.internNamedTypeDecl(decl_key, name_id, info, nominal_id);
}

/// Register a top-level UNION decl under a per-decl nominal identity (E6a) —
/// the union twin of `registerEnumDecl` / `registerStructDecl`.
pub fn registerUnionDecl(self: *Lowering, ud: *const ast.UnionDecl) void {
    const table = &self.module.types;
    const name_id = table.internString(ud.name);
    const decl_key: *const anyopaque = @ptrCast(ud);
    const nominal_id: u32 = if (table.type_decl_tids.get(decl_key)) |id| nominalIdOf(table.get(id)) else self.shadowNominalId(name_id);
    // `self` as the visibility-aware `inner` hook — see `registerEnumDecl`.
    const info = type_bridge.buildUnionInfo(ud, table, self);
    _ = self.internNamedTypeDecl(decl_key, name_id, info, nominal_id);
}

/// Rename an __anon type to a qualified name: ParentStruct.field_name
/// Also renames variant payload struct types from __anon.X to ParentStruct.field_name.X
pub fn qualifyAnonType(self: *Lowering, table: *types.TypeTable, ty: TypeId, parent_name: []const u8, field_name: []const u8) void {
    const ti = table.get(ty);
    switch (ti) {
        .@"union" => |u| {
            const old_name = table.getString(u.name);
            if (!std.mem.eql(u8, old_name, "__anon")) return;
            const qualified = std.fmt.allocPrint(self.alloc, "{s}.{s}", .{ parent_name, field_name }) catch return;
            const qname_id = table.internString(qualified);
            table.replaceKeyedInfo(ty, .{ .@"union" = .{ .name = qname_id, .fields = u.fields } });
        },
        .tagged_union => |u| {
            const old_name = table.getString(u.name);
            if (!std.mem.eql(u8, old_name, "__anon")) return;
            const qualified = std.fmt.allocPrint(self.alloc, "{s}.{s}", .{ parent_name, field_name }) catch return;
            const qname_id = table.internString(qualified);
            // Rename variant payload structs: __anon.X → ParentStruct.field.X
            for (u.fields) |f| {
                if (!f.ty.isBuiltin()) {
                    const finfo = table.get(f.ty);
                    if (finfo == .@"struct") {
                        const sname = table.getString(finfo.@"struct".name);
                        if (std.mem.startsWith(u8, sname, "__anon.")) {
                            const suffix = sname["__anon".len..]; // .VariantName
                            const sq = std.fmt.allocPrint(self.alloc, "{s}{s}", .{ qualified, suffix }) catch continue;
                            const sq_id = table.internString(sq);
                            table.replaceKeyedInfo(f.ty, .{ .@"struct" = .{ .name = sq_id, .fields = finfo.@"struct".fields } });
                        }
                    }
                }
            }
            table.replaceKeyedInfo(ty, .{ .tagged_union = .{ .name = qname_id, .fields = u.fields, .tag_type = u.tag_type, .backing_type = u.backing_type, .explicit_tag_values = u.explicit_tag_values } });
        },
        .@"enum" => |e| {
            const old_name = table.getString(e.name);
            if (!std.mem.eql(u8, old_name, "__anon")) return;
            const qualified = std.fmt.allocPrint(self.alloc, "{s}.{s}", .{ parent_name, field_name }) catch return;
            const qname_id = table.internString(qualified);
            table.replaceKeyedInfo(ty, .{ .@"enum" = .{ .name = qname_id, .variants = e.variants, .explicit_values = e.explicit_values } });
        },
        .@"struct" => |s| {
            const old_name = table.getString(s.name);
            if (!std.mem.eql(u8, old_name, "__anon")) return;
            const qualified = std.fmt.allocPrint(self.alloc, "{s}.{s}", .{ parent_name, field_name }) catch return;
            const qname_id = table.internString(qualified);
            table.replaceKeyedInfo(ty, .{ .@"struct" = .{ .name = qname_id, .fields = s.fields } });
        },
        else => {},
    }
}
