"""
transpiler/generator.py — The core Luau code generator (IR-based).
"""

from __future__ import annotations
from dataclasses import dataclass, field
from typing import Any, List, Optional, Set, Dict, Union
from transpiler.middleend import cfg, ir
from transpiler.optimization import loop_detector
from transpiler.infrastructure import visitor
from transpiler.infrastructure import logger
from transpiler.backend import luau_ast as last
from transpiler.infrastructure.errors import UnsupportedFeatureError
from transpiler.backend.printer import LuauPrinter
from transpiler.infrastructure.flags import CompilerFlags
from transpiler.transformer import TransformResult
from transpiler.backend.runtime_snippets import get_used_snippets

def _compute_escaped_vars(body: List[ir.IRStatement]) -> Set[str]:
    """
    Determine which variable names actually need to be hoisted to the top of
    their enclosing scope as `local x` (with nil initialiser).

    A variable *escapes* its first-assignment scope when:
      - It is first assigned inside a nested block (if/for/while body)
      - AND it is read *outside* that same nested block subtree
        (e.g., from a sibling `if` branch or after the loop)

    Implementation uses block-path tagging:
      - Each block gets a tuple path e.g. (0,), (0, 1), (0, 1, 2) ...
      - Variables first defined at path P "escape" if they are read at any
        path R where P is NOT a prefix of R and R is not a descendant of P.
      - Practically: if len(def_path) > 1 and any read at len(read_path) <= 1
        that is NOT inside the same for/if/while subtree → hoist.
 
    Simpler correct proxy: assign each def a "block_id" counter and each read
    outside that block also gets a counter. If they differ at the samefunction depth,
    the variable crosses scope.

    Simplest safe version:
      Only hoist if the variable is assigned in MULTIPLE different nested blocks
      OR if it is read at depth <= 1 (function-level) while defined at depth > 1,
      where depth counts from the 1st level of the function body.
    """
    import re

    def _orig(var: ir.IRVariable) -> str:
        n = var.original_name or var.name
        # Strip SSA suffix like _1, _2 etc.
        m = re.match(r'^(.+?)(?:_\d+)?$', n)
        return m.group(1) if m else n

    # Map: orig_name -> set of (depth, block_index) tuples where it was defined
    # If defined in multiple distinct blocks or at depth>1 and read at depth 1 → hoist
    
    # Two sets: 
    #   def_blocks:   {name -> {block_path}} where block_path is a tuple
    #   read_blocks:  {name -> {block_path}}
    
    def_blocks: Dict[str, Set[tuple]] = {}
    read_blocks: Dict[str, Set[tuple]] = {}

    def _walk(stmts: List[ir.IRStatement], path: tuple):
        for idx, stmt in enumerate(stmts):
            if isinstance(stmt, ir.IRFunctionDef):
                continue  # Don't cross nested function boundaries
            stmt_path = path # Correctly use the SAME path for statements in the same linear block
            if isinstance(stmt, ir.IRAssignment):
                tgt = stmt.target
                if isinstance(tgt, ir.IRVariable):
                    n = _orig(tgt)
                    if n not in def_blocks:
                        def_blocks[n] = set()
                    def_blocks[n].add(path)  # record which container block
                # Walk the value (RHS is still a read)
                _walk_expr(stmt.value, path)
                # Walk target if it's a subscript/property (those are reads too)
                if isinstance(tgt, (ir.IRPropertyAccess, ir.IRIndexOperation)):
                    _walk_expr(tgt, path)
            elif isinstance(stmt, ir.IRMultiAssign):
                for tgt in stmt.targets:
                    if isinstance(tgt, ir.IRVariable):
                        n = _orig(tgt)
                        if n not in def_blocks:
                            def_blocks[n] = set()
                        def_blocks[n].add(path)
                if stmt.rhs_call:
                    _walk_expr(stmt.rhs_call, path)
                for v in stmt.rhs_values:
                    _walk_expr(v, path)
            elif isinstance(stmt, ir.IRIfStatement):
                _walk_expr(stmt.condition, path)
                _walk(stmt.then_block, stmt_path + (0,))
                if stmt.else_block:
                    _walk(stmt.else_block, stmt_path + (1,))
                for ci, (_c, blk) in enumerate(stmt.elif_blocks or []):
                    _walk_expr(_c, path)
                    _walk(blk, stmt_path + (2 + ci,))
            elif isinstance(stmt, (ir.IRWhileStatement, ir.IRForStatement)):
                cond = getattr(stmt, 'condition', None)
                _walk_expr(cond, path)
                _walk(stmt.body, stmt_path + (0,))
            elif isinstance(stmt, ir.IRGenericForStatement):
                _walk_expr(stmt.iterator, path)
                _walk(stmt.body, stmt_path + (0,))
            elif isinstance(stmt, (ir.IRReturnStatement,)):
                _walk_expr(stmt.value, path)
            elif isinstance(stmt, ir.IRMultiReturn):
                for v in stmt.values:
                    _walk_expr(v, path)
            else:
                # Generic: walk all expression fields
                for v in vars(stmt).values():
                    if isinstance(v, ir.IRExpression):
                        _walk_expr(v, path)
                    elif isinstance(v, list):
                        for item in v:
                            if isinstance(item, ir.IRExpression):
                                _walk_expr(item, path)

    def _walk_expr(expr, path: tuple):
        if expr is None:
            return
        if isinstance(expr, ir.IRVariable):
            n = _orig(expr)
            if n not in read_blocks:
                read_blocks[n] = set()
            read_blocks[n].add(path)
        elif isinstance(expr, ir.IRFunctionDef):
            return  # Don't cross function boundaries
        elif hasattr(expr, '__dict__'):
            for v in vars(expr).values():
                if isinstance(v, ir.IRExpression):
                    _walk_expr(v, path)
                elif isinstance(v, list):
                    for item in v:
                        if isinstance(item, ir.IRExpression):
                            _walk_expr(item, path)

    _walk(body, ())

    # A variable needs hoisting if:
    # it has defs in nested paths (len > 1) and is read in a path that is NOT
    # a descendant of all of its defining paths.
    escaped: Set[str] = set()
    for name, def_paths in def_blocks.items():
        if name in ('key_label', 'name_label', 'slot_ui'):
            print(f"[ESCAPE ANALYSIS] {name}:")
            print(f"  def_paths={def_paths}")
            print(f"  read_paths={read_blocks.get(name, set())}")
        if all(len(p) == 0 for p in def_paths):
            # Defined at top-level: no hoisting needed (already local)
            continue
        if all(len(p) > 0 for p in def_paths):
            # Defined only in nested blocks: check reads
            reads = read_blocks.get(name, set())
            for rpath in reads:
                # If the read path is NOT a descendant of ANY def path, it crosses scope
                crosses = True
                for dpath in def_paths:
                    if rpath[:len(dpath)] == dpath:
                        crosses = False  # read is inside the same block subtree → OK
                        break
                if crosses:
                    if name in ('key_label', 'name_label', 'slot_ui'):
                        print(f"  [ESCAPED] via rpath={rpath} not in any def_path")
                    escaped.add(name)
                    break
    return escaped





@dataclass
class GenerateResult:
    code: str
    runtime_helpers: Set[str]
    line_map: Optional[Dict[int, int]] = None

