//! AST to bytecode compiler.

use std::collections::HashMap;
use std::sync::Arc;

use tishlang_ast::{
    ArrayElement, ArrowBody, BinOp, CallArg, DestructElement, DestructPattern, ExportDeclaration,
    Expr, FunParam, JsxAttrValue, JsxChild, JsxProp, Literal, LogicalAssignOp, MemberProp,
    ObjectProp, Program, Span, Statement,
};

use crate::chunk::{Chunk, Constant};
use crate::encoding::{binop_to_u8, compound_op_to_u8, unaryop_to_u8};
use crate::opcode::Opcode;

enum SimpleMapResult {
    Identity,
    BinOp(BinOp, Constant, bool), // op, constant, param_on_left
}

fn literal_to_constant(expr: &Expr) -> Option<Constant> {
    if let Expr::Literal { value, .. } = expr {
        Some(match value {
            Literal::Number(n) => Constant::Number(*n),
            Literal::String(s) => Constant::String(Arc::clone(s)),
            Literal::Bool(b) => Constant::Bool(*b),
            Literal::Null => Constant::Null,
        })
    } else {
        None
    }
}

#[derive(Debug)]
pub struct CompileError {
    pub message: String,
}

impl std::fmt::Display for CompileError {
    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
        write!(f, "{}", self.message)
    }
}

impl std::error::Error for CompileError {}

/// Loop boundary for break/continue.
struct LoopInfo {
    break_patches: Vec<usize>,
    /// Operand positions for `continue`: either `JumpBack` (while / do-while / for-of) or `Jump`
    /// (C-style `for`, where the update clause is emitted after the body).
    continue_patches: Vec<usize>,
    /// When true, [`Opcode::Jump`] placeholders in `continue_patches` are patched forward with
    /// [`Self::patch_jump`]. When false, they are [`Opcode::JumpBack`] patched with
    /// [`Self::patch_jump_back`].
    continue_is_forward_jump: bool,
}

/// Switch boundary: break exits the switch.
struct SwitchInfo {
    break_patches: Vec<usize>,
}

/// Innermost break/continue target for unwinding `EnterBlock` before a jump.
#[derive(Clone, Copy)]
enum Breakable {
    /// `usize` = `block_depth` before the loop body (same as Continue unwind target).
    Loop { unwind_depth: usize },
    /// `usize` = `block_depth` before the switch statement.
    Switch { unwind_depth: usize },
}

struct Compiler<'a> {
    chunk: &'a mut Chunk,
    /// Current scope: variable name -> (depth, is_captured). Depth 0 = local.
    scope: Vec<HashMap<Arc<str>, bool>>,
    /// Stack of loop info for break/continue.
    loop_stack: Vec<LoopInfo>,
    switch_stack: Vec<SwitchInfo>,
    /// Parallel to nested loops/switches: innermost target for break/continue block unwind.
    breakable_stack: Vec<Breakable>,
    /// Nesting depth of emitted `EnterBlock` (lexical blocks) not yet closed on the compile path.
    block_depth: usize,
    /// When true (REPL mode), last ExprStmt leaves its value on the stack and we skip trailing LoadConst Null.
    retain_last_expr: bool,
}

impl<'a> Compiler<'a> {
    fn new(chunk: &'a mut Chunk, retain_last_expr: bool) -> Self {
        Self {
            chunk,
            scope: vec![HashMap::new()],
            loop_stack: Vec::new(),
            switch_stack: Vec::new(),
            breakable_stack: Vec::new(),
            block_depth: 0,
            retain_last_expr,
        }
    }

    fn emit_exit_blocks_until_depth(&mut self, target_depth: usize) {
        let n = self.block_depth.saturating_sub(target_depth);
        for _ in 0..n {
            self.emit(Opcode::ExitBlock);
        }
    }

    /// C-style `for` init: bindings are not inside the `{ ... }` body for block-undo purposes.
    /// Formal parameters as VM slot names plus optional destructure patterns (one per formal).
    fn plan_function_params(
        params: &[FunParam],
    ) -> Result<(Vec<Arc<str>>, Vec<Option<DestructPattern>>), CompileError> {
        let mut names = Vec::with_capacity(params.len());
        let mut slots: Vec<Option<DestructPattern>> = Vec::with_capacity(params.len());
        let mut syn_counter = 0u32;
        for p in params {
            match p {
                FunParam::Simple(tp) => {
                    names.push(Arc::clone(&tp.name));
                    slots.push(None);
                }
                FunParam::Destructure {
                    pattern, default, ..
                } => {
                    if default.is_some() {
                        return Err(CompileError {
                            message: "Default values on destructuring parameters are not supported in bytecode"
                                .to_string(),
                        });
                    }
                    names.push(Arc::from(format!("__param_{}", syn_counter)));
                    syn_counter += 1;
                    slots.push(Some(pattern.clone()));
                }
            }
        }
        Ok((names, slots))
    }

    /// After VM binds positional args to `param_names`, load each destructure slot and bind pattern locals.
    fn emit_param_destructure_prologue(
        &mut self,
        param_names: &[Arc<str>],
        slots: &[Option<DestructPattern>],
    ) -> Result<(), CompileError> {
        debug_assert_eq!(param_names.len(), slots.len());
        for (name, slot) in param_names.iter().zip(slots.iter()) {
            if let Some(pattern) = slot {
                let idx = self.name_idx(name);
                self.emit_u16(Opcode::LoadVar, idx);
                self.compile_destructure(pattern, false, false)?;
            }
        }
        Ok(())
    }

    fn compile_for_init_statement(&mut self, stmt: &Statement) -> Result<(), CompileError> {
        match stmt {
            Statement::VarDecl {
                name,
                init,
                mutable: _,
                ..
            } => {
                if let Some(expr) = init {
                    self.compile_expr(expr)?;
                } else {
                    let idx = self.constant_idx(Constant::Null);
                    self.emit(Opcode::LoadConst);
                    self.chunk.write_u16(idx);
                }
                let idx = self.name_idx(name);
                self.emit_u16(Opcode::DeclareVarPlain, idx);
                self.scope
                    .last_mut()
                    .unwrap()
                    .insert(Arc::clone(name), false);
            }
            Statement::VarDeclDestructure { pattern, init, .. } => {
                self.compile_expr(init)?;
                self.compile_destructure(pattern, false, true)?;
            }
            _ => self.compile_statement(stmt)?,
        }
        Ok(())
    }

    fn name_idx(&mut self, name: &Arc<str>) -> u16 {
        self.chunk.add_name(Arc::clone(name))
    }

    fn constant_idx(&mut self, c: Constant) -> u16 {
        self.chunk.add_constant(c)
    }

    fn emit(&mut self, op: Opcode) {
        self.chunk.write_u8(op as u8);
    }

    fn emit_u8(&mut self, op: Opcode, v: u8) {
        self.chunk.write_u8(op as u8);
        self.chunk.write_u16(v as u16);
    }

    fn emit_u16(&mut self, op: Opcode, v: u16) {
        self.chunk.write_u8(op as u8);
        self.chunk.write_u16(v);
    }

    fn emit_jump(&mut self, op: Opcode) -> usize {
        let pos = self.chunk.code.len();
        self.chunk.write_u8(op as u8);
        self.chunk.write_u16(0); // placeholder
        pos + 1
    }

    /// Emit JumpBack with placeholder distance; patch later with patch_jump_back.
    fn emit_jump_back(&mut self) -> usize {
        let pos = self.chunk.code.len();
        self.chunk.write_u8(Opcode::JumpBack as u8);
        self.chunk.write_u16(0);
        pos + 1
    }

    fn patch_jump(&mut self, patch_pos: usize, target: usize) {
        let base = patch_pos + 2;
        let jump_offset = (target as i32).wrapping_sub(base as i32);
        let bytes = (jump_offset as i16).to_be_bytes();
        self.chunk.code[patch_pos] = bytes[0];
        self.chunk.code[patch_pos + 1] = bytes[1];
    }

