//! Code generation: AST -> JavaScript source.

use std::path::{Path, PathBuf};

use sourcemap::SourceMapBuilder;
use tishlang_ast::{
    ArrayElement, ArrowBody, BinOp, CallArg, CompoundOp, DestructElement, DestructPattern, Expr,
    FunParam, Literal, LogicalAssignOp, MemberProp, ObjectProp, Program, Statement, UnaryOp,
};

use crate::error::CompileError;

struct Codegen {
    output: String,
    indent: usize,
    in_async: bool,
}

fn stmt_terminates_switch(stmt: Option<&Statement>) -> bool {
    matches!(
        stmt,
        Some(Statement::Break { .. })
            | Some(Statement::Return { .. })
            | Some(Statement::Throw { .. })
    )
}

impl Codegen {
    fn new() -> Self {
        Self {
            output: String::new(),
            indent: 0,
            in_async: false,
        }
    }

    /// ECMAScript does not allow `if (c) const x = 1` / `while (c) let y = 2` without a block.
    fn stmt_needs_braces_in_js_control_head(stmt: &Statement) -> bool {
        matches!(
            stmt,
            Statement::VarDecl { .. } | Statement::VarDeclDestructure { .. }
        )
    }

    fn emit_js_control_body(&mut self, body: &Statement) -> Result<(), CompileError> {
        if Self::stmt_needs_braces_in_js_control_head(body) {
            self.writeln("{");
            self.indent += 1;
            self.emit_statement(body)?;
            self.indent -= 1;
            self.writeln("}");
        } else {
            self.indent += 1;
            self.emit_statement(body)?;
            self.indent -= 1;
        }
        Ok(())
    }

    fn indent_str(&self) -> String {
        "  ".repeat(self.indent)
    }

    fn write(&mut self, s: &str) {
        self.output.push_str(s);
    }

    fn writeln(&mut self, s: &str) {
        self.output.push_str(&self.indent_str());
        self.output.push_str(s);
        self.output.push('\n');
    }

    fn output_line(&self) -> u32 {
        self.output
            .as_bytes()
            .iter()
            .filter(|&&b| b == b'\n')
            .count() as u32
    }

    fn escape_ident(s: &str) -> String {
        let s = s.to_string();
        if s == "await" || s == "default" {
            format!("_{}", s)
        } else {
            s
        }
    }

    fn emit_program(
        &mut self,
        program: &Program,
        map_sources: Option<(&[PathBuf], &Path)>,
        map_builder: Option<&mut SourceMapBuilder>,
    ) -> Result<(), CompileError> {
        self.write("// Generated by tishlang_compile_js\n");
        match (map_sources, map_builder) {
            (Some((srcs, root)), Some(sm)) => {
                for (i, stmt) in program.statements.iter().enumerate() {
                    if i < srcs.len() {
                        let dst_line = self.output_line();
                        let sp = stmt.span();
                        let src_line = sp.start.0.saturating_sub(1) as u32;
                        let src_col = sp.start.1.saturating_sub(1) as u32;
                        let abs = srcs[i].as_path();
                        let root_canon = root.canonicalize().unwrap_or_else(|_| root.to_path_buf());
                        let abs_canon = abs.canonicalize().unwrap_or_else(|_| abs.to_path_buf());
                        let rel = abs_canon
                            .strip_prefix(&root_canon)
                            .unwrap_or(abs_canon.as_path());
                        let rel_str = rel.to_string_lossy();
                        sm.add(
                            dst_line,
                            0,
                            src_line,
                            src_col,
                            Some(rel_str.as_ref()),
                            None,
                            false,
                        );
                    }
                    self.emit_statement(stmt)?;
                }
            }
            _ => {
                for stmt in &program.statements {
                    self.emit_statement(stmt)?;
                }
            }
        }
        Ok(())
    }

