const std = @import("std");
const ast = @import("../ast.zig");
const types = @import("types.zig");
const lower = @import("lower.zig");

const Node = ast.Node;
const TypeId = types.TypeId;
const Lowering = lower.Lowering;

/// AST-level expression typing (architecture phase A3.1), extracted from
/// `Lowering.inferExprType`. Owns the structural / non-call expression shapes —
/// literals, unary / binary ops, `try` / `catch`, `if`, block, field access,
/// identifier / type-name, struct / tuple literals, index / slice / deref,
/// null-coalesce, and the statement shapes that produce no value. Call result
/// typing is owned by `CallResolver` (`calls.zig`); a call node reaching here is
/// routed to it via `Lowering.inferExprType`.
///
/// A `*Lowering` facade (Principle 5), like `PackResolver`: expression typing
/// reads live lexical-scope / pack / target-type state and dozens of resolver
/// helpers, so it borrows `Lowering` rather than re-threading every field. The
/// dependency shrinks as later phases lift that state into an explicit context
/// (the plan's `TypeResolver` / `ProgramIndex` / `ResolveEnv` target).
pub const ExprTyper = struct {
    l: *Lowering,

    /// Infer the IR type an expression evaluates to (without lowering it).
    /// Recurses through `Lowering.inferExprType` so a nested call node is typed
    /// by its one owner.
    pub fn inferType(self: ExprTyper, node: *const Node) TypeId {
        return switch (node.data) {
            // Call result typing is owned by `CallResolver` (`calls.zig`);
            // delegate through `Lowering.inferExprType` so a call node reaching
            // here is typed by that single owner, not mistyped as `.unresolved`.
            .call => self.l.inferExprType(node),
            .string_literal => .string,
            .int_literal => .s64,
            .float_literal => .f64,
            .bool_literal => .bool,
            .null_literal => .void,
            .binary_op => |bop| switch (bop.op) {
                .or_op => blk: {
                    // A failable `or` (value-terminator or chain) yields the
                    // chain's success type (the error is absorbed/propagated);
                    // a non-failable `or` is boolean / optional-unwrap → bool.
                    // Detected structurally — a `try`-chain's operands type as
                    // non-failable `T`, so a type-only check would miss it.
                    if (self.l.orIsFailableChain(&bop)) break :blk self.l.orChainSuccessType(&bop);
                    break :blk .bool;
                },
                .eq, .neq, .lt, .lte, .gt, .gte, .and_op, .in_op => .bool,
                else => self.l.inferExprType(bop.lhs),
            },
            .unary_op => |uop| switch (uop.op) {
                .not => .bool,
                .negate => self.l.inferExprType(uop.operand),
                .xx => self.l.target_type orelse .unresolved,
                .address_of => blk: {
                    const inner = self.l.inferExprType(uop.operand);
                    break :blk self.l.module.types.ptrTo(inner);
                },
                else => .unresolved,
            },
            // `try X` evaluates to X's success type (the value part). A
            // pure-failable operand (`-> !` / `-> !Named`, whose type IS the
            // error set) has no value → `void`; a value-carrying `-> (T..., !)`
            // operand yields its value part (the lone value, or a value-tuple).
            .try_expr => |te| blk: {
                const op_ty = self.l.inferExprType(te.operand);
                const channel = self.l.errorChannelOf(op_ty) orelse break :blk .unresolved;
                if (op_ty == channel) break :blk .void;
                break :blk self.l.failableSuccessType(op_ty);
            },
            // `expr catch ...` strips the error channel → the success type
            // (void for a pure-failable LHS; the value part for value-carrying).
            .catch_expr => |ce| blk: {
                const op_ty = self.l.inferExprType(ce.operand);
                const channel = self.l.errorChannelOf(op_ty) orelse break :blk .unresolved;
                if (op_ty == channel) break :blk .void;
                break :blk self.l.failableSuccessType(op_ty);
            },
            .caller_location => self.l.module.types.findByName(self.l.module.types.internString("Source_Location")) orelse .unresolved,
            .if_expr => |ie| {
                // If-else types as its branches' unified type. A `noreturn`
                // branch (one that diverges — `return` / `raise` / `break` /
                // `continue`) unifies away, so the expression takes the other
                // branch's type; both diverging → `noreturn` (ERR E1.4c).
                if (ie.else_branch) |eb| {
                    const then_ty = self.l.inferExprType(ie.then_branch);
                    if (then_ty == .noreturn) return self.l.inferExprType(eb);
                    return then_ty;
                }
                return .void;
            },
            // Divergence shapes type as `noreturn` — they transfer control and
            // produce no value at their site. A block whose last statement is
            // one of these propagates `noreturn` (block arm below), which lets
            // a `catch` body that ends in `return` / `raise` unify with the
            // success type (ERR E1.4c / E1.5).
            .return_stmt, .raise_stmt, .break_expr, .continue_expr => .noreturn,
            .block => |blk| {
                // A block's type is its last expression's type only when it
                // produces a value (no trailing `;`); otherwise it is void.
                if (blk.produces_value and blk.stmts.len > 0) {
                    return self.l.inferExprType(blk.stmts[blk.stmts.len - 1]);
                }
                return .void;
            },
            .field_access => |fa| {
                // Pack-arity intercept: `<pack_name>.len` is s64. Mirrors
                // the lowerFieldAccess intercept so AST-level type
                // inference picks the same shape.
                if (self.l.pack_param_count) |ppc| {
                    if (fa.object.data == .identifier and std.mem.eql(u8, fa.field, "len")) {
                        if (ppc.contains(fa.object.data.identifier.name)) return .s64;
                    }
                }
                // Struct constant access: `Struct.CONST` — mirrors the
                // lowerFieldAccess intercept (line 3851). Without this,
                // `Phys.GRAVITY` (f64) inferred as s64 and pack-fn
                // callers boxed the float into the int slot.
                if (fa.object.data == .identifier) {
                    const obj_name = fa.object.data.identifier.name;
                    const qualified = std.fmt.allocPrint(self.l.alloc, "{s}.{s}", .{ obj_name, fa.field }) catch fa.field;
                    if (self.l.struct_const_map.get(qualified)) |info| {
                        if (info.ty) |t| return t;
                    }
                }
                // M1.3 — `obj.class` on an Obj-C-class pointer returns Class (*void).
                if (std.mem.eql(u8, fa.field, "class")) {
                    if (self.l.isObjcClassPointer(self.l.inferExprType(fa.object))) {
                        return self.l.module.types.ptrTo(.void);
                    }
                }
                // M2.2 — `obj.field` for an Obj-C `#property` field returns the field's type.
                if (self.l.lookupObjcPropertyOnPointer(fa.object, fa.field)) |prop| {
                    return self.l.resolveType(prop.field_type);
                }
                // M1.2 A.3 — sx-defined class state field returns the field's type.
                if (self.l.lookupObjcDefinedStateFieldOnPointer(fa.object, fa.field)) |info| {
                    return info.field_ty;
                }

                var obj_ty = self.l.inferExprType(fa.object);
                // Auto-deref: if object is a pointer, resolve through it (matches lowerFieldAccess behavior)
                if (!obj_ty.isBuiltin()) {
                    const ptr_info = self.l.module.types.get(obj_ty);
                    if (ptr_info == .pointer) {
                        obj_ty = ptr_info.pointer.pointee;
                    }
                }
                // Optional chaining: ?T.field → ?FieldType (flattened if field is already optional)
                const is_opt_chain = fa.is_optional;
                if (is_opt_chain and !obj_ty.isBuiltin()) {
                    const opt_info = self.l.module.types.get(obj_ty);
                    if (opt_info == .optional) {
                        obj_ty = opt_info.optional.child;
                    }
                }
                if (std.mem.eql(u8, fa.field, "len")) return if (is_opt_chain) self.l.module.types.optionalOf(.s64) else .s64;
                if (std.mem.eql(u8, fa.field, "ptr")) {
                    // .ptr on slice/string → [*]element_type
                    const elem_ty = self.l.getElementType(obj_ty);
                    const mp_ty = self.l.module.types.manyPtrTo(elem_ty);
                    return if (is_opt_chain) self.l.module.types.optionalOf(mp_ty) else mp_ty;
                }
                if (!obj_ty.isBuiltin()) {
                    const field_name_id = self.l.module.types.internString(fa.field);
                    // Check union fields (tagged enum payloads) + promoted struct fields
                    const info = self.l.module.types.get(obj_ty);
                    const u_fields2: ?[]const types.TypeInfo.StructInfo.Field = switch (info) {
                        .@"union" => |u| u.fields,
                        .tagged_union => |u| u.fields,
                        else => null,
                    };
                    if (u_fields2) |ufields| {
                        for (ufields) |f| {
                            if (f.name == field_name_id) return if (is_opt_chain) self.l.optionalOfFlattened(f.ty) else f.ty;
                            // Check promoted fields from anonymous struct variants
                            if (!f.ty.isBuiltin()) {
                                const fi = self.l.module.types.get(f.ty);
                                if (fi == .@"struct") {
                                    for (fi.@"struct".fields) |sf| {
                                        if (sf.name == field_name_id) return if (is_opt_chain) self.l.optionalOfFlattened(sf.ty) else sf.ty;
                                    }
                                }
                            }
                        }
                    }
                    // Check vector element access (.x/.y/.z/.w)
                    if (info == .vector) {
                        const elem = info.vector.element;
                        return if (is_opt_chain) self.l.optionalOfFlattened(elem) else elem;
                    }
                    // Tuple field access: numeric `t.0` or named `t.x`.
                    if (info == .tuple) {
                        const tup = info.tuple;
                        if (std.fmt.parseInt(usize, fa.field, 10)) |idx| {
                            if (idx < tup.fields.len)
                                return if (is_opt_chain) self.l.optionalOfFlattened(tup.fields[idx]) else tup.fields[idx];
                        } else |_| {}
                        if (tup.names) |names| {
                            for (names, 0..) |nm, i| {
                                if (nm == field_name_id and i < tup.fields.len)
                                    return if (is_opt_chain) self.l.optionalOfFlattened(tup.fields[i]) else tup.fields[i];
                            }
                        }
                    }
                    // Check struct fields
                    const fields = self.l.getStructFields(obj_ty);
                    for (fields) |f| {
                        if (f.name == field_name_id) return if (is_opt_chain) self.l.optionalOfFlattened(f.ty) else f.ty;
                    }
                }
                return .unresolved;
            },
            .identifier => |id| {
                if (self.l.scope) |scope| {
                    if (scope.lookup(id.name)) |binding| {
                        return binding.ty;
                    }
                }
                // `context` is the implicit-ctx identifier; type is Context
                // when the program has registered it (i.e. std.sx imported).
                if (self.l.implicit_ctx_enabled and std.mem.eql(u8, id.name, "context")) {
                    if (self.l.module.types.findByName(self.l.module.types.internString("Context"))) |ty| return ty;
                }
                // Check global variables (e.g., `context : Context`)
                if (self.l.program_index.global_names.get(id.name)) |gi| {
                    return gi.ty;
                }
                // Check module-level value constants (e.g., WIDTH :f32: 800)
                if (self.l.program_index.module_const_map.get(id.name)) |ci| {
                    return ci.ty;
                }
                // A bare type name (alias like `Vec4`, struct name, or
                // builtin primitive) referenced in expression position
                // is a Type value — IR type `.any`.
                if (self.l.isKnownTypeName(id.name)) return .any;
                return .unresolved;
            },
            .type_expr => |te| {
                // type_expr can also be a variable reference (e.g., "s1" matches builtin s1 type)
                if (self.l.scope) |scope| {
                    if (scope.lookup(te.name)) |binding| {
                        return binding.ty;
                    }
                }
                // A bare type name in expression position (e.g. `s64`,
                // `Point`, `*u8`) is a Type value — IR type `.any`.
                if (self.l.isKnownTypeName(te.name)) return .any;
                return .unresolved;
            },
            .enum_literal => {
                // Enum literals depend on context — use target_type if available
                return self.l.target_type orelse .unresolved;
            },
            .struct_literal => |sl| {
                if (sl.struct_name) |name| {
                    const name_id = self.l.module.types.internString(name);
                    return self.l.module.types.findByName(name_id) orelse
                        self.l.module.types.intern(.{ .@"struct" = .{ .name = name_id, .fields = &.{} } });
                }
                return self.l.target_type orelse .unresolved;
            },
            .tuple_literal => |tl| {
                var field_types = std.ArrayList(TypeId).empty;
                defer field_types.deinit(self.l.alloc);
                for (tl.elements) |elem| {
                    field_types.append(self.l.alloc, self.l.inferExprType(elem.value)) catch unreachable;
                }
                return self.l.module.types.intern(.{ .tuple = .{
                    .fields = self.l.alloc.dupe(TypeId, field_types.items) catch unreachable,
                    .names = null,
                } });
            },
            .index_expr => |ie| {
                // Pack-arg type lookup: `<pack_name>[<int_literal>]`.
                // Read directly from `pack_arg_types` — bypasses the
                // synthesized-ident detour in `pack_arg_nodes` which
                // would otherwise lose the type when the mono's
                // scope isn't set up yet (generic-`$R` pre-inference).
                if (self.l.pack_arg_types) |pat| {
                    if (ie.object.data == .identifier) {
                        if (pat.get(ie.object.data.identifier.name)) |arg_tys| {
                            if (self.l.comptimeIndexOf(ie.index)) |raw| {
                                if (raw >= 0) {
                                    const i: usize = @intCast(raw);
                                    if (i < arg_tys.len) return arg_tys[i];
                                }
                            }
                        }
                    }
                }
                if (self.l.packArgNodeAt(&ie)) |arg_node| {
                    return self.l.inferExprType(arg_node);
                }
                const obj_ty = self.l.inferExprType(ie.object);
                return self.l.getElementType(obj_ty);
            },
            .slice_expr => |se| {
                const obj_ty = self.l.inferExprType(se.object);
                if (obj_ty == .string) return .string;
                return self.l.module.types.sliceOf(self.l.getElementType(obj_ty));
            },
            .deref_expr => |de| {
                const ptr_ty = self.l.inferExprType(de.operand);
                if (!ptr_ty.isBuiltin()) {
                    const info = self.l.module.types.get(ptr_ty);
                    if (info == .pointer) return info.pointer.pointee;
                }
                return .unresolved;
            },
            .chained_comparison => .bool,
            .null_coalesce => |nc| blk: {
                // `opt ?? default` — result is the inner type when lhs is
                // optional (the unwrap path's value), else falls back to
                // the rhs's type. Without this arm pack-fn callers
                // misinferred float-optional coalesces as s64 and the
                // pack mono mangled the arg as int — the actual f64 value
                // got truncated through Any boxing.
                const lhs_ty = self.l.inferExprType(nc.lhs);
                if (!lhs_ty.isBuiltin()) {
                    const info = self.l.module.types.get(lhs_ty);
                    if (info == .optional) break :blk info.optional.child;
                }
                break :blk self.l.inferExprType(nc.rhs);
            },
            // Statements don't produce values (`.return_stmt` is handled above
            // as `.noreturn` — it diverges rather than yielding `void`).
            .assignment, .var_decl, .const_decl, .fn_decl,
            .defer_stmt, .push_stmt, .multi_assign, .destructure_decl,
            => .void,
            else => .unresolved,
        };
    }
};