    /// Patch a JumpBack operand: distance from the IP after this insn back to `target`.
    /// `patch_pos` is the first byte of the u16 operand (same as [`Self::emit_jump_back`]'s return value).
    fn patch_jump_back(&mut self, patch_pos: usize, target: usize) {
        let after_insn = patch_pos + 2;
        let dist = after_insn.saturating_sub(target);
        let bytes = (dist as u16).to_be_bytes();
        self.chunk.code[patch_pos] = bytes[0];
        self.chunk.code[patch_pos + 1] = bytes[1];
    }

    /// Detect property-based numeric sort: (a, b) => a.prop - b.prop or (a, b) => b.prop - a.prop.
    /// Returns Some((prop_name, asc)) or None.
    fn detect_property_sort_comparator(expr: &Expr) -> Option<(Arc<str>, bool)> {
        if let Expr::ArrowFunction { params, body, .. } = expr {
            if params.len() != 2 {
                return None;
            }
            let (param_a, param_b) = match (&params[0], &params[1]) {
                (FunParam::Simple(a), FunParam::Simple(b))
                    if a.default.is_none() && b.default.is_none() =>
                {
                    (a.name.as_ref(), b.name.as_ref())
                }
                _ => return None,
            };
            let body_expr = match body {
                ArrowBody::Expr(e) => e.as_ref(),
                ArrowBody::Block(stmt) => {
                    if let Statement::ExprStmt { expr: e, .. } = stmt.as_ref() {
                        e
                    } else {
                        return None;
                    }
                }
            };
            if let Expr::Binary {
                left,
                op: BinOp::Sub,
                right,
                ..
            } = body_expr
            {
                if let (
                    Expr::Member {
                        object: lo,
                        prop: MemberProp::Name { name: p, .. },
                        ..
                    },
                    Expr::Member {
                        object: ro,
                        prop: MemberProp::Name { name: pr, .. },
                        ..
                    },
                ) = (left.as_ref(), right.as_ref())
                {
                    if p != pr {
                        return None;
                    }
                    if let (Expr::Ident { name: ln, .. }, Expr::Ident { name: rn, .. }) =
                        (lo.as_ref(), ro.as_ref())
                    {
                        if ln.as_ref() == param_a && rn.as_ref() == param_b {
                            return Some((Arc::clone(p), true));
                        }
                        if ln.as_ref() == param_b && rn.as_ref() == param_a {
                            return Some((Arc::clone(p), false));
                        }
                    }
                }
            }
        }
        None
    }

    /// Detect numeric sort comparator: (a, b) => a - b (asc) or (a, b) => b - a (desc).
    fn detect_numeric_sort_comparator(expr: &Expr) -> Option<bool> {
        if let Expr::ArrowFunction { params, body, .. } = expr {
            if params.len() != 2 {
                return None;
            }
            let (param_a, param_b) = match (&params[0], &params[1]) {
                (FunParam::Simple(a), FunParam::Simple(b))
                    if a.default.is_none() && b.default.is_none() =>
                {
                    (a.name.as_ref(), b.name.as_ref())
                }
                _ => return None,
            };
            let body_expr = match body {
                ArrowBody::Expr(e) => e.as_ref(),
                ArrowBody::Block(stmt) => {
                    if let Statement::ExprStmt { expr: e, .. } = stmt.as_ref() {
                        e
                    } else {
                        return None;
                    }
                }
            };
            if let Expr::Binary {
                left,
                op: BinOp::Sub,
                right,
                ..
            } = body_expr
            {
                if let (
                    Expr::Ident {
                        name: left_name, ..
                    },
                    Expr::Ident {
                        name: right_name, ..
                    },
                ) = (left.as_ref(), right.as_ref())
                {
                    if left_name.as_ref() == param_a && right_name.as_ref() == param_b {
                        return Some(true);
                    }
                    if left_name.as_ref() == param_b && right_name.as_ref() == param_a {
                        return Some(false);
                    }
                }
            }
        }
        None
    }

    /// Detect simple map callback: x => x (identity) or x => x op const / x => const op x.
    /// Returns SimpleMapResult for map optimization.
    fn detect_simple_map_callback(expr: &Expr) -> Option<SimpleMapResult> {
        let (params, body) = match expr {
            Expr::ArrowFunction { params, body, .. } => (params, body),
            _ => return None,
        };
        if params.len() != 1 {
            return None;
        }
        let param_name = match &params[0] {
            FunParam::Simple(tp) if tp.default.is_none() => tp.name.as_ref(),
            _ => return None,
        };
        let expr_ref: &Expr = match body {
            ArrowBody::Expr(e) => e.as_ref(),
            ArrowBody::Block(stmt) => {
                let s = stmt.as_ref();
                if let Statement::Return {
                    value: Some(ref e), ..
                } = s
                {
                    e
                } else if let Statement::ExprStmt { expr: ref e, .. } = s {
                    e
                } else {
                    return None;
                }
            }
        };
        // Identity: x => x
        if let Expr::Ident { name, .. } = expr_ref {
            if name.as_ref() == param_name {
                return Some(SimpleMapResult::Identity);
            }
        }
        // Binary: x op const or const op x
        if let Expr::Binary {
            left, op, right, ..
        } = expr_ref
        {
            let left_is_param =
                matches!(left.as_ref(), Expr::Ident { name, .. } if name.as_ref() == param_name);
            let right_is_param =
                matches!(right.as_ref(), Expr::Ident { name, .. } if name.as_ref() == param_name);
            let left_is_literal = matches!(left.as_ref(), Expr::Literal { .. });
            let right_is_literal = matches!(right.as_ref(), Expr::Literal { .. });
            if left_is_param && right_is_literal {
                if let Some(c) = literal_to_constant(right.as_ref()) {
                    return Some(SimpleMapResult::BinOp(*op, c, true));
                }
            }
            if left_is_literal && right_is_param {
                if let Some(c) = literal_to_constant(left.as_ref()) {
                    return Some(SimpleMapResult::BinOp(*op, c, false));
                }
            }
        }
        None
    }

    /// Detect simple filter callback: x => x op const or x => const op x (comparison that returns bool).
    fn detect_simple_filter_callback(expr: &Expr) -> Option<(BinOp, Constant, bool)> {
        let (params, body) = match expr {
            Expr::ArrowFunction { params, body, .. } => (params, body),
            _ => return None,
        };
        if params.len() != 1 {
            return None;
        }
        let param_name = match &params[0] {
            FunParam::Simple(tp) if tp.default.is_none() => tp.name.as_ref(),
            _ => return None,
        };
        let expr_ref: &Expr = match body {
            ArrowBody::Expr(e) => e.as_ref(),
            ArrowBody::Block(stmt) => {
                let s = stmt.as_ref();
                if let Statement::Return {
                    value: Some(ref e), ..
                } = s
                {
                    e
                } else if let Statement::ExprStmt { expr: ref e, .. } = s {
                    e
                } else {
                    return None;
                }
            }
        };
        if let Expr::Binary {
            left, op, right, ..
        } = expr_ref
        {
            if !matches!(
                op,
                BinOp::Eq
                    | BinOp::Ne
                    | BinOp::StrictEq
                    | BinOp::StrictNe
                    | BinOp::Lt
                    | BinOp::Le
                    | BinOp::Gt
                    | BinOp::Ge
                    | BinOp::And
                    | BinOp::Or
            ) {
                return None;
            }
            let left_is_param =
                matches!(left.as_ref(), Expr::Ident { name, .. } if name.as_ref() == param_name);
            let right_is_param =
                matches!(right.as_ref(), Expr::Ident { name, .. } if name.as_ref() == param_name);
            let left_is_literal = matches!(left.as_ref(), Expr::Literal { .. });
            let right_is_literal = matches!(right.as_ref(), Expr::Literal { .. });
            if left_is_param && right_is_literal {
                if let Some(c) = literal_to_constant(right.as_ref()) {
                    return Some((*op, c, true));
                }
            }
            if left_is_literal && right_is_param {
                if let Some(c) = literal_to_constant(left.as_ref()) {
                    return Some((*op, c, false));
                }
            }
        }
        None
    }