    fn emit_statement(&mut self, stmt: &Statement) -> Result<(), CompileError> {
        match stmt {
            Statement::Block { statements, .. } => {
                self.writeln("{");
                self.indent += 1;
                for s in statements {
                    self.emit_statement(s)?;
                }
                self.indent -= 1;
                self.writeln("}");
            }
            // Comma-declarators: emit each declarator as its own JS statement in the
            // current scope (no wrapping braces).
            Statement::Multi { statements, .. } => {
                for s in statements {
                    self.emit_statement(s)?;
                }
            }
            Statement::VarDecl {
                name,
                mutable,
                type_ann: _,
                init,
                ..
            } => {
                let decl = if *mutable { "let" } else { "const" };
                let escaped = Self::escape_ident(name.as_ref());
                if let Some(expr) = init {
                    let e = self.emit_expr(expr)?;
                    self.writeln(&format!("{} {} = {};", decl, escaped, e));
                } else {
                    self.writeln(&format!("{} {};", decl, escaped));
                }
            }
            Statement::VarDeclDestructure {
                pattern,
                mutable,
                init,
                ..
            } => {
                let decl = if *mutable { "let" } else { "const" };
                let rhs = self.emit_expr(init)?;
                let pat = self.emit_destruct_pattern(pattern)?;
                self.writeln(&format!("{} {} = {};", decl, pat, rhs));
            }
            Statement::ExprStmt { expr, .. } => {
                let e = self.emit_expr(expr)?;
                self.writeln(&format!("{};", e));
            }
            Statement::If {
                cond,
                then_branch,
                else_branch,
                ..
            } => {
                let c = self.emit_expr(cond)?;
                self.writeln(&format!("if ({})", c));
                self.emit_js_control_body(then_branch)?;
                if let Some(eb) = else_branch {
                    self.writeln("else");
                    self.emit_js_control_body(eb)?;
                }
            }
            Statement::While { cond, body, .. } => {
                let c = self.emit_expr(cond)?;
                self.writeln(&format!("while ({})", c));
                self.emit_js_control_body(body)?;
            }
            Statement::For {
                init,
                cond,
                update,
                body,
                ..
            } => {
                // Keep the whole `for (...)` on one line with normal statement indentation (do not
                // mix bare `write("for (")` with `writeln(")")`, which indents `)` on a new line).
                let mut header = self.indent_str();
                header.push_str("for (");
                if let Some(i) = init {
                    match i.as_ref() {
                        Statement::VarDecl {
                            name,
                            mutable,
                            init: opt_init,
                            ..
                        } => {
                            let decl = if *mutable { "let" } else { "const" };
                            let escaped = Self::escape_ident(name.as_ref());
                            if let Some(e) = opt_init {
                                let ex = self.emit_expr(e)?;
                                header.push_str(&format!("{} {} = {}", decl, escaped, ex));
                            } else {
                                header.push_str(&format!("{} {}", decl, escaped));
                            }
                        }
                        Statement::ExprStmt { expr, .. } => {
                            let ex = self.emit_expr(expr)?;
                            header.push_str(&ex);
                        }
                        // Comma-declarators (`for (let i = 0, n = len; …)`): emit JS-native
                        // `let i = 0, n = len` — one keyword, declarators joined by `, `.
                        Statement::Multi { statements, .. } => {
                            let mut decl_kw = "let";
                            let mut parts: Vec<String> = Vec::new();
                            for (idx, st) in statements.iter().enumerate() {
                                let Statement::VarDecl {
                                    name,
                                    mutable,
                                    init: opt_init,
                                    ..
                                } = st
                                else {
                                    return Err(CompileError::new("Unsupported for init"));
                                };
                                if idx == 0 {
                                    decl_kw = if *mutable { "let" } else { "const" };
                                }
                                let escaped = Self::escape_ident(name.as_ref());
                                if let Some(e) = opt_init {
                                    let ex = self.emit_expr(e)?;
                                    parts.push(format!("{} = {}", escaped, ex));
                                } else {
                                    parts.push(escaped);
                                }
                            }
                            header.push_str(&format!("{} {}", decl_kw, parts.join(", ")));
                        }
                        _ => return Err(CompileError::new("Unsupported for init")),
                    }
                }
                header.push_str("; ");
                if let Some(c) = cond {
                    let ce = self.emit_expr(c)?;
                    header.push_str(&ce);
                }
                header.push_str("; ");
                if let Some(u) = update {
                    let ue = self.emit_expr(u)?;
                    header.push_str(&ue);
                }
                header.push(')');
                header.push('\n');
                self.output.push_str(&header);
                self.emit_js_control_body(body)?;
            }
            Statement::ForOf {
                name,
                iterable,
                body,
                ..
            } => {
                let escaped = Self::escape_ident(name.as_ref());
                let it = self.emit_expr(iterable)?;
                self.writeln(&format!("for (const {} of {})", escaped, it));
                self.emit_js_control_body(body)?;
            }
            Statement::Return { value, .. } => {
                if let Some(v) = value {
                    let e = self.emit_expr(v)?;
                    self.writeln(&format!("return {};", e));
                } else {
                    self.writeln("return;");
                }
            }
            Statement::Break { .. } => self.writeln("break;"),
            Statement::Continue { .. } => self.writeln("continue;"),
            Statement::FunDecl {
                async_,
                name,
                params,
                rest_param,
                return_type: _,
                body,
                ..
            } => {
                let async_prefix = if *async_ { "async " } else { "" };
                let escaped = Self::escape_ident(name.as_ref());
                let params_str = self.emit_params(params, rest_param.as_ref())?;
                self.writeln(&format!(
                    "{}function {} ({}) {{",
                    async_prefix, escaped, params_str
                ));
                self.indent += 1;
                if *async_ {
                    self.in_async = true;
                }
                self.emit_statement(body)?;
                if *async_ {
                    self.in_async = false;
                }
                self.indent -= 1;
                self.writeln("}");
            }
            Statement::Switch {
                expr,
                cases,
                default_body,
                ..
            } => {
                let e = self.emit_expr(expr)?;
                self.writeln(&format!("switch ({}) {{", e));
                self.indent += 1;
                for (case_expr, stmts) in cases {
                    if let Some(ce) = case_expr {
                        let c = self.emit_expr(ce)?;
                        self.writeln(&format!("case {}:", c));
                    }
                    for s in stmts {
                        self.emit_statement(s)?;
                    }
                    // Tish has no fall-through; add break unless case ends with break/return/throw
                    if !stmt_terminates_switch(stmts.last()) {
                        self.writeln("break;");
                    }
                }
                if let Some(stmts) = default_body {
                    self.writeln("default:");
                    for s in stmts {
                        self.emit_statement(s)?;
                    }
                    if !stmt_terminates_switch(stmts.last()) {
                        self.writeln("break;");
                    }
                }
                self.indent -= 1;
                self.writeln("}");
            }
            Statement::DoWhile { body, cond, .. } => {
                self.writeln("do {");
                self.indent += 1;
                self.emit_statement(body)?;
                self.indent -= 1;
                let c = self.emit_expr(cond)?;
                self.writeln(&format!("}} while ({});", c));
            }
            Statement::Throw { value, .. } => {
                let v = self.emit_expr(value)?;
                self.writeln(&format!("throw {};", v));
            }
            Statement::Try {
                body,
                catch_param,
                catch_body,
                finally_body,
                ..
            } => {
                self.writeln("try {");
                self.indent += 1;
                self.emit_statement(body)?;
                self.indent -= 1;
                if let (Some(param), Some(cb)) = (catch_param, catch_body) {
                    let p = Self::escape_ident(param.as_ref());
                    self.writeln(&format!("}} catch ({}) {{", p));
                    self.indent += 1;
                    self.emit_statement(cb)?;
                    self.indent -= 1;
                }
                if let Some(fb) = finally_body {
                    self.writeln("} finally {");
                    self.indent += 1;
                    self.emit_statement(fb)?;
                    self.indent -= 1;
                }
                self.writeln("}");
            }
            Statement::Import { .. } | Statement::Export { .. } => {
                // Resolved away by merge_modules
            }
            Statement::TypeAlias { .. }
            | Statement::DeclareVar { .. }
            | Statement::DeclareFun { .. } => {}
        }
        Ok(())
    }