class IRToLuauGenerator:
    def __init__(self, flags: CompilerFlags):
        self._declared_vars = set()
        self._loop_vars = set()
        self.flags = flags
        self.runtime_used = set()
        self.module_name: str = "unknown_module"
        self.metadata: Dict[str, Any] = {}
        self._current_class: Optional[ir.IRClassDef] = None
        self.is_native = False
        self.strict_mode = True # Default to strict for AAA scale
        self._scope_depth = 0
        self._function_decls: Set[str] = set()
        # Variables that must be hoisted because they escape their first-assignment scope
        self._escaped_vars: Set[str] = set()

    def generate(self, module: ir.IRModule, module_name: str = "unknown_module") -> last.Block:
        self.module_name = module_name
        self.metadata = module.metadata
        self._scope_depth = 0
        self._function_decls = set()
        self._declaration_stack: List[Set[str]] = [set()]
        self._loop_vars = set()
        self._escaped_vars = _compute_escaped_vars(module.body)
        
        body = self._visit_block(module.body)
        
        # Emit any variables that were first assigned in nested module blocks and escape their scope
        if self._function_decls:
            decls = sorted(list(self._function_decls))
            body.insert(0, last.RawCode(f"local {', '.join(decls)}"))
        
        # Phase S4: Traceback Registration
        if self.flags.debug:
            # We'll need a way for the printer to return the line_map.
            # Generator should probably collect it or trigger a mapping block.
            # For now, we'll assume the printer handles the actual stringification,
            # but we can insert a placeholder node that the printer replaces with the map.
             body.append(last.Comment(text="RPY_TRACEBACK_MAP_PLACEHOLDER"))
             self.runtime_used.add("traceback_handler")

        if self.strict_mode:
            body.insert(0, last.RawCode("--!strict"))
        return last.Block(body=body)

    def visit(self, node: ir.IRNode) -> Any:
        method_name = f"visit_{type(node).__name__}"
        visitor = getattr(self, method_name, self.generic_visit)
        res = visitor(node)
        
        # Phase S4: Track line mapping
        if hasattr(node, "lineno") and node.lineno is not None:
             # We'll need to know the CURRENT Luau line being emitted.
             # This is a bit tricky with the AST-to-Luau-AST approach.
             # We'll add a 'source_lineno' attribute to Luau nodes to help the printer.
             if res is not None and isinstance(res, last.Node):
                 res.source_lineno = node.lineno
        
        if res is not None and isinstance(res, last.Node) and hasattr(node, "lineno"):
            res.lineno = node.lineno
        return res
    def _visit_block(self, nodes: List[ir.IRStatement], is_function_body: bool = False) -> List[last.Statement]:
        self._scope_depth += 1
        results = []
        count = len(nodes)
        for i, n in enumerate(nodes):
            # Polish: Omit bare 'return' only at the TAIL of a function body.
            # Do NOT strip it from if/for/while bodies — bare returns there are meaningful
            # (e.g. early exit: "if processed then return end").
            if is_function_body and i == count - 1 and isinstance(n, ir.IRReturnStatement) and n.value is None:
                continue
                
            res = self.visit(n)
            if res is not None:
                if isinstance(res, list):
                    results.extend(res)
                else:
                    results.append(res)
        self._scope_depth -= 1
        return results

    def visit_IRModule(self, node: ir.IRModule) -> last.Block:
        return last.Block(body=self._visit_block(node.body, is_function_body=True))

    def _map_type(self, py_type: str) -> str:
        mapping = {
            "int": "number",
            "float": "number",
            "str": "string",
            "bool": "boolean",
            "list": "table",
            "dict": "table",
            "Any": "any",
            "None": "nil"
        }
        return mapping.get(py_type, py_type if py_type.isidentifier() else "any")

    # --- Expressions ---

    def visit_IRLiteral(self, node: ir.IRLiteral) -> last.Literal:
        return last.Literal(value=node.value)

    def visit_IRVariable(self, node: ir.IRVariable) -> last.Variable:
        name = getattr(node, "name", None)
        if not name:
             return last.Variable("?")
        
        remap = {
            "print": "print", "len": "py_len", "str": "py_str", "int": "py_int", "float": "py_float",
            "abs": "math.abs", "min": "math.min", "max": "math.max", "type": "typeof"
        }
        final_name = remap.get(name, name)
        
        if final_name and final_name.startswith("py_"):
            self.runtime_used.add(final_name)
        return last.Variable(name=final_name)

    def _is_native_op(self, node: ir.IRBinaryOperation, op: str, l_type: str, r_type: str) -> bool:
        """Return True if Luau can handle this operation natively without py_mult."""
        # Trust inferred types for Roblox math types
        math_types = {"Vector3", "Vector2", "CFrame", "UDim2", "UDim", "Color3"}
        
        if op in ("*", "/", "+", "-"):
            # Asymmetric Dispatch: Vector3 * number, CFrame * Vector3, etc.
            if l_type in math_types or r_type in math_types:
                # If either side is a known math type, use native op.
                # py_mult is ONLY for sequence multiplication (String * Int, List * Int).
                # Sequence types are never in math_types.
                return True
                
            # Heuristic for literals
            if op == "*":
                if isinstance(node.right, ir.IRLiteral) and isinstance(node.right.value, (float, int)):
                    if isinstance(node.right.value, float): return True # definitely math
                if isinstance(node.left, ir.IRLiteral) and isinstance(node.left.value, (float, int)):
                    if isinstance(node.left.value, float): return True # definitely math

        # Unified dispatch table for natively safe (operator, lhs_type, rhs_type)
        NATIVE_OPS = {
            ("*", "number", "number"),
            ("/", "number", "number"),
            ("+", "number", "number"),
            ("-", "number", "number"),
        }
        return (op, l_type, r_type) in NATIVE_OPS

    def visit_IRBinaryOperation(self, node: ir.IRBinaryOperation) -> last.BinaryOperation:
        op_map = {
            "Add": "+", "Sub": "-", "Mult": "*", "Div": "/",
            "Mod": "%", "Pow": "^", "BitAnd": "&", "BitOr": "|",
            "BitXor": "~", "LShift": "<<", "RShift": ">>",
            "Lt": "<", "LtE": "<=", "Gt": ">", "GtE": ">=",
            "Eq": "==", "NotEq": "~=", "And": "and", "Or": "or",
            "Concat": ".."
        }
        luau_op = op_map.get(node.operator, node.operator)
        left = self.visit(node.left)
        right = self.visit(node.right)

        if node.operator == "FloorDiv":
            return last.FunctionCall(
                func=last.MemberAccess(receiver=last.Variable(name="math"), member="floor"),
                args=[last.BinaryOperation(left=left, operator="/", right=right)]
            )


        # Python-style list replication/multiplication: [x] * n or Vector3 * number
        if node.operator == "Mult":
            left_type = getattr(node.left, "inferred_type", None)
            right_type = getattr(node.right, "inferred_type", None)
                        
            # Type-Aware Operator Dispatch
            l_type_str = getattr(left_type, "name", str(left_type)) if left_type else "Any"
            r_type_str = getattr(right_type, "name", str(right_type)) if right_type else "Any"

            if self._is_native_op(node, "*", l_type_str, r_type_str):
                return last.BinaryOperation(left=left, operator="*", right=right)
            
            # Sequence Lowering: [x] * n -> table.create(n, x)
            if isinstance(node.left, ir.IRTableLiteral) and len(node.left.elements) == 1:
                return last.FunctionCall(
                    func=last.MemberAccess(receiver=last.Variable(name="table"), member="create"),
                    args=[right, self.visit(node.left.elements[0])]
                )
            if isinstance(node.right, ir.IRTableLiteral) and len(node.right.elements) == 1:
                 return last.FunctionCall(
                    func=last.MemberAccess(receiver=last.Variable(name="table"), member="create"),
                    args=[left, self.visit(node.right.elements[0])]
                )
            
            # Runtime shim fallback
            final_name = "py_mult"
            self.runtime_used.add(final_name)
            return last.FunctionCall(func=last.Variable(name=final_name), args=[left, right])

        # Peephole: #str > 0  ->  str ~= ""
        # A string length > 0 check is better expressed as inequality in Luau.
        if luau_op == ">" and isinstance(right, last.Literal) and right.value == 0:
            if isinstance(left, last.UnaryOperation) and left.operator == "#":
                return last.BinaryOperation(left=left.operand, operator="~=", right=last.Literal(""))

        return last.BinaryOperation(left=left, operator=luau_op, right=right)

    def visit_IRIfElseExpression(self, node: ir.IRIfElseExpression) -> last.IfElseExpression:
        return last.IfElseExpression(
            condition=self.visit(node.condition),
            then_expr=self.visit(node.then_expr),
            else_expr=self.visit(node.else_expr)
        )

    def visit_IRUnaryOperation(self, node: ir.IRUnaryOperation) -> last.UnaryOperation:
        op_map = {"USub": "-", "UAdd": "+", "Not": "not ", "Invert": "~"}
        return last.UnaryOperation(
            operator=op_map.get(node.operator, node.operator),
            operand=self.visit(node.operand)
        )

    def visit_IRFunctionCall(self, node: ir.IRFunctionCall) -> last.FunctionCall:
        # Check for re and json module shims
        if isinstance(node.func, ir.IRPropertyAccess) and isinstance(node.func.receiver, ir.IRVariable):
            mod = node.func.receiver.name
            prop = node.func.property
            if mod == "re":
                shim_name = f"py_re_{prop}"
                self.runtime_used.add(shim_name)
                return last.FunctionCall(
                    func=last.Variable(shim_name),
                    args=[self.visit(arg) for arg in node.args]
                )
            elif mod == "json":
                shim_map = {"dumps": "py_json_dumps", "loads": "py_json_loads"}
                if prop in shim_map:
                    shim_name = shim_map[prop]
                    self.runtime_used.add(shim_name)
                    return last.FunctionCall(
                        func=last.Variable(shim_name),
                        args=[self.visit(arg) for arg in node.args]
                    )
            elif mod == "math":
                shim_map = {"radians": "py_math_radians", "degrees": "py_math_degrees"}
                if prop in shim_map:
                    shim_name = shim_map[prop]
                    self.runtime_used.add(shim_name)
                    return last.FunctionCall(
                        func=last.Variable(shim_name),
                        args=[self.visit(arg) for arg in node.args]
                    )
            elif mod == "random":
                shim_map = {"Random": "py_random_new", "randint": "py_random_randint", "random": "py_random_random"}
                if prop in shim_map:
                    shim_name = shim_map[prop]
                    self.runtime_used.add(shim_name)
                    return last.FunctionCall(
                        func=last.Variable(shim_name),
                        args=[self.visit(arg) for arg in node.args]
                    )
            elif mod == "struct":
                shim_map = {"pack": "py_struct_pack", "unpack": "py_struct_unpack"}
                if prop in shim_map:
                    shim_name = shim_map[prop]
                    self.runtime_used.add(shim_name)
                    return last.FunctionCall(
                        func=last.Variable(shim_name),
                        args=[self.visit(arg) for arg in node.args]
                    )
            elif mod == "logging":
                shim_map = {
                    "debug": "py_logging_debug",
                    "info": "py_logging_info",
                    "warning": "py_logging_warning",
                    "error": "py_logging_error"
                }
                if prop in shim_map:
                    shim_name = shim_map[prop]
                    self.runtime_used.add(shim_name)
                    return last.FunctionCall(
                        func=last.Variable(shim_name),
                        args=[self.visit(arg) for arg in node.args]
                    )
            elif mod == "itertools":
                shim_map = {"chain": "py_itertools_chain", "cycle": "py_itertools_cycle", "repeat": "py_itertools_repeat"}
                if prop in shim_map:
                    shim_name = shim_map[prop]
                    self.runtime_used.add(shim_name)
                    return last.FunctionCall(
                        func=last.Variable(shim_name),
                        args=[self.visit(arg) for arg in node.args]
                    )
            elif mod == "functools":
                shim_map = {"partial": "py_functools_partial"}
                if prop in shim_map:
                    shim_name = shim_map[prop]
                    self.runtime_used.add(shim_name)
                    return last.FunctionCall(
                        func=last.Variable(shim_name),
                        args=[self.visit(arg) for arg in node.args]
                    )
            elif mod == "heapq":
                shim_map = {"heappush": "py_heapq_heappush", "heappop": "py_heapq_heappop"}
                if prop in shim_map:
                    shim_name = shim_map[prop]
                    self.runtime_used.add(shim_name)
                    return last.FunctionCall(
                        func=last.Variable(shim_name),
                        args=[self.visit(arg) for arg in node.args]
                    )
            elif mod == "bisect":
                shim_map = {"bisect_left": "py_bisect_bisect_left", "bisect_right": "py_bisect_bisect_right"}
                if prop in shim_map:
                    shim_name = shim_map[prop]
                    self.runtime_used.add(shim_name)
                    return last.FunctionCall(
                        func=last.Variable(shim_name),
                        args=[self.visit(arg) for arg in node.args]
                    )
            elif mod == "buffer":
                shim_map = {
                    "create": "py_buffer_create",
                    "readi8": "py_buffer_readi8",
                    "writei8": "py_buffer_writei8",
                    "readf32": "py_buffer_readf32",
                    "writef32": "py_buffer_writef32"
                }
                if prop in shim_map:
                    shim_name = shim_map[prop]
                    self.runtime_used.add(shim_name)
                    return last.FunctionCall(
                        func=last.Variable(shim_name),
                        args=[self.visit(arg) for arg in node.args]
                    )
            elif mod == "collections":
                shim_map = {
                    "deque": "py_collections_deque_new",
                    "Counter": "py_collections_counter_new",
                    "defaultdict": "py_collections_defaultdict_new",
                    "namedtuple": "py_collections_namedtuple_new"
                }
                if prop in shim_map:
                    shim_name = shim_map[prop]
                    self.runtime_used.add(shim_name)
                    return last.FunctionCall(
                        func=last.Variable(shim_name),
                        args=[self.visit(arg) for arg in node.args]
                    )
            # Phase S4: Standard Library (math, json, base64)
            elif mod == "math":
                # Most are native Luau math, handled in IRVariable remap or standard visit
                pass
            elif mod == "json":
                self.runtime_used.add("json") # Trigger stdlib/json logic
            elif mod == "base64":
                self.runtime_used.add("base64")

        # 3. Builtin optimizations
        if isinstance(node.func, ir.IRVariable):
            name = node.func.name
            if name == "enumerate" and len(node.args) == 2:
                # Optimized enumerate(x, 1) -> ipairs(x)
                # Note: only safe if start is 1, as Luau ipairs is 1-based
                start_arg = node.args[1]
                if isinstance(start_arg, ir.IRLiteral) and start_arg.value == 1:
                    return last.FunctionCall(
                        func=last.Variable("ipairs"),
                        args=[self.visit(node.args[0])]
                    )
            elif name == "len" and len(node.args) == 1:
                # Optimization: len(str) > 0 is better expressed as str ~= ""
                # We emit #x here; the BinaryOperation visitor will collapse #x > 0 -> x ~= ""
                self.runtime_used.discard("len")
                return last.UnaryOperation(
                    operator="#",
                    operand=self.visit(node.args[0])
                )
            elif name == "int" and len(node.args) == 1:
                # int(x) -> math.floor(tonumber(x) or 0)
                return last.FunctionCall(
                    func=last.Variable("math.floor"),
                    args=[last.BinaryOperation(
                        left=last.FunctionCall(func=last.Variable("tonumber"), args=[self.visit(node.args[0])]),
                        operator="or",
                        right=last.Literal(0)
                    )]
                )
            elif name == "str" and len(node.args) == 1:
                # str(x) -> tostring(x)
                return last.FunctionCall(
                    func=last.Variable("tostring"),
                    args=[self.visit(node.args[0])]
                )
            elif name == "setattr" and len(node.args) == 3:
                # setattr(obj, "prop", val) -> obj.prop = val
                prop_arg = node.args[1]
                if isinstance(prop_arg, ir.IRLiteral) and isinstance(prop_arg.value, str):
                    receiver = self.visit(node.args[0])
                    value = self.visit(node.args[2])
                    return last.Assignment(
                        target=last.MemberAccess(receiver=receiver, member=prop_arg.value),
                        value=value
                    )
            elif name == "py_bool" and len(node.args) == 1:
                # Eliminate py_bool: native Luau truthiness is sufficient
                return self.visit(node.args[0])

        return last.FunctionCall(
            func=self.visit(node.func),
            args=[self.visit(arg) for arg in node.args]
        )

    ROBLOX_API_MAP = {
        "get_children": "GetChildren",
        "wait_for_child": "WaitForChild",
        "find_first_child": "FindFirstChild",
        "get_service": "GetService",
        "connect": "Connect",
        "clone": "Clone",
        "destroy": "Destroy",
        "is_a": "IsA",
        "get_descendants": "GetDescendants",
        "get_property_changed_signal": "GetPropertyChangedSignal",
        "fire_server": "FireServer",
        "fire_client": "FireClient",
        "on_server_event": "OnServerEvent",
        "on_client_event": "OnClientEvent",
        "is_descendant_of": "IsDescendantOf"
    }

    def visit_IRPropertyAccess(self, node: ir.IRPropertyAccess) -> last.MemberAccess:
        receiver = self.visit(node.receiver)
        # Member remapping
        prop = node.property
        well_known = {
            "name": "Name", "parent": "Parent", "value": "Value", "visible": "Visible",
            "on_server_event": "OnServerEvent", "on_client_event": "OnClientEvent"
        }
        prop = well_known.get(prop, prop)
        
        # Point 4 Fix: Handle Enum.Value -> use the Enum Item itself or a safe number convert
        # Roblox Enums DO have .Value, but if the user wants idiomatic Enum comparisons, 
        # we can just let it be. However, if they want to get the number for keys:
        if prop == "Value" and self._is_enum(receiver):
            # Special case for KeyCode hack: item.Value - 48
            # We'll keep the .Value as it is actually supported, but the user's remark 
            # suggests they prefer a different way. We'll stick to valid Lua first.
            pass
        
        return last.MemberAccess(receiver=receiver, member=prop)

    def visit_IRMethodCall(self, node: ir.IRMethodCall) -> last.Node:
        receiver = self.visit(node.receiver)
        args = [self.visit(arg) for arg in node.args]
        
        # Roblox API case mapping
        method_name = self.ROBLOX_API_MAP.get(node.method, node.method)
        # Handle snake_case to CamelCase fallback for any colon call
        if method_name == node.method and "_" in node.method:
            method_name = "".join(word.capitalize() for word in node.method.split("_"))

        # Check for re and json module shims
        if isinstance(node.receiver, ir.IRVariable):
            mod = node.receiver.name
            if mod == "re":
                shim_name = f"py_re_{node.method}"
                self.runtime_used.add(shim_name)
                return last.FunctionCall(func=last.Variable(shim_name), args=args)
            elif mod == "json":
                shim_map = {"dumps": "py_json_dumps", "loads": "py_json_loads"}
                if node.method in shim_map:
                    shim_name = shim_map[node.method]
                    self.runtime_used.add(shim_name)
                    return last.FunctionCall(func=last.Variable(shim_name), args=args)

        if node.method == "items" and len(node.args) == 0:
            # Map Python dict.items() -> pairs(dict)
            return last.FunctionCall(
                func=last.Variable("pairs"),
                args=[receiver]
            )
        elif node.method == "get" and len(node.args) >= 1:
            # Map Python dict.get(key, default) -> dict[key] or default
            key = self.visit(node.args[0])
            access = last.IndexOperation(receiver=receiver, index=key)
            if len(node.args) == 2:
                default = self.visit(node.args[1])
                return last.BinaryOperation(left=access, operator="or", right=default)
            return access
        elif node.method == "split" and len(node.args) >= 0:
            # Map Python str.split(sep) -> string.split(str, sep)
            sep = self.visit(node.args[0]) if len(node.args) > 0 else last.Literal(" ")
            return last.FunctionCall(
                func=last.Variable("string.split"),
                args=[receiver, sep]
            )
        elif node.method == "setattr" and len(node.args) == 2:
            # Method-style setattr(prop_name, val)
            prop_arg = node.args[0]
            if isinstance(prop_arg, ir.IRLiteral) and isinstance(prop_arg.value, str):
                return last.Assignment(
                    target=last.MemberAccess(receiver=receiver, member=prop_arg.value),
                    value=self.visit(node.args[1])
                )

        # Certain Roblox namespaces are pure-module dot-calls, never colon-calls.
        # e.g. CFrame.new(), Vector3.new(), math.rad(), Color3.fromRGB()
        DOT_ONLY_RECEIVERS = {
            "CFrame", "Vector3", "Vector2", "UDim", "UDim2", "Rect", "Color3",
            "TweenInfo", "NumberSequence", "ColorSequence", "Ray", "Region3",
            "math", "table", "string", "os", "task", "Instance",
        }
        force_dot = False
        if isinstance(node.receiver, ir.IRVariable) and node.receiver.name in DOT_ONLY_RECEIVERS:
            force_dot = True

        if method_name in ("Connect", "connect") and len(args) == 1:
            # Signal handling: strip any leftover phantom args from callbacks.
            # Roblox UI events (Click, etc.) pass no arguments.
            # Lambda default-arg captures (lambda tool=val:) are handled at the call-site
            # by emitting local captures BEFORE the Connect call, not as function params.
            cb = args[0]
            target_cb = None
            if isinstance(cb, (last.Lambda, last.FunctionDef)):
                target_cb = cb
            elif isinstance(cb, last.FunctionCall) and isinstance(cb.func, last.Lambda):
                # Check if it's an IIFE wrapper containing the actual lambda
                if len(cb.func.body) == 1 and isinstance(cb.func.body[0], last.ReturnStatement):
                    if isinstance(cb.func.body[0].value, (last.Lambda, last.FunctionDef)):
                        target_cb = cb.func.body[0].value
            
            if target_cb:
                # Remove all parameters from Click-style callbacks — they receive no args
                receiver_str = self._print_node(receiver) or ""
                if "Click" in receiver_str or "MouseButton" in receiver_str or "InputBegan" in receiver_str:
                    target_cb.args = []
            
        if node.call_style == ir.CallStyle.DOT or force_dot:
            # Optimized to MemberAccess instead of Variable with dots
            return last.FunctionCall(
                func=last.MemberAccess(receiver=receiver, member=method_name),
                args=args
            )
        else:
            return last.MethodCall(
                receiver=receiver,
                method=method_name,
                args=args
            )

    def visit_IRTableLiteral(self, node: ir.IRTableLiteral) -> last.TableLiteral:
        return last.TableLiteral(
            elements=[self.visit(e) for e in node.elements],
            fields=[(k, self.visit(v)) for k, v in node.fields]
        )

    def visit_IRIfElseExpression(self, node: ir.IRIfElseExpression) -> last.Expression:
        # (if cond then a else b)
        return last.IfElseExpression(
            condition=self.visit(node.condition),
            then_expr=self.visit(node.then_expr),
            else_expr=self.visit(node.else_expr)
        )

    def visit_IRIndexOperation(self, node: ir.IRIndexOperation) -> Union[last.IndexOperation, last.MemberAccess]:
        index_node = node.index
        # Optimization: Map negative indexing [-1] to [#receiver]
        if isinstance(index_node, ir.IRLiteral) and isinstance(index_node.value, int) and index_node.value < 0:
            receiver_node = self.visit(node.receiver)
            # [-1] -> #receiver, [-2] -> #receiver - 1
            # To avoid redundant calls for complex receivers, we assume the user 
            # uses variables or we just accept the slight bloat for now.
            # (Wait, actually, I'll just use the visited node twice here.)
            idx_expr = last.UnaryOperation(operator="#", operand=receiver_node)
            if index_node.value < -1:
                idx_expr = last.BinaryOperation(
                    left=idx_expr, 
                    operator="-", 
                    right=last.Literal(abs(index_node.value) - 1)
                )
            return last.IndexOperation(
                receiver=receiver_node,
                index=idx_expr
            )
            
        visited_receiver = self.visit(node.receiver)
        visited_index = self.visit(index_node)
        
        # Optimization: data["items"] -> data.items if key is a valid identifier
        if isinstance(visited_index, last.Literal) and isinstance(visited_index.value, str):
            if visited_index.value.isidentifier():
                return last.MemberAccess(receiver=visited_receiver, member=visited_index.value)
                
        return last.IndexOperation(
            receiver=visited_receiver,
            index=visited_index
        )

    def visit_IRAwait(self, node: ir.IRAwait) -> last.Expression:
        # Python 'await x' maps to a direct expression in Luau.
        # If the expression is a yielding function call, Luau handles it natively.
        return self.visit(node.value)

    # --- Statements ---

    def visit_IRAssignment(self, node: ir.IRAssignment) -> last.Statement:
        target_node = self.visit(node.target)
        value_node = self.visit(node.value)
        
        # Determine if it should be 'local'
        should_be_local = node.is_local
        
        # CORRECTNESS: NEVER prefix property access or index with 'local'
        # e.g. 'local obj.prop' or 'local tab[k]' is invalid Luau syntax.
        if isinstance(target_node, (last.MemberAccess, last.IndexOperation)):
            # Optimization: data["items"] -> data.items if key is a valid identifier
            if isinstance(target_node, last.IndexOperation) and isinstance(target_node.index, last.Literal):
                idx_val = target_node.index.value
                if isinstance(idx_val, str) and idx_val.isidentifier():
                    target_node = last.MemberAccess(receiver=target_node.receiver, member=idx_val)
            should_be_local = False
        elif isinstance(target_node, last.Variable):
            # Use original_name for declaration tracking to avoid shadowing SSA versions
            decl_name = node.target.original_name if isinstance(node.target, ir.IRVariable) and node.target.original_name else target_node.name
            
            # If it's a simple variable, we decide where to declare it
            is_already_declared = any(decl_name in s for s in self._declaration_stack)
            if not is_already_declared:
                if self._scope_depth > 1 and decl_name in self._escaped_vars:
                    # Variable escapes its scope: hoist to function top as nil
                    self._function_decls.add(decl_name)
                    self._declaration_stack[-1].add(decl_name)
                    should_be_local = False
                else:
                    # Variable is scoped: declare inline as `local` on first assignment
                    should_be_local = True
                    self._declaration_stack[-1].add(decl_name)
            else:
                # Already declared in this function/module/parent scope
                should_be_local = False
        else:
            should_be_local = False

        if should_be_local:
            # Handle type annotations for strict mode
            type_ann = node.annotation if getattr(node, "annotation", None) else (node.value.inferred_type if self.flags.typed else None)
            if type_ann:
                type_ann = self._map_type(type_ann)
            
            return last.LocalAssign(
                name=self._print_node(target_node), 
                value=value_node, 
                type_annotation=type_ann, 
                lineno=node.lineno
            )
        
        return last.Assignment(target=target_node, value=value_node, lineno=node.lineno)

    def visit_IRMultiAssign(self, node: ir.IRMultiAssign) -> last.MultiAssign:
        targets = []
        for i, t in enumerate(node.targets):
            visited = self.visit(t)
            # Guard: DCE may replace unused multi-assign targets with IRLiteral(None).
            # Emitting 'local x, nil = f()' is a Luau syntax error. Use '_' discard instead.
            if isinstance(visited, last.Literal) and visited.value is None:
                visited = last.Variable(name=f"_discard_{i}")
            targets.append(visited)
        rhs_values = [self.visit(v) for v in node.rhs_values]
        rhs_call = self.visit(node.rhs_call) if node.rhs_call else None
        
        is_local = node.is_local
        # Track declarations
        for i, t in enumerate(targets):
            if isinstance(t, last.Variable):
                orig_t = node.targets[i]
                decl_name = orig_t.original_name if isinstance(orig_t, ir.IRVariable) and orig_t.original_name else t.name
                is_already_declared = any(decl_name in s for s in self._declaration_stack)
                if not is_already_declared:
                    # Point 3 Fix: If this is a loop assignment or the variable is a loop header
                    # do NOT hoist it. The for loop itself handles the scoping in Luau correctly.
                    if node.is_loop_assignment or decl_name in self._loop_vars:
                         self._declaration_stack[-1].add(decl_name)
                    elif self._scope_depth > 1 and decl_name in self._escaped_vars:
                        # Variable escapes its scope: hoist to function top as nil
                        self._function_decls.add(decl_name)
                        self._declaration_stack[-1].add(decl_name)
                        is_local = False
                    else:
                        self._declaration_stack[-1].add(decl_name)
        
        # If it's a redundant loop assignment, we skip the emission but kept the declaration tracking above
        if node.is_loop_assignment:
            return None # type: ignore
            
        # Dead Return Pruning: If we have trailing discards, remove them.
        # Luau safely ignores extra return values.
        # e.g. local a, _discard_1 = f()  ->  local a = f()
        final_targets = list(targets)
        while len(final_targets) > 1:
            last_t = final_targets[-1]
            if isinstance(last_t, last.Variable) and last_t.name.startswith("_discard_"):
                final_targets.pop()
            else:
                break

        return last.MultiAssign(
            targets=final_targets,
            values=rhs_values,
            rhs_call=rhs_call,
            is_local=is_local,
            lineno=node.lineno
        )

    def visit_IRPersistentAssign(self, node: ir.IRPersistentAssign) -> last.Statement:
        # User requested elimination of Attribute system / _G abuse.
        # Persistent variables in the source Python map to idiomatic module-level locals.
        target_node = self.visit(node.target)
        value_node = self.visit(node.value)
        
        # Fallback to standard IRAssignment logic for clean idiomatic output
        return self.visit_IRAssignment(ir.IRAssignment(
            target=node.target,
            value=node.value,
            lineno=node.lineno,
            is_local=True
        ))

    def _is_roblox_global(self, name: str) -> bool:
        """Roblox-specific globals that should not be shadowed by local args."""
        return name in ("char", "player", "plr", "game", "workspace", "script")

    def _is_used_only_as_bool(self, var_name: str, body: List[ir.IRStatement]) -> bool:
        """Determines if a variable is only used in boolean contexts (if/while/not/ternary)."""
        used_non_bool = False
        
        class UsageVisitor(visitor.IRVisitor):
            def __init__(self):
                self.used_non_bool = False
            
            def visit_IRVariable(self, node: ir.IRVariable):
                if node.name == var_name or node.original_name == var_name:
                    # Generic visit means it's used somewhere we don't know is boolean
                    self.used_non_bool = True
            
            def visit_IRIfStatement(self, node: ir.IRIfStatement):
                # Condition is a boolean context
                self.visit_boolean_context(node.condition)
                self.visit(node.then_block)
                if node.else_block: self.visit(node.else_block)
                for c, b in (node.elif_blocks or []):
                    self.visit_boolean_context(c)
                    self.visit(b)
            
            def visit_IRWhileStatement(self, node: ir.IRWhileStatement):
                self.visit_boolean_context(node.condition)
                self.visit(node.body)
                
            def visit_IRUnaryOperation(self, node: ir.IRUnaryOperation):
                if node.operator == "Not":
                    self.visit_boolean_context(node.operand)
                else:
                    self.visit(node.operand)
                    
            def visit_IRBinaryOperation(self, node: ir.IRBinaryOperation):
                if node.operator in ("And", "Or"):
                    self.visit_boolean_context(node.left)
                    self.visit_boolean_context(node.right)
                else:
                    self.visit(node.left)
                    self.visit(node.right)

            def visit_boolean_context(self, node: ir.IRNode):
                # Don't visit IRVariable here directly to avoid marking it as non-bool
                if isinstance(node, ir.IRVariable):
                    pass # It's fine!
                else:
                    self.visit(node)
            
            def visit_IRLiteral(self, node: ir.IRLiteral):
                pass
            
            def visit_IRAssignment(self, node: ir.IRAssignment):
                self.visit(node.value)
                if not isinstance(node.target, ir.IRVariable):
                    self.visit(node.target)
            
            def visit_IRGenericForStatement(self, node: ir.IRGenericForStatement):
                self.visit(node.iterator)
                self.visit(node.body)
                
            def visit_IRMethodCall(self, node: ir.IRMethodCall):
                self.visit(node.receiver)
                self.visit(node.args)
                
            def visit_IRFunctionCall(self, node: ir.IRFunctionCall):
                self.visit(node.func)
                self.visit(node.args)
                
            def visit_IRReturnStatement(self, node: ir.IRReturnStatement):
                self.visit(node.value)
                
            def visit_IRBreakStatement(self, node: ir.IRBreakStatement):
                pass
                
            def visit_IRContinueStatement(self, node: ir.IRContinueStatement):
                pass

            def visit_IRIndexOperation(self, node: ir.IRIndexOperation):
                self.visit(node.receiver)
                self.visit(node.index)
                    
        v = UsageVisitor()
        for stmt in body:
            v.visit(stmt)
            if v.used_non_bool: return False
        return True

    def visit_IRFunctionDef(self, node: ir.IRFunctionDef) -> last.FunctionDef:
        # Handle decorators efficiently
        decorators = getattr(node, "decorators", [])
        if "native" in decorators:
            self.is_native = True
        
        # Save previous declared vars to restore after function body
        old_function_decls = self._function_decls.copy()
        old_escaped_vars = self._escaped_vars.copy()
        old_depth = self._scope_depth
        
        self._function_decls = set()
        self._escaped_vars = _compute_escaped_vars(node.body)
        self._scope_depth = 0
        
        new_scope = set()
        self._declaration_stack.append(new_scope)
        for arg in node.args:
            new_scope.add(arg)
            
        # Lower default arguments at the start of the body
        default_stmts = []
        if hasattr(node, "defaults"):
            for i, d in enumerate(node.defaults):
                if d is not None:
                    arg_name = node.args[i]
                    # Elite-Tier Polish: If the argument is only used as a boolean, 
                    # we don't need to normalize nil -> false because nil is falsy.
                    if (isinstance(d, ir.IRLiteral) and d.value is False and 
                        self._is_used_only_as_bool(arg_name, node.body)):
                        continue

                    # Luau idiomatic: arg = if arg == nil then default else arg
                    # We'll use a direct last.Assignment for this
                    default_stmts.append(ir.IRAssignment(
                        target=ir.IRVariable(name=arg_name, lineno=node.lineno),
                        value=ir.IRIfElseExpression(
                            condition=ir.IRBinaryOperation(
                                left=ir.IRVariable(name=arg_name, lineno=node.lineno),
                                operator="Eq",
                                right=ir.IRLiteral(value=None, lineno=node.lineno),
                                lineno=node.lineno
                            ),
                            then_expr=d,
                            else_expr=ir.IRVariable(name=arg_name, lineno=node.lineno),
                            lineno=node.lineno
                        ),
                        lineno=node.lineno,
                        is_local=False # It's already an argument (declared)
                    ))
        
        body = self._visit_block(default_stmts + node.body, is_function_body=True)
        
        # Insert function-level declarations at the top of the body
        if self._function_decls:
            # Filter out arguments and already declared top-level locals
            # (Though our logic should avoid adding them to _function_decls anyway)
            sorted_decls = sorted(list(self._function_decls))
            decl_stmt = last.RawCode(f"local {', '.join(sorted_decls)}")
            body.insert(0, decl_stmt)

        # Restore declared vars (function scope isolation)
        self._declaration_stack.pop()
        self._function_decls = old_function_decls
        self._escaped_vars = old_escaped_vars
        self._scope_depth = old_depth
        
        # Type Bridge: Map Python hints to Luau types
        arg_types = []
        if self.strict_mode:
            arg_types = [self._map_type(t) if t and t != "Any" and t != "any" else None for t in getattr(node, "arg_types", [])]
        
        return_type = None
        if self.strict_mode and getattr(node, "return_type", None):
            rt = self._map_type(node.return_type)
            if rt not in ("any", "Any", "nil", "None"):
                return_type = rt

        return last.FunctionDef(
            name=node.name,
            args=node.args,
            body=body,
            is_local=getattr(node, "is_local", True),
            is_method=getattr(node, "is_method", False),
            arg_types=arg_types,
            return_type=return_type,
            lineno=node.lineno
        )

    def visit_IRAsyncFunctionDef(self, node: ir.IRAsyncFunctionDef) -> last.Node:
        # Lua functions are naturally yielding-compatible.
        # Mapping 'async def' to standard 'function' is idiomatic in Luau.
        return self.visit_IRFunctionDef(node)

    def visit_IRLambda(self, node: ir.IRLambda) -> last.Node:
        # Save previous declared vars to restore after lambda
        old_function_decls = self._function_decls.copy()
        old_escaped_vars = self._escaped_vars.copy()
        old_depth = self._scope_depth
        
        self._function_decls = set()
        self._escaped_vars = _compute_escaped_vars(node.body)
        self._scope_depth = 0
        
        new_scope = set()
        self._declaration_stack.append(new_scope)
        for arg in node.args:
            new_scope.add(arg)

        if node.defaults:
            # Python evaluates default args at definition time.
            # We lower `lambda x=i: body` to an IIFE capturing the defaults:
            #   (function(x) return function() return body end end)(i)
            # The inner function takes the non-default args as normal params.
            num_defaults = len(node.defaults)
            default_param_names = node.args[-num_defaults:]
            non_default_param_names = node.args[:-num_defaults] if num_defaults < len(node.args) else []
            
            # IIFE Minimization Optimization
            # If the user does `lambda x=x: ...`, they are likely attempting to capture a loop
            # variable to avoid late-binding in Python. In Luau, loop variables are already distinct per 
            # iteration. Capturing by reference is safe and identical to capturing by value in this case.
            requires_iife = False
            for p_name, default_expr in zip(default_param_names, node.defaults):
                # We check original_name because SSA might have appended _1, etc., 
                # but the underlying captured semantic variable is the same.
                expr_name = getattr(default_expr, "original_name", getattr(default_expr, "name", "")) if isinstance(default_expr, ir.IRVariable) else ""
                
                # Check against the parameter name (which is what it binds to in the lambda scope)
                if expr_name != p_name:
                    requires_iife = True
                    break

            if not requires_iife:
                # Emit a direct lambda, discarding the default parameters from the signature
                # since they are inherently captured from the lexical scope.
                res = last.Lambda(
                    args=non_default_param_names,
                    body=self._visit_block(node.body),
                    arg_types=node.arg_types[:len(non_default_param_names)] if getattr(node, "arg_types", None) and self.flags.typed else [],
                    return_type=node.return_type if self.flags.typed else None
                )
            else:
                # Fallback: Safe IIFE capture
                # The inner lambda uses all args (default args are now in scope from outer)
                inner_lam = last.Lambda(
                    args=non_default_param_names,
                    body=self._visit_block(node.body),
                    arg_types=[],
                    return_type=None
                )

                # The IIFE takes the captured default args as outer params
                iife_args = [self.visit(d) for d in node.defaults]
                iife_func = last.Lambda(
                    args=default_param_names,
                    body=[last.ReturnStatement(value=inner_lam, is_bare=False)],
                    arg_types=[],
                    return_type=None
                )
            
                res = last.FunctionCall(func=iife_func, args=iife_args)
        else:
            res = last.Lambda(
                args=node.args,
                body=self._visit_block(node.body),
                arg_types=node.arg_types if self.flags.typed else [],
                return_type=node.return_type if self.flags.typed else None
            )

        # Restore
        self._declaration_stack.pop()
        self._function_decls = old_function_decls
        self._escaped_vars = old_escaped_vars
        self._scope_depth = old_depth
        return res

    def _is_known_bool(self, node: ir.IRNode) -> bool:
        """Check if an IR node is known to return a boolean value at runtime."""
        if isinstance(node, ir.IRBinaryOperation) and node.operator in ("Eq", "NotEq", "Lt", "LtE", "Gt", "GtE", "And", "Or"):
            return True
        
        if isinstance(node, ir.IRUnaryOperation) and node.operator == "Not":
            return True

        if isinstance(node, ir.IRIfElseExpression):
            # Known bool if both paths are bools
            return self._is_known_bool(node.then_expr) and self._is_known_bool(node.else_expr)
            
        # Standardize method names for checking
        method_name = ""
        if isinstance(node, ir.IRMethodCall):
            method_name = node.method
        elif isinstance(node, ir.IRFunctionCall) and isinstance(node.func, ir.IRPropertyAccess):
            method_name = node.func.property
        
        # Property access like .Value (Roblox ObjectValue/BoolValue)
        if isinstance(node, ir.IRPropertyAccess) and node.property == "Value":
            return True
            
        if method_name.lower().replace("_", "") in ("isa", "isdescendantof", "findfirstchild"):
            return True
            
        if isinstance(node, ir.IRLiteral) and isinstance(node.value, (bool, type(None))):
            return True
        if getattr(node, "inferred_type", None) in ("bool", "boolean"):
            return True
        return False

    def _wrap_bool(self, node: ir.IRNode) -> last.Expression:
        """
        Returns the visited node for use in a boolean context.
        In Luau, nil and false are the only falsy values, which maps cleanly to
        Python's None/False semantics for the Roblox use-case.
        py_bool() is never emitted — all truthiness is handled natively.
        """
        visited = self.visit(node)
        # Guard against invalid 'if if A then B else C then' syntax: wrap ternaries.
        if isinstance(visited, last.IfElseExpression):
            visited = last.UnaryOperation(operator="(", operand=visited)
        return visited

    def _is_enum(self, node: last.Node) -> bool:
        """Determines if a Luau node represents a Roblox Enum item."""
        if isinstance(node, last.MemberAccess):
            if isinstance(node.receiver, last.Variable) and node.receiver.name == "Enum":
                return True
            return self._is_enum(node.receiver)
        elif isinstance(node, last.Variable):
            return node.name.startswith("Enum.")
        return False

    def visit_IRIfStatement(self, node: ir.IRIfStatement) -> last.IfStatement:
        elif_blocks = []
        for cond, body in node.elif_blocks:
            elif_blocks.append((
                self._wrap_bool(cond),
                self._visit_block(body)
            ))

        return last.IfStatement(
            condition=self._wrap_bool(node.condition),
            then_block=self._visit_block(node.then_block),
            elif_blocks=elif_blocks,
            else_block=self._visit_block(node.else_block) if node.else_block else None,
            lineno=node.lineno
        )

    def visit_IRWhileStatement(self, node: ir.IRWhileStatement) -> last.WhileStatement:
        # Pushing a scope ensures that variables defined only in the loop 
        # (like those first assigned in the loop body) don't leak out 
        # unless they were identified as escaped.
        self._declaration_stack.append(set())
        body = self._visit_block(node.body)
        self._declaration_stack.pop()
        
        return last.WhileStatement(
            condition=self._wrap_bool(node.condition),
            body=body,
            lineno=node.lineno
        )

    def visit_IRGenericForStatement(self, node: ir.IRGenericForStatement) -> last.GenericForStatement:
        iterator = self.visit(node.iterator)
        vars = list(node.vars)  # copy so we don't mutate the IR node
        
        # Pushing a scope ensures loop variables are isolated to the loop body
        loop_vars_scope = set()
        for v in vars:
            if isinstance(v, ir.IRVariable):
                 self._loop_vars.add(v.name)
                 loop_vars_scope.add(v.name)
            elif isinstance(v, str) and v != "_":
                 self._loop_vars.add(v)
                 loop_vars_scope.add(v)
        
        self._declaration_stack.append(loop_vars_scope)

        # ipairs produces (index, value). Wrap Roblox array returns in ipairs() idiomatically.
        is_array_iter = (
            (isinstance(node.iterator, ir.IRMethodCall) and
             node.iterator.method in ("get_children", "GetChildren", "get_descendants", "GetDescendants")) or
            isinstance(node.iterator, ir.IRTableLiteral)
        )
        if is_array_iter:
            iterator = last.FunctionCall(func=last.Variable("ipairs"), args=[iterator])
            # If caller only declared 1 loop var, they want the value — prepend _ for index.
            if len(vars) == 1:
                vars = ["_"] + vars
            elif len(vars) == 2:
                # If the index (first var) is never used in the body, use '_' for idiomatic Luau
                if not self._is_var_used(vars[0], node.body):
                    vars[0] = "_"

        # pairs() is correct for dict iteration (Python .items()); already emitted by visit_IRMethodCall.
        # For any remaining FunctionCall(pairs, ...) with 1 var, also prepend _.
        elif isinstance(iterator, last.FunctionCall) and isinstance(iterator.func, last.Variable):
            if iterator.func.name == "ipairs" and len(vars) == 1:
                vars = ["_"] + vars

        body = self._visit_block(node.body)
        self._declaration_stack.pop()

        return last.GenericForStatement(
            vars=vars,
            iterator=iterator,
            body=body,
            var_types=node.var_types if self.flags.typed else [],
            lineno=node.lineno
        )

    def visit_IRForStatement(self, node: ir.IRForStatement) -> last.ForStatement:
        # Python range(stop) is exclusive, Luau for is inclusive.
        # stop -> stop - 1
        stop_val = self.visit(node.end)
        stop_expr = last.BinaryOperation(
            left=stop_val,
            operator="-",
            right=last.Literal(1)
        )
        return last.ForStatement(
            var=node.var,
            start=self.visit(node.start),
            end=stop_expr,
            step=self.visit(node.step) if node.step else None,
            body=self._visit_block(node.body)
        )

    def visit_IRReturnStatement(self, node: ir.IRReturnStatement) -> List[last.Statement]:
        if node.value is None:
            # Suppress bare 'return' at end of void functions — Luau doesn't require it.
            return [last.ReturnStatement(value=None, is_bare=True)]

        visited_value = self.visit(node.value)
        # Luau doesn't allow 'return x = 1'. Split into assignment + return.
        if isinstance(visited_value, (last.Assignment, last.CompoundAssignment, last.LocalAssign)):
            return [visited_value, last.ReturnStatement(value=None, is_bare=True)]

        return [last.ReturnStatement(
            value=visited_value,
            lineno=node.lineno
        )]

    def visit_IRMultiReturn(self, node: ir.IRMultiReturn) -> last.MultiReturnStatement:
        return last.MultiReturnStatement(
            values=[self.visit(v) for v in node.values],
            lineno=node.lineno
        )

    def visit_IRBreakStatement(self, node: ir.IRBreakStatement) -> last.BreakStatement:
        return last.BreakStatement()

    def visit_IRContinueStatement(self, node: ir.IRContinueStatement) -> last.ContinueStatement:
        return last.ContinueStatement()

    def visit_IRPassStatement(self, node: ir.IRPassStatement) -> None:
        return None

    def visit_IRClassDef(self, node: ir.IRClassDef) -> last.Node:
        old_class = getattr(self, "_current_class", None)
        self._current_class = node
        
        class_name = node.name
        results = []
        
        # 1. Class table creation with initial fields (class attributes)
        class_fields = []
        for m in node.body:
            if isinstance(m, ir.IRAssignment):
                assign_node: ir.IRAssignment = m
                if isinstance(assign_node.target, ir.IRVariable):
                    class_fields.append((assign_node.target.name, self.visit(assign_node.value)))
        
        if node.bases:
            # We support multiple inheritance by chaining metatables.
            # Base1 is the primary metatable for the class table.
            primary_base = self.visit(node.bases[0])
            primary_base_str = self._print_node(primary_base)
            results.append(last.LocalAssign(
                name=class_name, 
                value=last.FunctionCall(
                    func=last.Variable("setmetatable"), 
                    args=[last.TableLiteral(fields=class_fields), last.Variable(primary_base_str)]
                )
            ))
            # If there are additional bases, we'd ideally chain them in a custom __index, 
            # but for now we'll support the first base as the primary ancestor for performance.
        else:
            results.append(last.LocalAssign(name=class_name, value=last.TableLiteral(fields=class_fields)))
            
        # 2. Method types and __index setup
        has_properties = any("property" in m.decorators for m in node.body if isinstance(m, ir.IRFunctionDef))
        method_types = {}
        for m in node.body:
            if isinstance(m, ir.IRFunctionDef):
                if "classmethod" in m.decorators:
                    method_types[m.name] = "classmethod"
                elif "staticmethod" in m.decorators:
                    method_types[m.name] = "staticmethod"

        # Emit _method_types table (with inheritance if applicable)
        mt_fields = [(name, last.Literal(val)) for name, val in method_types.items()]
        mt_init = last.TableLiteral(fields=mt_fields)
        if node.bases:
            # Chain _method_types from all bases.
            # We'll use the primary base's _method_types as the __index.
            primary_base = self.visit(node.bases[0])
            primary_base_str = self._print_node(primary_base)
            results.append(last.Assignment(
                target=last.Variable(f"{class_name}._method_types"),
                value=last.FunctionCall(
                    func=last.Variable("setmetatable"),
                    args=[mt_init, last.TableLiteral(fields=[("__index", last.Variable(f"{primary_base_str}._method_types"))])]
                )
            ))
        else:
            results.append(last.Assignment(target=last.Variable(f"{class_name}._method_types"), value=mt_init))

        # Build __index
        idx_body = []
        if has_properties:
            idx_body.extend([
                last.LocalAssign(name="getter", value=last.IndexOperation(
                    receiver=last.Variable(class_name),
                    index=last.BinaryOperation(left=last.Literal("get_"), operator="..", right=last.Variable("key"))
                )),
                last.IfStatement(
                    condition=last.Variable("getter"),
                    then_block=[last.ReturnStatement(value=last.FunctionCall(func=last.Variable("getter"), args=[last.Variable("self")]))]
                )
            ])
        
        if method_types or node.bases:
            # Enhanced __index logic for class/static methods and multiple inheritance
            idx_body.append(last.LocalAssign(name="val", value=last.IndexOperation(receiver=last.Variable(class_name), index=last.Variable("key"))))
            
            # If not found in current class, search secondary bases for multiple inheritance
            if len(node.bases) > 1:
                # We'll generate a loop over secondary bases for cleaner Luau
                secondary_bases = []
                for i in range(1, len(node.bases)):
                    secondary_bases.append(self.visit(node.bases[i]))
                
                # For simplicity and performance, we'll just emit them sequentially for now 
                # but use a more robust search pattern.
                for base in secondary_bases:
                    base_str = self._print_node(base)
                    idx_body.append(last.IfStatement(
                        condition=last.BinaryOperation(left=last.Variable("val"), operator="==", right=last.Literal(None)),
                        then_block=[last.Assignment(target=last.Variable("val"), value=last.IndexOperation(receiver=last.Variable(base_str), index=last.Variable("key")))]
                    ))

            idx_body.extend([
                last.IfStatement(
                    condition=last.BinaryOperation(left=last.FunctionCall(func=last.Variable("type"), args=[last.Variable("val")]), operator="==", right=last.Literal("function")),
                    then_block=[
                        last.LocalAssign(name="mtype", value=last.IndexOperation(receiver=last.Variable(f"{class_name}._method_types"), index=last.Variable("key"))),
                        last.IfStatement(
                            condition=last.BinaryOperation(left=last.Variable("mtype"), operator="==", right=last.Literal("classmethod")),
                            then_block=[
                                last.ReturnStatement(value=last.Lambda(
                                    args=["..."],
                                    body=[last.ReturnStatement(value=last.FunctionCall(
                                        func=last.Variable("val"),
                                        args=[last.Variable(class_name), last.Variable("...")]
                                    ))]
                                ))
                            ]
                        ),
                        last.IfStatement(
                            condition=last.BinaryOperation(left=last.Variable("mtype"), operator="==", right=last.Literal("staticmethod")),
                            then_block=[last.ReturnStatement(value=last.Variable("val"))]
                        )
                    ]
                ),
                last.ReturnStatement(value=last.Variable("val"))
            ])
            
        if not idx_body:
            # Standard __index
            results.append(last.Assignment(
                target=last.Variable(f"{class_name}.__index"),
                value=last.Variable(class_name)
            ))
            results.append(last.Assignment(
                target=last.Variable(f"{class_name}.__call__"),
                value=last.Lambda(
                    args=["self", "..."],
                    body=[last.ReturnStatement(value=last.MethodCall(receiver=last.Variable("self"), method="__call__", args=[last.Variable("...")]))]
                )
            ))
            results.append(last.Assignment(
                target=last.Variable(f"{class_name}.__iter__"),
                value=last.Lambda(
                    args=["self"],
                    body=[last.ReturnStatement(value=last.MethodCall(receiver=last.Variable("self"), method="__iter__", args=[]))]
                )
            ))
        else:
            # Full __index
            if not any(isinstance(s, last.ReturnStatement) for s in idx_body):
                 idx_body.append(last.ReturnStatement(value=last.IndexOperation(receiver=last.Variable(class_name), index=last.Variable("key"))))
            
            results.append(last.FunctionDef(
                name=f"{class_name}.__index",
                args=["self", "key"],
                body=idx_body,
                is_local=False
            ))
            results.append(last.Assignment(
                target=last.Variable(f"{class_name}.__call__"),
                value=last.Lambda(
                    args=["self", "..."],
                    body=[last.ReturnStatement(value=last.MethodCall(receiver=last.Variable("self"), method="__call__", args=[last.Variable("...")]))]
                )
            ))
            results.append(last.Assignment(
                target=last.Variable(f"{class_name}.__iter__"),
                value=last.Lambda(
                    args=["self"],
                    body=[last.ReturnStatement(value=last.MethodCall(receiver=last.Variable("self"), method="__iter__", args=[]))]
                )
            ))
        
        # 3. .new() constructor
        new_body = [
            last.LocalAssign(
                name="self", 
                value=last.FunctionCall(
                    func=last.Variable("setmetatable"), 
                    args=[last.TableLiteral(), last.Variable(class_name)]
                )
            ),
            last.IfStatement(
                condition=last.Variable("self.__init__"),
                then_block=[
                    last.MethodCall(receiver=last.Variable("self"), method="__init__", args=[last.Variable("...")])
                ]
            ),
            last.ReturnStatement(value=last.Variable("self"))
        ]
        results.append(last.FunctionDef(
            name=f"{class_name}.new",
            args=["..."],
            body=new_body,
            is_local=False
        ))
        
        # 4. Methods
        for m in node.body:
            if isinstance(m, ir.IRFunctionDef):
                # Ensure decorators is a list if it somehow isn't (from older IR versions)
                decs = getattr(m, "decorators", [])
                is_static = "staticmethod" in decs
                is_class = "classmethod" in decs
                is_prop = "property" in decs
                
                fn_name = f"{class_name}." + (f"get_{m.name}" if is_prop else m.name)
                
                # In Luau, we just define them as Class.Method.
                # The caller (super() or direct) decides how to call them.
                
                results.append(last.FunctionDef(
                    name=fn_name,
                    args=m.args,
                    body=self._visit_block(m.body),
                    is_local=False,
                    arg_types=m.arg_types if self.flags.typed else [],
                    return_type=m.return_type if self.flags.typed else None,
                    lineno=m.lineno
                ))
            
        self._current_class = old_class
        return last.Block(results)

    def _is_var_used(self, name: str, body: List[ir.IRStatement]) -> bool:
        """Heuristic to check if a variable name is used within a block of IR statements."""
        found = [False]
        def check(node):
            if found[0]: return
            if isinstance(node, ir.IRVariable) and node.name == name:
                found[0] = True
            elif hasattr(node, "__dict__"):
                for v in node.__dict__.values():
                    if isinstance(v, (ir.IRNode, list, dict)):
                        if isinstance(v, list):
                            for item in v: check(item)
                        elif isinstance(v, dict):
                            for item in v.values(): check(item)
                        else: check(v)
        for stmt in body:
            check(stmt)
            if found[0]: break
        return found[0]

    def visit_IRSuperCall(self, node: ir.IRSuperCall) -> last.Node:
        curr_class = self._current_class
        if curr_class is None:
            raise UnsupportedFeatureError("super() call outside of a class")
        
        if not curr_class.bases:
            # Fallback for dynamic bases if we want, but for now strict
            raise UnsupportedFeatureError("super() used in a class with no bases")
            
        # We assume the first base is the target for super()
        base_name = self._print_node(self.visit(curr_class.bases[0]))
        args = [last.Variable("self")] + [self.visit(arg) for arg in node.args]
        
        return last.FunctionCall(
            func=last.Variable(f"{base_name}.{node.method}"),
            args=args
        )

    def visit_IRComment(self, node: ir.IRComment) -> last.Comment:
        return last.Comment(text=node.text)

    def visit_IRImport(self, node: ir.IRImport) -> last.Statement:
        # Python-only modules have no Luau equivalent — skip them silently
        PYTHON_ONLY_MODULES = {"typing", "types", "abc", "dataclasses", "functools", "collections", "contextlib", "__future__", "roblox"}
        mod_root = node.module.split(".")[0]
        if mod_root in PYTHON_ONLY_MODULES:
            return last.Comment(text=f"Python-only import '{node.module}' omitted")

        module_path = node.module.replace(".", "/")
        alias_name = node.alias or node.module.split(".")[-1]
        
        req_call = last.FunctionCall(
            func=last.Variable("_G.__rpy_require" if not self.flags.shared_runtime else "require"),
            args=[last.Literal(module_path)]
        )
        return last.LocalAssign(name=alias_name, value=req_call)

    def visit_IRImportFrom(self, node: ir.IRImportFrom) -> last.Statement:
        # Python-only modules have no Luau equivalent — skip them silently
        PYTHON_ONLY_MODULES = {"typing", "types", "abc", "dataclasses", "functools", "collections", "contextlib", "__future__", "roblox"}
        mod_root = (node.module or "").split(".")[0]
        if mod_root in PYTHON_ONLY_MODULES:
            return last.Comment(text=f"Python-only import 'from {node.module} import ...' omitted")

        # Example: from x.y import z as w
        # Luau: local _mod = _G.__rpy_require("x/y"); local w = _mod.z
        module_path = node.module.replace(".", "/")
        
        results = []
        mod_var = f"_module_{node.module.replace('.', '_')}"
        
        # Require the module once
        req_call = last.FunctionCall(
            func=last.Variable("_G.__rpy_require" if not self.flags.shared_runtime else "require"),
            args=[last.Literal(module_path)]
        )
        results.append(last.LocalAssign(name=mod_var, value=req_call))
        
        # Extract imported names
        for i, name in enumerate(node.names):
            alias = node.aliases[i] if (hasattr(node, "aliases") and node.aliases and i < len(node.aliases) and node.aliases[i] is not None) else name
            prop_acc = last.IndexOperation(
                receiver=last.Variable(mod_var),
                index=last.Literal(name)
            )
            results.append(last.LocalAssign(name=alias, value=prop_acc))
            
        return last.Block(results)

    def generic_visit(self, node: ir.IRNode) -> Any:
        raise UnsupportedFeatureError(f"Node {type(node).__name__} unsupported in Luau generation")

    def _print_node(self, node: last.Node) -> str:
        printer = LuauPrinter()
        return printer.print_node(node)

def generate(result: TransformResult, flags: CompilerFlags, module_name: str = "unknown_module") -> GenerateResult:
    gen = IRToLuauGenerator(flags)
    luau_ast = gen.generate(result.ir_module, module_name)
    
    # Apply peephole optimizations
    from transpiler.optimization.peephole import optimize
    luau_ast = optimize(luau_ast)
    
    printer = LuauPrinter()
    code = printer.print_node(luau_ast)
    line_map = printer.line_map
    
    if not flags.no_runtime:
        snippets = get_used_snippets(gen.runtime_used)
        # Shift line map if we add a header
        header_lines = 1
        if snippets:
            header_lines += snippets.count("\n") + 2
            
        shifted_map = {k + header_lines: v for k, v in line_map.items()}
        
        header = "-- Generated by RPy — do not edit manually\n"
        if gen.is_native:
            header = "--!native\n" + header
            
        code = header + (snippets + "\n\n" if snippets else "") + code
        return GenerateResult(code=code, runtime_helpers=gen.runtime_used, line_map=shifted_map)
        
    return GenerateResult(code=code, runtime_helpers=gen.runtime_used, line_map=line_map)