    fn compile_program(&mut self, program: &Program) -> Result<(), CompileError> {
        let stmts = &program.statements;
        let last_is_expr = self.retain_last_expr
            && stmts
                .last()
                .map(|s| matches!(s, Statement::ExprStmt { .. }))
                .unwrap_or(false);

        if last_is_expr {
            let (rest, last) = stmts.split_at(stmts.len().saturating_sub(1));
            for stmt in rest {
                self.compile_statement(stmt)?;
            }
            if let Some(Statement::ExprStmt { expr, .. }) = last.first() {
                self.compile_expr(expr)?;
            }
        } else {
            for stmt in stmts {
                self.compile_statement(stmt)?;
            }
            let idx = self.constant_idx(Constant::Null);
            self.emit(Opcode::LoadConst);
            self.chunk.write_u16(idx);
        }
        Ok(())
    }

    fn compile_statement(&mut self, stmt: &Statement) -> Result<(), CompileError> {
        match stmt {
            Statement::Block { statements, .. } => {
                self.emit(Opcode::EnterBlock);
                self.block_depth += 1;
                self.scope.push(HashMap::new());
                for s in statements {
                    self.compile_statement(s)?;
                }
                self.scope.pop();
                self.emit(Opcode::ExitBlock);
                self.block_depth -= 1;
            }
            Statement::VarDecl {
                name,
                init,
                mutable: _,
                ..
            } => {
                if let Some(expr) = init {
                    self.compile_expr(expr)?;
                } else {
                    let idx = self.constant_idx(Constant::Null);
                    self.emit(Opcode::LoadConst);
                    self.chunk.write_u16(idx);
                }
                let idx = self.name_idx(name);
                self.emit_u16(Opcode::DeclareVar, idx);
                self.scope
                    .last_mut()
                    .unwrap()
                    .insert(Arc::clone(name), false);
            }
            Statement::VarDeclDestructure { pattern, init, .. } => {
                self.compile_expr(init)?;
                self.compile_destructure(pattern, false, false)?;
            }
            Statement::ExprStmt { expr, .. } => {
                self.compile_expr(expr)?;
                self.emit(Opcode::Pop);
            }
            Statement::If {
                cond,
                then_branch,
                else_branch,
                ..
            } => {
                self.compile_expr(cond)?;
                let jump_else = self.emit_jump(Opcode::JumpIfFalse);
                self.compile_statement(then_branch)?;
                let jump_end = self.emit_jump(Opcode::Jump);
                self.patch_jump(jump_else, self.chunk.code.len());
                if let Some(else_s) = else_branch {
                    self.compile_statement(else_s)?;
                }
                self.patch_jump(jump_end, self.chunk.code.len());
            }
            Statement::While { cond, body, .. } => {
                let start = self.chunk.code.len();
                self.loop_stack.push(LoopInfo {
                    break_patches: Vec::new(),
                    continue_patches: Vec::new(),
                    continue_is_forward_jump: false,
                });
                self.breakable_stack.push(Breakable::Loop {
                    unwind_depth: self.block_depth,
                });
                self.compile_expr(cond)?;
                let jump_out = self.emit_jump(Opcode::JumpIfFalse);
                // JumpIfFalse already pops condition when taking body
                self.compile_statement(body)?;
                let jump_back_dist = (self.chunk.code.len() + 3).saturating_sub(start);
                self.emit_u16(Opcode::JumpBack, jump_back_dist as u16);
                let end = self.chunk.code.len();
                self.patch_jump(jump_out, end);
                let info = self.loop_stack.pop().unwrap();
                self.breakable_stack.pop();
                for p in info.continue_patches {
                    self.patch_jump_back(p, start);
                }
                for p in info.break_patches {
                    self.patch_jump(p, end);
                }
            }
            Statement::For {
                init,
                cond,
                update,
                body,
                ..
            } => {
                self.scope.push(HashMap::new());
                if let Some(i) = init {
                    self.compile_for_init_statement(i.as_ref())?;
                }
                let cond_start = self.chunk.code.len();
                if let Some(c) = cond {
                    self.compile_expr(c)?;
                } else {
                    let idx = self.constant_idx(Constant::Bool(true));
                    self.emit(Opcode::LoadConst);
                    self.chunk.write_u16(idx);
                }
                let jump_out = self.emit_jump(Opcode::JumpIfFalse);
                self.loop_stack.push(LoopInfo {
                    break_patches: Vec::new(),
                    continue_patches: Vec::new(),
                    continue_is_forward_jump: true,
                });
                self.breakable_stack.push(Breakable::Loop {
                    unwind_depth: self.block_depth,
                });
                self.compile_statement(body)?;
                let update_start = self.chunk.code.len();
                if let Some(u) = update {
                    self.compile_expr(u)?;
                    self.emit(Opcode::Pop);
                }
                let info = self.loop_stack.pop().unwrap();
                for p in info.continue_patches {
                    self.patch_jump(p, update_start);
                }
                let jump_back_dist = (self.chunk.code.len() + 3).saturating_sub(cond_start);
                self.emit_u16(Opcode::JumpBack, jump_back_dist as u16);
                let end = self.chunk.code.len();
                self.patch_jump(jump_out, end);
                for p in info.break_patches {
                    self.patch_jump(p, end);
                }
                self.breakable_stack.pop();
                self.scope.pop();
            }
            Statement::ForOf {
                name,
                iterable,
                body,
                ..
            } => {
                self.compile_expr(iterable)?;
                self.scope.push(HashMap::new());
                let arr_name = Arc::from("__forof_arr__");
                let i_name = Arc::from("__forof_i__");
                let len_name = Arc::from("__forof_len__");
                let arr_idx = self.name_idx(&arr_name);
                let i_idx = self.name_idx(&i_name);
                let len_idx = self.name_idx(&len_name);
                let name_idx = self.name_idx(name);
                self.emit_u16(Opcode::DeclareVar, arr_idx);
                self.scope
                    .last_mut()
                    .unwrap()
                    .insert(arr_name.clone(), false);
                self.emit_u16(Opcode::LoadVar, arr_idx);
                let len_name_idx = self.name_idx(&Arc::from("length"));
                self.emit_u16(Opcode::GetMember, len_name_idx);
                self.emit_u16(Opcode::DeclareVar, len_idx);
                self.scope
                    .last_mut()
                    .unwrap()
                    .insert(len_name.clone(), false);
                let zero_idx = self.constant_idx(Constant::Number(0.0));
                self.emit(Opcode::LoadConst);
                self.chunk.write_u16(zero_idx);
                self.emit_u16(Opcode::DeclareVar, i_idx);
                self.scope.last_mut().unwrap().insert(i_name.clone(), false);
                let loop_start = self.chunk.code.len();
                self.loop_stack.push(LoopInfo {
                    break_patches: Vec::new(),
                    continue_patches: Vec::new(),
                    continue_is_forward_jump: false,
                });
                self.breakable_stack.push(Breakable::Loop {
                    unwind_depth: self.block_depth,
                });
                self.emit_u16(Opcode::LoadVar, arr_idx);
                self.emit_u16(Opcode::LoadVar, i_idx);
                self.emit(Opcode::GetIndex);
                self.emit_u16(Opcode::DeclareVar, name_idx);
                self.scope
                    .last_mut()
                    .unwrap()
                    .insert(Arc::clone(name), false);
                self.compile_statement(body)?;
                self.emit_u16(Opcode::LoadVar, i_idx);
                let one_idx = self.constant_idx(Constant::Number(1.0));
                self.emit(Opcode::LoadConst);
                self.chunk.write_u16(one_idx);
                self.emit_u8(Opcode::BinOp, 0);
                self.emit_u16(Opcode::StoreVar, i_idx);
                self.emit_u16(Opcode::LoadVar, i_idx);
                self.emit_u16(Opcode::LoadVar, len_idx);
                self.emit_u8(Opcode::BinOp, 10);
                let jump_out = self.emit_jump(Opcode::JumpIfFalse);
                let jump_back_dist = (self.chunk.code.len() + 3).saturating_sub(loop_start);
                self.emit_u16(Opcode::JumpBack, jump_back_dist as u16);
                let end = self.chunk.code.len();
                self.patch_jump(jump_out, end);
                let info = self.loop_stack.pop().unwrap();
                self.breakable_stack.pop();
                for p in info.continue_patches {
                    self.patch_jump_back(p, loop_start);
                }
                for p in info.break_patches {
                    self.patch_jump(p, end);
                }
                self.scope.pop();
            }
            Statement::Return { value, .. } => {
                if let Some(v) = value {
                    self.compile_expr(v)?;
                } else {
                    let idx = self.constant_idx(Constant::Null);
                    self.emit(Opcode::LoadConst);
                    self.chunk.write_u16(idx);
                }
                self.emit(Opcode::Return);
            }
            Statement::Break { .. } => {
                let unwind_depth = match self.breakable_stack.last() {
                    Some(Breakable::Loop { unwind_depth })
                    | Some(Breakable::Switch { unwind_depth }) => *unwind_depth,
                    None => {
                        return Err(CompileError {
                            message: "break not inside a loop or switch".to_string(),
                        });
                    }
                };
                self.emit_exit_blocks_until_depth(unwind_depth);
                let pos = self.emit_jump(Opcode::Jump);
                match self.breakable_stack.last() {
                    Some(Breakable::Loop { .. }) => {
                        self.loop_stack.last_mut().unwrap().break_patches.push(pos);
                    }
                    Some(Breakable::Switch { .. }) => {
                        self.switch_stack
                            .last_mut()
                            .unwrap()
                            .break_patches
                            .push(pos);
                    }
                    None => {}
                }
            }
            Statement::Continue { .. } => {
                let unwind_depth = self
                    .breakable_stack
                    .iter()
                    .rev()
                    .find_map(|b| match b {
                        Breakable::Loop { unwind_depth } => Some(*unwind_depth),
                        Breakable::Switch { .. } => None,
                    })
                    .ok_or_else(|| CompileError {
                        message: "continue not inside a loop".to_string(),
                    })?;
                self.emit_exit_blocks_until_depth(unwind_depth);
                let forward = self
                    .loop_stack
                    .last()
                    .expect("continue not inside a loop")
                    .continue_is_forward_jump;
                let pos = if forward {
                    self.emit_jump(Opcode::Jump)
                } else {
                    self.emit_jump_back()
                };
                self.loop_stack
                    .last_mut()
                    .expect("continue not inside a loop")
                    .continue_patches
                    .push(pos);
            }
            Statement::FunDecl {
                name,
                params,
                body,
                rest_param,
                async_: _,
                ..
            } => {
                let formal_len = params.len();
                let (mut param_names, slots) = Self::plan_function_params(params)?;
                let mut inner = Chunk::new();
                if let Some(rp) = rest_param {
                    param_names.push(Arc::clone(&rp.name));
                    inner.rest_param_index = (param_names.len() as u16).saturating_sub(1);
                }
                for p in &param_names {
                    inner.add_name(Arc::clone(p));
                }
                inner.param_count = param_names.len() as u16;
                let mut inner_comp = Compiler::new(&mut inner, false);
                inner_comp.scope = vec![param_names
                    .iter()
                    .map(|n| (Arc::clone(n), false))
                    .collect::<HashMap<_, _>>()];
                inner_comp.emit_param_destructure_prologue(&param_names[..formal_len], &slots)?;
                inner_comp.compile_statement(body)?;
                inner_comp.emit(Opcode::LoadConst);
                let idx = inner_comp.constant_idx(Constant::Null);
                inner_comp.chunk.write_u16(idx);
                inner_comp.emit(Opcode::Return);
                let nested_idx = self.chunk.add_nested(inner);
                self.emit(Opcode::LoadConst);
                let idx = self.constant_idx(Constant::Closure(nested_idx));
                self.chunk.write_u16(idx);
                let idx = self.name_idx(name);
                self.emit_u16(Opcode::DeclareVar, idx);
                self.scope
                    .last_mut()
                    .unwrap()
                    .insert(Arc::clone(name), false);
            }
            Statement::DoWhile { body, cond, .. } => {
                let start = self.chunk.code.len();
                self.loop_stack.push(LoopInfo {
                    break_patches: Vec::new(),
                    continue_patches: Vec::new(),
                    continue_is_forward_jump: false,
                });
                self.breakable_stack.push(Breakable::Loop {
                    unwind_depth: self.block_depth,
                });
                self.compile_statement(body)?;
                let cond_start = self.chunk.code.len();
                self.compile_expr(cond)?;
                let jump_back = self.emit_jump(Opcode::JumpIfFalse);
                let jump_back_dist = (self.chunk.code.len() + 3).saturating_sub(start);
                self.emit_u16(Opcode::JumpBack, jump_back_dist as u16);
                let end = self.chunk.code.len();
                self.patch_jump(jump_back, end);
                let info = self.loop_stack.pop().unwrap();
                self.breakable_stack.pop();
                for p in info.continue_patches {
                    self.patch_jump_back(p, cond_start);
                }
                for p in info.break_patches {
                    self.patch_jump(p, end);
                }
            }
            Statement::Switch {
                expr,
                cases,
                default_body,
                ..
            } => {
                let switch_unwind_depth = self.block_depth;
                self.switch_stack.push(SwitchInfo {
                    break_patches: Vec::new(),
                });
                self.breakable_stack.push(Breakable::Switch {
                    unwind_depth: switch_unwind_depth,
                });
                self.compile_expr(expr)?;
                self.emit(Opcode::Dup);
                let mut end_patches = Vec::new();
                for (case_expr, case_body) in cases {
                    self.emit(Opcode::Dup);
                    if let Some(ce) = case_expr {
                        self.compile_expr(ce)?;
                        self.emit_u8(Opcode::BinOp, 8);
                        let jump_next = self.emit_jump(Opcode::JumpIfFalse);
                        // JumpIfFalse already pops the match result when taking this case
                        self.compile_statement(&Statement::Block {
                            statements: case_body.clone(),
                            span: Span {
                                start: (0, 0),
                                end: (0, 0),
                            },
                        })?;
                        let jump_end = self.emit_jump(Opcode::Jump);
                        end_patches.push(jump_end);
                        self.patch_jump(jump_next, self.chunk.code.len());
                    } else {
                        self.emit(Opcode::Pop);
                        self.compile_statement(&Statement::Block {
                            statements: case_body.clone(),
                            span: Span {
                                start: (0, 0),
                                end: (0, 0),
                            },
                        })?;
                    }
                }
                if let Some(body) = default_body {
                    self.emit(Opcode::Pop);
                    self.compile_statement(&Statement::Block {
                        statements: body.clone(),
                        span: Span {
                            start: (0, 0),
                            end: (0, 0),
                        },
                    })?;
                } else {
                    self.emit(Opcode::Pop);
                }
                for p in end_patches {
                    self.patch_jump(p, self.chunk.code.len());
                }
                let sw = self.switch_stack.pop().unwrap();
                self.breakable_stack.pop();
                for p in sw.break_patches {
                    self.patch_jump(p, self.chunk.code.len());
                }
            }
            Statement::Throw { value, .. } => {
                self.compile_expr(value)?;
                self.emit(Opcode::Throw);
            }
            Statement::Try {
                body,
                catch_param,
                catch_body,
                finally_body,
                ..
            } => {
                let catch_offset_pos = self.chunk.code.len();
                self.emit(Opcode::EnterTry);
                self.chunk.write_u16(0);
                self.compile_statement(body)?;
                self.emit(Opcode::ExitTry);
                let jump_over_catch = self.emit_jump(Opcode::Jump);
                let catch_start = self.chunk.code.len();
                if let Some(catch_stmt) = catch_body {
                    if let Some(param) = catch_param {
                        self.emit(Opcode::EnterBlock);
                        self.block_depth += 1;
                        self.scope.push(HashMap::new());
                        let param_idx = self.name_idx(param);
                        self.emit_u16(Opcode::DeclareVar, param_idx);
                        self.scope
                            .last_mut()
                            .unwrap()
                            .insert(Arc::clone(param), false);
                        self.compile_statement(catch_stmt)?;
                        self.scope.pop();
                        self.emit(Opcode::ExitBlock);
                        self.block_depth -= 1;
                    } else {
                        self.emit(Opcode::Pop);
                        self.compile_statement(catch_stmt)?;
                    }
                } else {
                    self.emit(Opcode::Throw);
                }
                let after_catch = self.chunk.code.len();
                self.patch_jump(jump_over_catch, after_catch);
                if let Some(finally) = finally_body {
                    self.compile_statement(finally)?;
                }
                let catch_offset =
                    catch_start.wrapping_sub(catch_offset_pos).wrapping_sub(3) as u16;
                self.chunk.code[catch_offset_pos + 1] = (catch_offset >> 8) as u8;
                self.chunk.code[catch_offset_pos + 2] = (catch_offset & 0xff) as u8;
            }
            Statement::Import { .. } => {
                return Err(CompileError {
                    message: "Import not supported in bytecode".to_string(),
                });
            }
            Statement::Export { declaration, .. } => match declaration.as_ref() {
                ExportDeclaration::Named(inner_stmt) => {
                    self.compile_statement(inner_stmt.as_ref())?;
                }
                ExportDeclaration::Default(_) => {
                    return Err(CompileError {
                        message: "export default is not supported in bytecode".to_string(),
                    });
                }
            },
            Statement::TypeAlias { .. }
            | Statement::DeclareVar { .. }
            | Statement::DeclareFun { .. } => {}
        }
        Ok(())
    }