    fn emit_params(
        &mut self,
        params: &[FunParam],
        rest_param: Option<&tishlang_ast::TypedParam>,
    ) -> Result<String, CompileError> {
        let mut parts: Vec<String> = Vec::new();
        for p in params {
            match p {
                FunParam::Simple(tp) => {
                    let n = Self::escape_ident(tp.name.as_ref());
                    let s = if let Some(ref d) = tp.default {
                        format!("{} = {}", n, self.emit_expr(d)?)
                    } else {
                        n
                    };
                    parts.push(s);
                }
                FunParam::Destructure {
                    pattern,
                    type_ann: _,
                    default,
                } => {
                    let mut s = self.emit_destruct_pattern(pattern)?;
                    if let Some(ref d) = default {
                        s = format!("{} = {}", s, self.emit_expr(d)?);
                    }
                    parts.push(s);
                }
            }
        }
        if let Some(rest) = rest_param {
            parts.push(format!("...{}", Self::escape_ident(rest.name.as_ref())));
        }
        Ok(parts.join(", "))
    }

    fn emit_destruct_pattern(&mut self, pattern: &DestructPattern) -> Result<String, CompileError> {
        match pattern {
            DestructPattern::Array(elements) => {
                let parts: Vec<String> = elements
                    .iter()
                    .map(|el| match el {
                        Some(DestructElement::Ident(n, _)) => Ok(Self::escape_ident(n.as_ref())),
                        Some(DestructElement::Pattern(p)) => self.emit_destruct_pattern(p),
                        Some(DestructElement::Rest(n, _)) => {
                            Ok(format!("...{}", Self::escape_ident(n.as_ref())))
                        }
                        None => Ok("".to_string()),
                    })
                    .collect::<Result<_, _>>()?;
                Ok(format!("[{}]", parts.join(", ")))
            }
            DestructPattern::Object(props) => {
                let parts: Vec<String> = props
                    .iter()
                    .map(|p| {
                        let k = p.key.as_ref();
                        match &p.value {
                            DestructElement::Ident(n, _) => {
                                if k == n.as_ref() {
                                    Ok(k.to_string())
                                } else {
                                    Ok(format!("{}: {}", k, Self::escape_ident(n.as_ref())))
                                }
                            }
                            DestructElement::Pattern(pat) => {
                                Ok(format!("{}: {}", k, self.emit_destruct_pattern(pat)?))
                            }
                            DestructElement::Rest(n, _) => {
                                Ok(format!("...{}", Self::escape_ident(n.as_ref())))
                            }
                        }
                    })
                    .collect::<Result<_, _>>()?;
                Ok(format!("{{ {} }}", parts.join(", ")))
            }
        }
    }