    fn compile_destructure(
        &mut self,
        pattern: &DestructPattern,
        mutable: bool,
        for_header_binding: bool,
    ) -> Result<(), CompileError> {
        let decl_op = if for_header_binding {
            Opcode::DeclareVarPlain
        } else {
            Opcode::DeclareVar
        };
        match pattern {
            DestructPattern::Array(elements) => {
                for (i, elem) in elements.iter().enumerate() {
                    match elem {
                        Some(DestructElement::Ident(name, _)) => {
                            self.emit(Opcode::Dup);
                            let idx = self.constant_idx(Constant::Number(i as f64));
                            self.emit(Opcode::LoadConst);
                            self.chunk.write_u16(idx);
                            self.emit(Opcode::GetIndex);
                            let idx = self.name_idx(name);
                            self.emit_u16(decl_op, idx);
                            self.scope
                                .last_mut()
                                .unwrap()
                                .insert(Arc::clone(name), false);
                        }
                        _ => {
                            return Err(CompileError {
                                message: "Complex destructuring not yet supported".to_string(),
                            });
                        }
                    }
                }
                self.emit(Opcode::Pop);
            }
            DestructPattern::Object(props) => {
                for prop in props {
                    self.emit(Opcode::Dup);
                    let key_idx = self.constant_idx(Constant::String(Arc::clone(&prop.key)));
                    self.emit(Opcode::LoadConst);
                    self.chunk.write_u16(key_idx);
                    self.emit(Opcode::GetIndex); // GetIndex pops obj, index and uses get_member
                    match &prop.value {
                        DestructElement::Ident(name, _) => {
                            let idx = self.name_idx(name);
                            self.emit_u16(decl_op, idx);
                            if mutable {
                                self.scope
                                    .last_mut()
                                    .unwrap()
                                    .insert(Arc::clone(name), false);
                            }
                        }
                        _ => {
                            return Err(CompileError {
                                message: "Nested object destructuring not yet supported"
                                    .to_string(),
                            });
                        }
                    }
                }
                self.emit(Opcode::Pop);
            }
        }
        Ok(())
    }

    fn compile_expr(&mut self, expr: &Expr) -> Result<(), CompileError> {
        match expr {
            Expr::Literal { value, .. } => {
                let c = match value {
                    Literal::Number(n) => Constant::Number(*n),
                    Literal::String(s) => Constant::String(Arc::clone(s)),
                    Literal::Bool(b) => Constant::Bool(*b),
                    Literal::Null => Constant::Null,
                };
                let idx = self.constant_idx(c);
                self.emit(Opcode::LoadConst);
                self.chunk.write_u16(idx);
            }
            Expr::Ident { name, .. } => {
                let idx = self.name_idx(name);
                self.emit_u16(Opcode::LoadVar, idx);
            }
            Expr::Binary {
                left, op, right, ..
            } => {
                match op {
                    BinOp::And => {
                        // Short-circuit: a && b => if !a then a else b
                        self.compile_expr(left)?;
                        self.emit(Opcode::Dup);
                        let jump_shortcut = self.emit_jump(Opcode::JumpIfFalse);
                        self.compile_expr(right)?; // left still on stack from Dup
                        self.emit_u8(Opcode::BinOp, binop_to_u8(BinOp::And));
                        let jump_end = self.emit_jump(Opcode::Jump);
                        self.patch_jump(jump_shortcut, self.chunk.code.len());
                        self.patch_jump(jump_end, self.chunk.code.len());
                    }
                    BinOp::Or => {
                        // Short-circuit: a || b => if a then a else b
                        self.compile_expr(left)?;
                        self.emit(Opcode::Dup);
                        let jump_eval_right = self.emit_jump(Opcode::JumpIfFalse);
                        let jump_end = self.emit_jump(Opcode::Jump);
                        self.patch_jump(jump_eval_right, self.chunk.code.len());
                        self.emit(Opcode::Pop); // discard falsy left
                        self.compile_expr(right)?;
                        self.patch_jump(jump_end, self.chunk.code.len());
                    }
                    _ => {
                        self.compile_expr(left)?;
                        self.compile_expr(right)?;
                        self.emit_u8(Opcode::BinOp, binop_to_u8(*op));
                    }
                }
            }
            Expr::Unary { op, operand, .. } => {
                self.compile_expr(operand)?;
                self.emit_u8(Opcode::UnaryOp, unaryop_to_u8(*op));
            }
            Expr::Call { callee, args, .. } => {
                // Fast path: arr.sort((a,b)=>a-b) or arr.sort((a,b)=>b-a) -> ArraySortNumeric
                if !args.iter().any(|a| matches!(a, CallArg::Spread(_)))
                    && args.len() == 1
                    && matches!(args[0], CallArg::Expr(_))
                {
                    if let (
                        Expr::Member {
                            object,
                            prop: MemberProp::Name { name: key, .. },
                            optional: false,
                            ..
                        },
                        CallArg::Expr(cmp_expr),
                    ) = (callee.as_ref(), &args[0])
                    {
                        if key.as_ref() == "sort" {
                            if let Some(ascending) = Self::detect_numeric_sort_comparator(cmp_expr)
                            {
                                self.compile_expr(object)?;
                                self.emit_u8(
                                    Opcode::ArraySortNumeric,
                                    if ascending { 0 } else { 1 },
                                );
                                return Ok(());
                            }
                            if let Some((prop, ascending)) =
                                Self::detect_property_sort_comparator(cmp_expr)
                            {
                                self.compile_expr(object)?;
                                let prop_idx = self.constant_idx(Constant::String(prop));
                                self.emit(Opcode::ArraySortByProperty);
                                self.chunk.write_u16(prop_idx);
                                self.chunk.write_u16(if ascending { 0 } else { 1 });
                                return Ok(());
                            }
                        }
                        if key.as_ref() == "map" {
                            if let Some(simple) = Self::detect_simple_map_callback(cmp_expr) {
                                self.compile_expr(object)?;
                                match simple {
                                    SimpleMapResult::Identity => {
                                        self.emit(Opcode::ArrayMapIdentity);
                                    }
                                    SimpleMapResult::BinOp(op, c, param_left) => {
                                        let const_idx = self.constant_idx(c);
                                        self.emit(Opcode::ArrayMapBinOp);
                                        self.chunk.write_u8(binop_to_u8(op));
                                        self.chunk.write_u16(const_idx);
                                        self.chunk.write_u8(if param_left { 0 } else { 1 });
                                    }
                                }
                                return Ok(());
                            }
                        }
                        if key.as_ref() == "filter" {
                            if let Some((op, const_val, param_left)) =
                                Self::detect_simple_filter_callback(cmp_expr)
                            {
                                self.compile_expr(object)?;
                                let const_idx = self.constant_idx(const_val);
                                self.emit(Opcode::ArrayFilterBinOp);
                                self.chunk.write_u8(binop_to_u8(op));
                                self.chunk.write_u16(const_idx);
                                self.chunk.write_u8(if param_left { 0 } else { 1 });
                                return Ok(());
                            }
                        }
                    }
                }
                let has_spread = args.iter().any(|a| matches!(a, CallArg::Spread(_)));
                if has_spread {
                    // Build args array [a, ...b, c], then callee, then CallSpread
                    self.emit_u16(Opcode::NewArray, 0);
                    for arg in args {
                        match arg {
                            CallArg::Expr(e) => {
                                self.compile_expr(e)?;
                                self.emit_u16(Opcode::NewArray, 1);
                                self.emit(Opcode::ConcatArray);
                            }
                            CallArg::Spread(expr) => {
                                self.compile_expr(expr)?;
                                self.emit(Opcode::ConcatArray);
                            }
                        }
                    }
                    self.compile_expr(callee)?;
                    self.emit(Opcode::CallSpread);
                } else {
                    self.compile_expr(callee)?;
                    for arg in args {
                        if let CallArg::Expr(e) = arg {
                            self.compile_expr(e)?;
                        }
                    }
                    self.emit_u16(Opcode::Call, args.len() as u16);
                }
            }
            Expr::Member {
                object,
                prop,
                optional,
                ..
            } => {
                self.compile_expr(object)?;
                if *optional {
                    self.emit(Opcode::Dup);
                    let null_idx = self.constant_idx(Constant::Null);
                    self.emit(Opcode::LoadConst);
                    self.chunk.write_u16(null_idx);
                    self.emit_u8(Opcode::BinOp, 8);
                    let jump_to_null = self.emit_jump(Opcode::JumpIfFalse);
                    let jump_to_get_instr = self.chunk.code.len();
                    let jump_to_get = self.emit_jump(Opcode::Jump);
                    self.patch_jump(jump_to_null, jump_to_get_instr);
                    self.emit(Opcode::Pop);
                    self.emit(Opcode::LoadConst);
                    self.chunk.write_u16(null_idx);
                    let jump_end = self.emit_jump(Opcode::Jump);
                    self.patch_jump(jump_to_get, self.chunk.code.len());
                    match prop {
                        MemberProp::Name { name: key, .. } => {
                            let idx = self.name_idx(key);
                            self.emit_u16(Opcode::GetMemberOptional, idx);
                        }
                        MemberProp::Expr(e) => {
                            self.compile_expr(e)?;
                            self.emit(Opcode::GetIndex);
                        }
                    }
                    self.patch_jump(jump_end, self.chunk.code.len());
                } else {
                    match prop {
                        MemberProp::Name { name: key, .. } => {
                            let idx = self.name_idx(key);
                            self.emit_u16(Opcode::GetMember, idx);
                        }
                        MemberProp::Expr(e) => {
                            self.compile_expr(e)?;
                            self.emit(Opcode::GetIndex);
                        }
                    }
                }
            }
            Expr::Index { object, index, .. } => {
                self.compile_expr(object)?;
                self.compile_expr(index)?;
                self.emit(Opcode::GetIndex);
            }
            Expr::Conditional {
                cond,
                then_branch,
                else_branch,
                ..
            } => {
                self.compile_expr(cond)?;
                let jump_else = self.emit_jump(Opcode::JumpIfFalse);
                // JumpIfFalse pops condition when taking then; when taking else it also pops
                self.compile_expr(then_branch)?;
                let jump_end = self.emit_jump(Opcode::Jump);
                self.patch_jump(jump_else, self.chunk.code.len());
                // no Pop: condition was already popped by JumpIfFalse
                self.compile_expr(else_branch)?;
                self.patch_jump(jump_end, self.chunk.code.len());
            }
            Expr::NullishCoalesce { left, right, .. } => {
                self.compile_expr(left)?;
                self.emit(Opcode::Dup);
                let idx = self.constant_idx(Constant::Null);
                self.emit(Opcode::LoadConst);
                self.chunk.write_u16(idx);
                self.emit_u8(Opcode::BinOp, binop_to_u8(BinOp::StrictNe));
                let jump_to_right = self.emit_jump(Opcode::JumpIfFalse);
                let jump_end = self.emit_jump(Opcode::Jump);
                self.patch_jump(jump_to_right, self.chunk.code.len());
                self.emit(Opcode::Pop);
                self.compile_expr(right)?;
                self.patch_jump(jump_end, self.chunk.code.len());
            }
            Expr::Array { elements, .. } => {
                let has_spread = elements
                    .iter()
                    .any(|e| matches!(e, ArrayElement::Spread(_)));
                if has_spread {
                    // Build array incrementally: start with [], concat each element
                    self.emit_u16(Opcode::NewArray, 0);
                    for elem in elements {
                        match elem {
                            ArrayElement::Expr(e) => {
                                self.compile_expr(e)?;
                                self.emit_u16(Opcode::NewArray, 1);
                                self.emit(Opcode::ConcatArray);
                            }
                            ArrayElement::Spread(expr) => {
                                self.compile_expr(expr)?;
                                self.emit(Opcode::ConcatArray);
                            }
                        }
                    }
                } else {
                    for elem in elements {
                        if let ArrayElement::Expr(e) = elem {
                            self.compile_expr(e)?;
                        }
                    }
                    self.emit_u16(Opcode::NewArray, elements.len() as u16);
                }
            }
            Expr::Object { props, .. } => {
                let has_spread = props.iter().any(|p| matches!(p, ObjectProp::Spread(_)));
                if has_spread {
                    self.emit_u16(Opcode::NewObject, 0); // start with {}
                    for prop in props {
                        match prop {
                            ObjectProp::KeyValue(k, v) => {
                                let idx = self.constant_idx(Constant::String(Arc::clone(k)));
                                self.emit(Opcode::LoadConst);
                                self.chunk.write_u16(idx);
                                self.compile_expr(v)?;
                                self.emit_u16(Opcode::NewObject, 1);
                                self.emit(Opcode::MergeObject);
                            }
                            ObjectProp::Spread(expr) => {
                                self.compile_expr(expr)?;
                                self.emit(Opcode::MergeObject);
                            }
                        }
                    }
                } else {
                    for prop in props {
                        if let ObjectProp::KeyValue(k, v) = prop {
                            let idx = self.constant_idx(Constant::String(Arc::clone(k)));
                            self.emit(Opcode::LoadConst);
                            self.chunk.write_u16(idx);
                            self.compile_expr(v)?;
                        }
                    }
                    self.emit_u16(Opcode::NewObject, props.len() as u16);
                }
            }
            Expr::Assign { name, value, .. } => {
                self.compile_expr(value)?;
                let idx = self.name_idx(name);
                self.emit_u16(Opcode::StoreVar, idx);
                self.emit_u16(Opcode::LoadVar, idx); // assign yields value
            }
            Expr::TypeOf { operand, .. } => {
                let typeof_idx = self.name_idx(&Arc::from("typeof"));
                self.emit_u16(Opcode::LoadGlobal, typeof_idx);
                self.compile_expr(operand)?;
                self.emit_u16(Opcode::Call, 1);
            }
            Expr::ArrowFunction { params, body, .. } => {
                let formal_len = params.len();
                let (param_names, slots) = Self::plan_function_params(params)?;
                let mut inner = Chunk::new();
                for p in &param_names {
                    inner.add_name(Arc::clone(p));
                }
                inner.param_count = param_names.len() as u16;
                let mut inner_comp = Compiler::new(&mut inner, false);
                inner_comp.scope = vec![param_names
                    .iter()
                    .map(|n| (Arc::clone(n), false))
                    .collect::<HashMap<_, _>>()];
                inner_comp.emit_param_destructure_prologue(&param_names[..formal_len], &slots)?;
                match body {
                    ArrowBody::Expr(e) => {
                        inner_comp.compile_expr(e)?;
                        inner_comp.emit(Opcode::Return);
                    }
                    ArrowBody::Block(s) => {
                        inner_comp.compile_statement(s)?;
                        let idx = inner_comp.constant_idx(Constant::Null);
                        inner_comp.emit(Opcode::LoadConst);
                        inner_comp.chunk.write_u16(idx);
                        inner_comp.emit(Opcode::Return);
                    }
                }
                let nested_idx = self.chunk.add_nested(inner);
                let idx = self.constant_idx(Constant::Closure(nested_idx));
                self.emit(Opcode::LoadConst);
                self.chunk.write_u16(idx);
            }
            Expr::TemplateLiteral { quasis, exprs, .. } => {
                if exprs.is_empty() {
                    let s = quasis[0].to_string();
                    let idx = self.constant_idx(Constant::String(Arc::from(s)));
                    self.emit(Opcode::LoadConst);
                    self.chunk.write_u16(idx);
                } else {
                    // Interleave quasis and exprs: quasi[0] + expr[0] + quasi[1] + expr[1] + ... + quasi[n]
                    let first = quasis[0].to_string();
                    let idx = self.constant_idx(Constant::String(Arc::from(first)));
                    self.emit(Opcode::LoadConst);
                    self.chunk.write_u16(idx);
                    for (i, expr) in exprs.iter().enumerate() {
                        self.compile_expr(expr)?;
                        self.emit_u8(Opcode::BinOp, 0); // Add (string concat)
                        let quasi_s = quasis[i + 1].to_string();
                        let qidx = self.constant_idx(Constant::String(Arc::from(quasi_s)));
                        self.emit(Opcode::LoadConst);
                        self.chunk.write_u16(qidx);
                        self.emit_u8(Opcode::BinOp, 0); // Add
                    }
                }
            }
            Expr::PostfixInc { name, .. } => {
                let idx = self.name_idx(name);
                let one = self.constant_idx(Constant::Number(1.0));
                self.emit_u16(Opcode::LoadVar, idx);
                self.emit(Opcode::Dup);
                self.emit(Opcode::LoadConst);
                self.chunk.write_u16(one);
                self.emit_u8(Opcode::BinOp, 0);
                self.emit_u16(Opcode::StoreVar, idx);
            }
            Expr::PostfixDec { name, .. } => {
                let idx = self.name_idx(name);
                let one = self.constant_idx(Constant::Number(1.0));
                self.emit_u16(Opcode::LoadVar, idx);
                self.emit(Opcode::Dup);
                self.emit(Opcode::LoadConst);
                self.chunk.write_u16(one);
                self.emit_u8(Opcode::BinOp, 1);
                self.emit_u16(Opcode::StoreVar, idx);
            }
            Expr::PrefixInc { name, .. } => {
                let idx = self.name_idx(name);
                let one = self.constant_idx(Constant::Number(1.0));
                self.emit_u16(Opcode::LoadVar, idx);
                self.emit(Opcode::LoadConst);
                self.chunk.write_u16(one);
                self.emit_u8(Opcode::BinOp, 0);
                self.emit(Opcode::Dup);
                self.emit_u16(Opcode::StoreVar, idx);
            }
            Expr::PrefixDec { name, .. } => {
                let idx = self.name_idx(name);
                let one = self.constant_idx(Constant::Number(1.0));
                self.emit_u16(Opcode::LoadVar, idx);
                self.emit(Opcode::LoadConst);
                self.chunk.write_u16(one);
                self.emit_u8(Opcode::BinOp, 1);
                self.emit(Opcode::Dup);
                self.emit_u16(Opcode::StoreVar, idx);
            }
            Expr::CompoundAssign {
                name, op, value, ..
            } => {
                let idx = self.name_idx(name);
                self.emit_u16(Opcode::LoadVar, idx);
                self.compile_expr(value)?;
                self.emit_u8(Opcode::BinOp, compound_op_to_u8(*op));
                self.emit(Opcode::Dup);
                self.emit_u16(Opcode::StoreVar, idx);
            }
            Expr::MemberAssign {
                object,
                prop,
                value,
                ..
            } => {
                self.compile_expr(object)?;
                self.compile_expr(value)?;
                let idx = self.name_idx(prop);
                self.emit_u16(Opcode::SetMember, idx); // SetMember pops obj, val and pushes val back
            }
            Expr::IndexAssign {
                object,
                index,
                value,
                ..
            } => {
                self.compile_expr(object)?;
                self.compile_expr(index)?;
                self.compile_expr(value)?;
                self.emit(Opcode::Dup); // leave copy for assignment expression result
                self.emit(Opcode::SetIndex);
            }
            Expr::NativeModuleLoad {
                spec, export_name, ..
            } => {
                let spec_idx = self.constant_idx(Constant::String(Arc::clone(spec)));
                let export_idx = self.constant_idx(Constant::String(Arc::clone(export_name)));
                self.emit(Opcode::LoadNativeExport);
                self.chunk.write_u16(spec_idx);
                self.chunk.write_u16(export_idx);
            }
            Expr::JsxElement {
                tag,
                props,
                children,
                ..
            } => {
                self.compile_jsx_element(tag, props, children)?;
            }
            Expr::JsxFragment { children, .. } => {
                self.compile_jsx_fragment(children)?;
            }
            Expr::Await { operand, .. } => {
                // await expr => evaluate operand, then VM Opcode::AwaitPromise (throw on reject).
                self.compile_expr(operand)?;
                self.emit(Opcode::AwaitPromise);
            }
            Expr::LogicalAssign {
                name, op, value, ..
            } => {
                let idx = self.name_idx(name);
                match op {
                    LogicalAssignOp::OrOr => {
                        // ||= : if current is truthy, keep it; else eval rhs, assign, yield rhs
                        self.emit_u16(Opcode::LoadVar, idx);
                        self.emit(Opcode::Dup);
                        let j_rhs = self.emit_jump(Opcode::JumpIfFalse);
                        let j_end = self.emit_jump(Opcode::Jump);
                        self.patch_jump(j_rhs, self.chunk.code.len());
                        self.emit(Opcode::Pop);
                        self.compile_expr(value)?;
                        self.emit_u16(Opcode::StoreVar, idx);
                        self.emit_u16(Opcode::LoadVar, idx);
                        let end = self.chunk.code.len();
                        self.patch_jump(j_end, end);
                    }
                    LogicalAssignOp::AndAnd => {
                        // &&= : if current is falsy, keep it; else eval rhs, assign, yield rhs
                        self.emit_u16(Opcode::LoadVar, idx);
                        self.emit(Opcode::Dup);
                        let j_short = self.emit_jump(Opcode::JumpIfFalse);
                        self.emit(Opcode::Pop);
                        self.compile_expr(value)?;
                        self.emit_u16(Opcode::StoreVar, idx);
                        self.emit_u16(Opcode::LoadVar, idx);
                        let j_end = self.emit_jump(Opcode::Jump);
                        let end = self.chunk.code.len();
                        self.patch_jump(j_short, end);
                        self.patch_jump(j_end, end);
                    }
                    LogicalAssignOp::Nullish => {
                        // ??= : assign only when current === null (matches interpreter)
                        let null_c = self.constant_idx(Constant::Null);
                        self.emit_u16(Opcode::LoadVar, idx);
                        self.emit(Opcode::Dup);
                        self.emit(Opcode::LoadConst);
                        self.chunk.write_u16(null_c);
                        self.emit_u8(Opcode::BinOp, binop_to_u8(BinOp::StrictEq));
                        let j_not_null = self.emit_jump(Opcode::JumpIfFalse);
                        self.emit(Opcode::Pop);
                        self.compile_expr(value)?;
                        self.emit_u16(Opcode::StoreVar, idx);
                        self.emit_u16(Opcode::LoadVar, idx);
                        let j_end = self.emit_jump(Opcode::Jump);
                        let end = self.chunk.code.len();
                        self.patch_jump(j_not_null, end);
                        self.patch_jump(j_end, end);
                    }
                }
            }
            Expr::New { callee, args, .. } => {
                let has_spread = args.iter().any(|a| matches!(a, CallArg::Spread(_)));
                if has_spread {
                    self.emit_u16(Opcode::NewArray, 0);
                    for arg in args {
                        match arg {
                            CallArg::Expr(e) => {
                                self.compile_expr(e)?;
                                self.emit_u16(Opcode::NewArray, 1);
                                self.emit(Opcode::ConcatArray);
                            }
                            CallArg::Spread(expr) => {
                                self.compile_expr(expr)?;
                                self.emit(Opcode::ConcatArray);
                            }
                        }
                    }
                    self.compile_expr(callee)?;
                    self.emit(Opcode::ConstructSpread);
                } else {
                    self.compile_expr(callee)?;
                    for arg in args {
                        if let CallArg::Expr(e) = arg {
                            self.compile_expr(e)?;
                        }
                    }
                    self.emit_u16(Opcode::Construct, args.len() as u16);
                }
            }
        }
        Ok(())
    }