    fn emit_expr(&mut self, expr: &Expr) -> Result<String, CompileError> {
        Ok(match expr {
            Expr::Literal { value, .. } => match value {
                Literal::Number(n) => format!("{}", n),
                Literal::String(s) => format!("{:?}", s.as_ref()),
                Literal::Bool(b) => format!("{}", b),
                Literal::Null => "null".to_string(),
            },
            Expr::Ident { name, .. } => Self::escape_ident(name.as_ref()),
            Expr::Binary {
                left, op, right, ..
            } => {
                let l = self.emit_expr(left)?;
                let r = self.emit_expr(right)?;
                let op_str = match op {
                    BinOp::Add => "+",
                    BinOp::Sub => "-",
                    BinOp::Mul => "*",
                    BinOp::Div => "/",
                    BinOp::Mod => "%",
                    BinOp::Pow => "**",
                    BinOp::Eq => "==",
                    BinOp::Ne => "!=",
                    BinOp::StrictEq => "===",
                    BinOp::StrictNe => "!==",
                    BinOp::Lt => "<",
                    BinOp::Le => "<=",
                    BinOp::Gt => ">",
                    BinOp::Ge => ">=",
                    BinOp::And => "&&",
                    BinOp::Or => "||",
                    BinOp::BitAnd => "&",
                    BinOp::BitOr => "|",
                    BinOp::BitXor => "^",
                    BinOp::Shl => "<<",
                    BinOp::Shr => ">>",
                    BinOp::UShr => ">>>",
                    BinOp::In => {
                        // key in object (property/index existence check)
                        return Ok(format!("({} in {})", l, r));
                    }
                };
                format!("({} {} {})", l, op_str, r)
            }
            Expr::Unary { op, operand, .. } => {
                let o = self.emit_expr(operand)?;
                match op {
                    UnaryOp::Not => format!("!{}", o),
                    UnaryOp::Neg => format!("(-{})", o),
                    UnaryOp::Pos => format!("(+{})", o),
                    UnaryOp::BitNot => format!("(~{})", o),
                    UnaryOp::Void => format!("((void {}), null)", o), // Tish void returns null, not undefined
                }
            }
            Expr::Call { callee, args, .. } => {
                let c = self.emit_expr(callee)?;
                let arg_strs: Result<Vec<_>, _> =
                    args.iter().map(|a| self.emit_call_arg(a)).collect();
                let arg_strs = arg_strs?.join(", ");
                // Tish uses null for undefined (e.g. empty array pop/shift)
                format!("({}({}) ?? null)", c, arg_strs)
            }
            Expr::New { callee, args, .. } => {
                let c = self.emit_expr(callee)?;
                let arg_strs: Result<Vec<_>, _> =
                    args.iter().map(|a| self.emit_call_arg(a)).collect();
                let arg_strs = arg_strs?.join(", ");
                format!("(new {}({}) ?? null)", c, arg_strs)
            }
            Expr::Member {
                object,
                prop,
                optional,
                ..
            } => {
                let obj = self.emit_expr(object)?;
                let expr = match prop {
                    MemberProp::Name { name, .. } => {
                        if name.parse::<u32>().is_ok()
                            || !name.chars().all(|c| c.is_alphanumeric() || c == '_')
                        {
                            format!("{}[{:?}]", obj, name.as_ref())
                        } else {
                            let sep = if *optional { "?." } else { "." };
                            format!("{}{}{}", obj, sep, name.as_ref())
                        }
                    }
                    MemberProp::Expr(e) => {
                        let idx = self.emit_expr(e)?;
                        format!("{}[{}]", obj, idx)
                    }
                };
                // Tish uses null where JS uses undefined for optional chaining short-circuit only
                if *optional {
                    format!("({} ?? null)", expr)
                } else {
                    expr
                }
            }
            Expr::Index {
                object,
                index,
                optional,
                ..
            } => {
                let obj = self.emit_expr(object)?;
                let idx = self.emit_expr(index)?;
                let sep = if *optional { "?." } else { "" };
                let expr = format!("{}{}[{}]", obj, sep, idx);
                // Tish uses null for array holes / missing indices (JS returns undefined)
                format!("({} ?? null)", expr)
            }
            Expr::Conditional {
                cond,
                then_branch,
                else_branch,
                ..
            } => {
                let c = self.emit_expr(cond)?;
                let t = self.emit_expr(then_branch)?;
                let e = self.emit_expr(else_branch)?;
                format!("({} ? {} : {})", c, t, e)
            }
            Expr::NullishCoalesce { left, right, .. } => {
                let l = self.emit_expr(left)?;
                let r = self.emit_expr(right)?;
                format!("({} ?? {})", l, r)
            }
            Expr::Array { elements, .. } => {
                let parts: Result<Vec<_>, _> = elements
                    .iter()
                    .map(|el| match el {
                        ArrayElement::Expr(e) => self.emit_expr(e),
                        ArrayElement::Spread(e) => Ok(format!("...{}", self.emit_expr(e)?)),
                    })
                    .collect();
                format!("[{}]", parts?.join(", "))
            }
            Expr::Object { props, .. } => {
                let parts: Result<Vec<_>, _> = props
                    .iter()
                    .map(|p| match p {
                        ObjectProp::KeyValue(k, v) => {
                            let key = k.as_ref();
                            let val = self.emit_expr(v)?;
                            Ok(if key.chars().all(|c| c.is_alphanumeric() || c == '_') {
                                format!("{}: {}", key, val)
                            } else {
                                format!("{:?}: {}", key, val)
                            })
                        }
                        ObjectProp::Spread(e) => Ok(format!("...{}", self.emit_expr(e)?)),
                    })
                    .collect();
                format!("{{ {} }}", parts?.join(", "))
            }
            Expr::Assign { name, value, .. } => {
                let n = Self::escape_ident(name.as_ref());
                let v = self.emit_expr(value)?;
                format!("({} = {})", n, v)
            }
            Expr::TypeOf { operand, .. } => {
                let o = self.emit_expr(operand)?;
                format!("(typeof {})", o)
            }
            Expr::Delete { target, .. } => {
                // Emit the raw property *reference*, not a value: `emit_expr` wraps Index /
                // optional reads in `(… ?? null)`, and `delete (x ?? null)` is a no-op. So
                // reconstruct `obj.name` / `obj[key]` directly here.
                match target.as_ref() {
                    Expr::Member { object, prop: MemberProp::Name { name, .. }, .. } => {
                        let obj = self.emit_expr(object)?;
                        if name.parse::<u32>().is_ok()
                            || !name.chars().all(|c| c.is_alphanumeric() || c == '_')
                        {
                            format!("(delete {}[{:?}])", obj, name.as_ref())
                        } else {
                            format!("(delete {}.{})", obj, name.as_ref())
                        }
                    }
                    Expr::Member { object, prop: MemberProp::Expr(key), .. } => {
                        let obj = self.emit_expr(object)?;
                        let k = self.emit_expr(key)?;
                        format!("(delete {}[{}])", obj, k)
                    }
                    Expr::Index { object, index, .. } => {
                        let obj = self.emit_expr(object)?;
                        let idx = self.emit_expr(index)?;
                        format!("(delete {}[{}])", obj, idx)
                    }
                    _ => {
                        let t = self.emit_expr(target)?;
                        format!("(delete {})", t)
                    }
                }
            }
            Expr::PostfixInc { name, .. } => {
                format!("{}++", Self::escape_ident(name.as_ref()))
            }
            Expr::PostfixDec { name, .. } => {
                format!("{}--", Self::escape_ident(name.as_ref()))
            }
            Expr::PrefixInc { name, .. } => {
                format!("++{}", Self::escape_ident(name.as_ref()))
            }
            Expr::PrefixDec { name, .. } => {
                format!("--{}", Self::escape_ident(name.as_ref()))
            }
            Expr::CompoundAssign {
                name, op, value, ..
            } => {
                let n = Self::escape_ident(name.as_ref());
                let v = self.emit_expr(value)?;
                let op_str = match op {
                    CompoundOp::Add => "+=",
                    CompoundOp::Sub => "-=",
                    CompoundOp::Mul => "*=",
                    CompoundOp::Div => "/=",
                    CompoundOp::Mod => "%=",
                };
                format!("({} {} {})", n, op_str, v)
            }
            Expr::LogicalAssign {
                name, op, value, ..
            } => {
                let n = Self::escape_ident(name.as_ref());
                let v = self.emit_expr(value)?;
                let op_str = match op {
                    LogicalAssignOp::AndAnd => "&&=",
                    LogicalAssignOp::OrOr => "||=",
                    LogicalAssignOp::Nullish => "??=",
                };
                format!("({} {} {})", n, op_str, v)
            }
            Expr::MemberAssign {
                object,
                prop,
                value,
                ..
            } => {
                let obj = self.emit_expr(object)?;
                let val = self.emit_expr(value)?;
                format!("({}.{} = {})", obj, prop.as_ref(), val)
            }
            Expr::IndexAssign {
                object,
                index,
                value,
                ..
            } => {
                let obj = self.emit_expr(object)?;
                let idx = self.emit_expr(index)?;
                let val = self.emit_expr(value)?;
                format!("({}[{}] = {})", obj, idx, val)
            }
            Expr::ArrowFunction { params, body, .. } => {
                let ps = self.emit_params(params, None)?;
                let body_str = match body {
                    ArrowBody::Expr(e) => self.emit_expr(e)?,
                    ArrowBody::Block(s) => {
                        let saved = std::mem::take(&mut self.output);
                        self.writeln("{");
                        self.indent += 1;
                        self.emit_statement(s)?;
                        self.indent -= 1;
                        self.writeln("}");
                        let block = self.output.trim().to_string();
                        self.output = saved;
                        block
                    }
                };
                if matches!(body, ArrowBody::Expr(_)) {
                    format!("({}) => ({})", ps, body_str)
                } else {
                    format!("({}) => {}", ps, body_str)
                }
            }
            Expr::TemplateLiteral { quasis, exprs, .. } => {
                let mut s = String::from('`');
                for (i, q) in quasis.iter().enumerate() {
                    let escaped = q
                        .replace('\\', "\\\\")
                        .replace('`', "\\`")
                        .replace('$', "\\$");
                    s.push_str(&escaped);
                    if i < exprs.len() {
                        s.push_str("${");
                        s.push_str(&self.emit_expr(&exprs[i])?);
                        s.push('}');
                    }
                }
                s.push('`');
                s
            }
            Expr::Await { operand, .. } => {
                let o = self.emit_expr(operand)?;
                format!("(await {})", o)
            }
            Expr::JsxElement { .. } | Expr::JsxFragment { .. } => {
                tishlang_ui::jsx::emit_jsx_js(expr, &mut |e| {
                    self.emit_expr(e).map_err(|ce| ce.message)
                })
                .map_err(|m| CompileError { message: m })?
            }
            Expr::NativeModuleLoad { spec, .. } => {
                return Err(CompileError {
                    message: format!(
                        "Native module imports ({}) are only supported when compiling to Rust. Omit --target js.",
                        spec.as_ref()
                    ),
                });
            }
        })
    }