    fn compile_jsx_element(
        &mut self,
        tag: &Arc<str>,
        props: &[JsxProp],
        children: &[JsxChild],
    ) -> Result<(), CompileError> {
        let h_idx = self.name_idx(&Arc::from("h"));
        self.emit_u16(Opcode::LoadGlobal, h_idx);
        let tag_str = tag.as_ref();
        let is_component = tag_str
            .chars()
            .next()
            .map(|c| c.is_uppercase())
            .unwrap_or(false);
        if is_component {
            let tag_idx = self.name_idx(tag);
            self.emit_u16(Opcode::LoadGlobal, tag_idx);
        } else {
            let tag_const = self.constant_idx(Constant::String(Arc::from(tag_str)));
            self.emit(Opcode::LoadConst);
            self.chunk.write_u16(tag_const);
        }
        self.compile_jsx_props(props)?;
        self.compile_jsx_children(children)?;
        self.emit_u16(Opcode::Call, 3);
        Ok(())
    }

    fn compile_jsx_fragment(&mut self, children: &[JsxChild]) -> Result<(), CompileError> {
        let h_idx = self.name_idx(&Arc::from("h"));
        self.emit_u16(Opcode::LoadGlobal, h_idx);
        let fragment_idx = self.name_idx(&Arc::from("Fragment"));
        self.emit_u16(Opcode::LoadGlobal, fragment_idx);
        let null_idx = self.constant_idx(Constant::Null);
        self.emit(Opcode::LoadConst);
        self.chunk.write_u16(null_idx);
        self.compile_jsx_children(children)?;
        self.emit_u16(Opcode::Call, 3);
        Ok(())
    }

    fn compile_jsx_props(&mut self, props: &[JsxProp]) -> Result<(), CompileError> {
        if props.is_empty() {
            let null_idx = self.constant_idx(Constant::Null);
            self.emit(Opcode::LoadConst);
            self.chunk.write_u16(null_idx);
            return Ok(());
        }
        let has_spread = props.iter().any(|p| matches!(p, JsxProp::Spread(_)));
        if has_spread {
            self.emit_u16(Opcode::NewObject, 0);
            for prop in props {
                match prop {
                    JsxProp::Attr { name, value } => {
                        let key_idx = self.constant_idx(Constant::String(Arc::clone(name)));
                        self.emit(Opcode::LoadConst);
                        self.chunk.write_u16(key_idx);
                        match value {
                            JsxAttrValue::String(s) => {
                                let val_idx = self.constant_idx(Constant::String(Arc::clone(s)));
                                self.emit(Opcode::LoadConst);
                                self.chunk.write_u16(val_idx);
                            }
                            JsxAttrValue::Expr(e) => self.compile_expr(e)?,
                            JsxAttrValue::ImplicitTrue => {
                                let true_idx = self.constant_idx(Constant::Bool(true));
                                self.emit(Opcode::LoadConst);
                                self.chunk.write_u16(true_idx);
                            }
                        }
                        self.emit_u16(Opcode::NewObject, 1);
                        self.emit(Opcode::MergeObject);
                    }
                    JsxProp::Spread(expr) => {
                        self.compile_expr(expr)?;
                        self.emit(Opcode::MergeObject);
                    }
                }
            }
        } else {
            for prop in props {
                if let JsxProp::Attr { name, value } = prop {
                    let key_idx = self.constant_idx(Constant::String(Arc::clone(name)));
                    self.emit(Opcode::LoadConst);
                    self.chunk.write_u16(key_idx);
                    match value {
                        JsxAttrValue::String(s) => {
                            let val_idx = self.constant_idx(Constant::String(Arc::clone(s)));
                            self.emit(Opcode::LoadConst);
                            self.chunk.write_u16(val_idx);
                        }
                        JsxAttrValue::Expr(e) => self.compile_expr(e)?,
                        JsxAttrValue::ImplicitTrue => {
                            let true_idx = self.constant_idx(Constant::Bool(true));
                            self.emit(Opcode::LoadConst);
                            self.chunk.write_u16(true_idx);
                        }
                    }
                }
            }
            self.emit_u16(Opcode::NewObject, props.len() as u16);
        }
        Ok(())
    }