    fn emit_call_arg(&mut self, arg: &CallArg) -> Result<String, CompileError> {
        match arg {
            CallArg::Expr(e) => self.emit_expr(e),
            CallArg::Spread(e) => Ok(format!("...{}", self.emit_expr(e)?)),
        }
    }
}

/// Compile a single program (no imports) to JavaScript. JSX lowers to `h` / `Fragment` (Lattish).
pub fn compile_with_jsx(program: &Program, optimize: bool) -> Result<String, CompileError> {
    let program = if optimize {
        tishlang_opt::optimize(program)
    } else {
        program.clone()
    };
    let mut g = Codegen::new();
    g.emit_program(&program, None, None)?;
    Ok(g.output)
}

/// JavaScript plus optional v3 source map JSON (for publishing Tish libraries consumed from JS/TS).
#[derive(Debug, Clone)]
pub struct JsBundle {
    pub js: String,
    pub source_map_json: Option<String>,
}

/// Same as [`compile_project_with_jsx`] plus a v3 source map pointing at merged statements’ original `.tish` files.
/// **Does not run AST optimization** (required so statement ↔ file alignment stays valid).
pub fn compile_project_with_jsx_and_source_map(
    entry_path: &Path,
    project_root: Option<&Path>,
    output_js_file_name: &str,
) -> Result<JsBundle, CompileError> {
    compile_project_js_inner(entry_path, project_root, false, true, output_js_file_name)
}