    fn compile_jsx_children(&mut self, children: &[JsxChild]) -> Result<(), CompileError> {
        for child in children {
            match child {
                JsxChild::Text(s) => {
                    let idx = self.constant_idx(Constant::String(Arc::clone(s)));
                    self.emit(Opcode::LoadConst);
                    self.chunk.write_u16(idx);
                }
                JsxChild::Expr(e) => self.compile_expr(e)?,
            }
        }
        self.emit_u16(Opcode::NewArray, children.len() as u16);
        Ok(())
    }
}

/// Compile a Tish program to bytecode (with peephole optimizations).
pub fn compile(program: &Program) -> Result<Chunk, CompileError> {
    compile_internal(program, true, false)
}

/// Compile without peephole optimizations (for --no-optimize).
pub fn compile_unoptimized(program: &Program) -> Result<Chunk, CompileError> {
    compile_internal(program, false, false)
}

/// Compile for REPL: last expression statement leaves its value on the stack (no Pop, no trailing Null).
pub fn compile_for_repl(program: &Program) -> Result<Chunk, CompileError> {
    compile_internal(program, true, true)
}

/// Compile for REPL without peephole optimizations.
pub fn compile_for_repl_unoptimized(program: &Program) -> Result<Chunk, CompileError> {
    compile_internal(program, false, true)
}

fn compile_internal(
    program: &Program,
    peephole: bool,
    retain_last_expr: bool,
) -> Result<Chunk, CompileError> {
    let mut chunk = Chunk::new();
    let mut compiler = Compiler::new(&mut chunk, retain_last_expr);
    compiler.compile_program(program)?;
    if peephole {
        crate::peephole::optimize(&mut chunk);
    }
    Ok(chunk)
}