fn compile_project_js_inner(
    entry_path: &Path,
    project_root: Option<&Path>,
    optimize: bool,
    emit_source_map: bool,
    output_js_file_name: &str,
) -> Result<JsBundle, CompileError> {
    use tishlang_ast::Statement;
    let modules = tishlang_compile::resolve_project(entry_path, project_root)
        .map_err(|e| CompileError { message: e })?;
    tishlang_compile::detect_cycles(&modules).map_err(|e| CompileError { message: e })?;
    let merged =
        tishlang_compile::merge_modules(modules).map_err(|e| CompileError { message: e })?;
    let program = if optimize {
        tishlang_opt::optimize(&merged.program)
    } else {
        merged.program.clone()
    };
    let stmt_sources = merged.statement_sources;
    let default_export = program.statements.iter().find_map(|s| {
        if let Statement::VarDecl { name, .. } = s {
            let n = name.as_ref();
            if n.starts_with("__default_") {
                Some(n.to_string())
            } else {
                None
            }
        } else {
            None
        }
    });
    if emit_source_map && optimize {
        return Err(CompileError {
            message: "internal: source map requested with optimize".into(),
        });
    }
    let root = project_root
        .map(Path::to_path_buf)
        .or_else(|| entry_path.parent().map(Path::to_path_buf))
        .unwrap_or_else(|| PathBuf::from("."));
    let mut gen = Codegen::new();
    let mut map_builder = if emit_source_map {
        let mut b = SourceMapBuilder::new(Some(output_js_file_name));
        b.set_source_root(Some(""));
        Some(b)
    } else {
        None
    };
    if let Some(ref mut b) = map_builder {
        gen.emit_program(
            &program,
            Some((stmt_sources.as_slice(), root.as_path())),
            Some(b),
        )?;
    } else {
        gen.emit_program(&program, None, None)?;
    }
    let mut js = gen.output;
    if let Some(name) = default_export {
        js.push_str(&format!("\nexport default {};\n", name));
    }
    let map_json = if let Some(b) = map_builder {
        let sm = b.into_sourcemap();
        let mut v = Vec::new();
        sm.to_writer(&mut v).map_err(|e| CompileError {
            message: e.to_string(),
        })?;
        Some(String::from_utf8(v).map_err(|e| CompileError {
            message: e.to_string(),
        })?)
    } else {
        None
    };
    Ok(JsBundle {
        js,
        source_map_json: map_json,
    })
}

/// Compile a project from entry path, resolving and merging modules.
/// Uses shared resolve from tishlang_compile (same pipeline as native/WASM).
pub fn compile_project_with_jsx(
    entry_path: &std::path::Path,
    project_root: Option<&std::path::Path>,
    optimize: bool,
) -> Result<String, CompileError> {
    let stem = entry_path
        .file_name()
        .and_then(|s| s.to_str())
        .unwrap_or("out.js");
    let out_name = if stem.ends_with(".tish") {
        format!("{}.js", stem.trim_end_matches(".tish"))
    } else {
        format!("{stem}.js")
    };
    Ok(compile_project_js_inner(entry_path, project_root, optimize, false, &out_name)?.js)
}
