//! Bytecode VM: executes compiled bytecode programs.

use crate::bytecode::{BytecodeFunction, BytecodeModule, CmpOp, Instruction, Value};
use crate::core_ir::FloatVal;
use std::collections::HashMap;

pub struct TryRegion {
    pub start_ip: usize,
    pub end_ip: usize,
    pub catch_label: String,
}

/// Execution context for a bytecode function.
pub struct CallFrame {
    pub locals: Vec<Value>,
    pub ip: usize,
    pub try_regions: Vec<TryRegion>,
}

/// The bytecode runtime (stack-based VM).
pub struct BytecodeVM {
    pub stack: Vec<Value>,
    pub frames: Vec<CallFrame>,
    pub module: BytecodeModule,
    pub globals: HashMap<String, Value>,
    pub error_state: Option<Value>,
    pub native_runtime: Option<crate::native_runtime::NativeRuntime>,
}

impl BytecodeVM {
    pub fn new(module: BytecodeModule) -> Self {
        BytecodeVM {
            stack: Vec::new(),
            frames: Vec::new(),
            module,
            globals: HashMap::new(),
            error_state: None,
            native_runtime: Some(crate::native_runtime::NativeRuntime::standard()),
        }
    }

    pub fn with_native_runtime(mut self, rt: crate::native_runtime::NativeRuntime) -> Self {
        self.native_runtime = Some(rt);
        self
    }

    /// Run the bytecode program.
    pub fn run(&mut self) -> Result<Value, String> {
        let entry = self.module.entry_point.clone();
        let result = self.call_function(&entry, vec![]);
        if let Some(ref err) = self.error_state {
            return Err(format!("uncaught exception: {}", err.to_string_display()));
        }
        result
    }

    /// Call a user-defined function.
    fn call_function(&mut self, name: &str, args: Vec<Value>) -> Result<Value, String> {
        if let Some(runtime) = self.module.package_exports.get(name) {
            return crate::package_runtime::invoke_package_export(runtime, &args);
        }
        // ponytail: try to resolve crate-prefixed names (Cli :: parse → parse)
        let func = if let Some(f) = self.module.find_function(name) {
            f.clone()
        } else if name.contains(" ::") {
            // Try the last segment (after final ::)
            let short = name.rsplit(" ::").next().unwrap_or(name);
            if let Some(f) = self.module.find_function(short.trim()) {
                f.clone()
            } else if let Some(rt) = &self.native_runtime
                && let Some(result) = rt.call(name, &args)
            {
                return result;
            } else {
                return Ok(Value::Nil);
            }
        } else if let Some(rt) = &self.native_runtime
            && let Some(result) = rt.call(name, &args)
        {
            return result;
        } else {
            // External/unlinked crate function — return Nil and continue
            return Ok(Value::Nil);
        };

        let mut frame = CallFrame {
            locals: vec![Value::Nil; func.local_count],
            ip: 0,
            try_regions: Vec::new(),
        };

        // Load arguments into locals
        for (i, arg) in args.iter().enumerate() {
            if i < frame.locals.len() {
                frame.locals[i] = arg.clone();
            }
        }

        self.frames.push(frame);
        self.execute_function(&func)?;
        self.frames.pop();

        if self.error_state.is_some() {
            return Ok(Value::Nil);
        }

        Ok(if self.stack.is_empty() {
            Value::Nil
        } else {
            self.stack.pop().unwrap()
        })
    }

    /// Execute instructions for a function.
    fn execute_function(&mut self, func: &BytecodeFunction) -> Result<(), String> {
        let frame_idx = self.frames.len() - 1;
        let label_map = build_label_map(&func.instructions);

        loop {
            let frame = &self.frames[frame_idx];
            let ip = frame.ip;

            if ip >= func.instructions.len() {
                break;
            }

            if self.error_state.is_some() {
                let regions = &self.frames[frame_idx].try_regions;
                if let Some(region) = regions
                    .iter()
                    .rev()
                    .find(|r| ip >= r.start_ip && ip < r.end_ip)
                {
                    let catch_label = region.catch_label.clone();
                    let catch_ip = label_map
                        .get(catch_label.as_str())
                        .copied()
                        .unwrap_or(usize::MAX);
                    self.frames[frame_idx].ip = catch_ip;
                    self.error_state = None;
                    continue;
                }
                break;
            }

            let inst = func.instructions[ip].clone();
            self.frames[frame_idx].ip += 1;

            match inst {
                Instruction::LoadInt(n) => {
                    self.stack.push(Value::Int(n));
                }
                Instruction::LoadFloat(f) => {
                    self.stack.push(Value::Float(f));
                }
                Instruction::LoadString(s) => {
                    self.stack.push(Value::String(s));
                }
                Instruction::LoadBool(b) => {
                    self.stack.push(Value::Bool(b));
                }
                Instruction::LoadNil => {
                    self.stack.push(Value::Nil);
                }
                Instruction::CallBuiltin(builtin_name, argc) => {
                    let args = self.pop_n(argc)?;
                    let results = self.call_builtin(&builtin_name, args)?;
                    for result in results {
                        self.stack.push(result);
                    }
                }
                Instruction::CallFunction(fn_name, argc) => {
                    // Pop arguments from stack
                    let args = self.pop_n(argc)?;
                    // Call the function properly (manages frame stack)
                    let result = self.call_function(&fn_name, args)?;
                    // Push result back on stack
                    self.stack.push(result);
                }
                Instruction::Return => {
                    break;
                }
                Instruction::BinOp(op) => {
                    let rhs = self.stack.pop().ok_or("stack underflow")?;
                    let lhs = self.stack.pop().ok_or("stack underflow")?;
                    let result = self.apply_binop(&op, lhs, rhs)?;
                    self.stack.push(result);
                }
                Instruction::FAdd => {
                    let rhs = self.stack.pop().ok_or("stack underflow")?.to_float();
                    let lhs = self.stack.pop().ok_or("stack underflow")?.to_float();
                    self.stack.push(Value::Float(FloatVal(lhs + rhs)));
                }
                Instruction::FSub => {
                    let rhs = self.stack.pop().ok_or("stack underflow")?.to_float();
                    let lhs = self.stack.pop().ok_or("stack underflow")?.to_float();
                    self.stack.push(Value::Float(FloatVal(lhs - rhs)));
                }
                Instruction::FMul => {
                    let rhs = self.stack.pop().ok_or("stack underflow")?.to_float();
                    let lhs = self.stack.pop().ok_or("stack underflow")?.to_float();
                    self.stack.push(Value::Float(FloatVal(lhs * rhs)));
                }
                Instruction::FDiv => {
                    let rhs = self.stack.pop().ok_or("stack underflow")?.to_float();
                    let lhs = self.stack.pop().ok_or("stack underflow")?.to_float();
                    if rhs == 0.0 {
                        return Err("float division by zero".to_string());
                    }
                    self.stack.push(Value::Float(FloatVal(lhs / rhs)));
                }
                Instruction::FCmp(op) => {
                    let rhs = self.stack.pop().ok_or("stack underflow")?.to_float();
                    let lhs = self.stack.pop().ok_or("stack underflow")?.to_float();
                    let result = match op {
                        CmpOp::Eq => lhs == rhs,
                        CmpOp::Ne => lhs != rhs,
                        CmpOp::Lt => lhs < rhs,
                        CmpOp::Gt => lhs > rhs,
                        CmpOp::Le => lhs <= rhs,
                        CmpOp::Ge => lhs >= rhs,
                    };
                    self.stack.push(Value::Bool(result));
                }
                Instruction::UnOp(op) => {
                    let val = self.stack.pop().ok_or("stack underflow")?;
                    let result = self.apply_unop(&op, val)?;
                    self.stack.push(result);
                }
                Instruction::StructInit(name, fields) => {
                    let values = self.pop_n(fields.len())?;
                    self.stack.push(Value::Struct {
                        name,
                        fields: fields.into_iter().zip(values).collect(),
                    });
                }
                Instruction::FieldAccess(name) => {
                    let value = self.stack.pop().ok_or("stack underflow")?;
                    if let Value::Struct { fields, .. } = value {
                        let field = fields
                            .into_iter()
                            .find(|(field, _)| field == &name)
                            .map(|(_, value)| value)
                            .ok_or(format!("field not found: {}", name))?;
                        self.stack.push(field);
                    } else {
                        // ponytail: field access on non-struct (Nil from unresolved crate call)
                        // returns Nil instead of erroring
                        self.stack.push(Value::Nil);
                    }
                }
                Instruction::ArrayInit(len) => {
                    let values = self.pop_n(len)?;
                    self.stack.push(Value::Array(values));
                }
                Instruction::IndexAccess => {
                    let index = self.stack.pop().ok_or("stack underflow")?.to_int();
                    let value = self.stack.pop().ok_or("stack underflow")?;
                    if index < 0 {
                        return Err(format!("array index out of bounds: {}", index));
                    }
                    if let Value::Array(values) = value {
                        let item = values
                            .get(index as usize)
                            .cloned()
                            .ok_or(format!("array index out of bounds: {}", index))?;
                        self.stack.push(item);
                    } else {
                        return Err("index access on non-array".to_string());
                    }
                }
                Instruction::IndexSet(slot) => {
                    let value = self.stack.pop().ok_or("stack underflow")?;
                    let index = self.stack.pop().ok_or("stack underflow")?.to_int();
                    if index < 0 {
                        return Err(format!("array index out of bounds: {}", index));
                    }
                    let Some(local) = self.frames[frame_idx].locals.get_mut(slot) else {
                        return Err(format!("invalid local slot: {}", slot));
                    };
                    if let Value::Array(values) = local {
                        let Some(item) = values.get_mut(index as usize) else {
                            return Err(format!("array index out of bounds: {}", index));
                        };
                        *item = value;
                    } else {
                        return Err("index assignment on non-array".to_string());
                    }
                }
                Instruction::Jump(label) => {
                    let target = label_map
                        .get(label.as_str())
                        .ok_or(format!("label not found: {}", label))?;
                    self.frames[frame_idx].ip = *target;
                }
                Instruction::JumpIfFalse(label) => {
                    let val = self.stack.pop().ok_or("stack underflow")?;
                    if !val.to_bool() {
                        let target = label_map
                            .get(label.as_str())
                            .ok_or(format!("label not found: {}", label))?;
                        self.frames[frame_idx].ip = *target;
                    }
                }
                Instruction::JumpIfTrue(label) => {
                    let val = self.stack.pop().ok_or("stack underflow")?;
                    if val.to_bool() {
                        let target = label_map
                            .get(label.as_str())
                            .ok_or(format!("label not found: {}", label))?;
                        self.frames[frame_idx].ip = *target;
                    }
                }
                Instruction::Label(_) => {
                    // Labels are no-ops at runtime
                }
                Instruction::Pop => {
                    self.stack.pop().ok_or("stack underflow")?;
                }
                Instruction::Dup => {
                    let val = self.stack.last().ok_or("stack underflow")?.clone();
                    self.stack.push(val);
                }
                Instruction::Store(slot) => {
                    let val = self.stack.pop().ok_or("stack underflow")?;
                    if slot < self.frames[frame_idx].locals.len() {
                        self.frames[frame_idx].locals[slot] = val;
                    } else {
                        return Err(format!("invalid local slot: {}", slot));
                    }
                }
                Instruction::Load(slot) => {
                    if slot < self.frames[frame_idx].locals.len() {
                        let val = self.frames[frame_idx].locals[slot].clone();
                        self.stack.push(val);
                    } else {
                        return Err(format!("invalid local slot: {}", slot));
                    }
                }
                Instruction::TryEnter(catch_label) => {
                    let start = self.frames[frame_idx].ip;
                    let catch_ip = label_map
                        .get(catch_label.as_str())
                        .copied()
                        .unwrap_or(usize::MAX);
                    let region = TryRegion {
                        start_ip: start,
                        end_ip: catch_ip.saturating_sub(1),
                        catch_label,
                    };
                    self.frames[frame_idx].try_regions.push(region);
                }
                Instruction::TryEnd => {
                    self.frames[frame_idx].try_regions.pop();
                }
            }
        }

        Ok(())
    }

    /// Pop n values from the stack.
    fn pop_n(&mut self, n: usize) -> Result<Vec<Value>, String> {
        let mut vals = Vec::new();
        for _ in 0..n {
            vals.push(self.stack.pop().ok_or("stack underflow")?);
        }
        vals.reverse();
        Ok(vals)
    }

    /// Call a built-in function. Returns a vec of results pushed onto the stack.
    fn call_builtin(&mut self, name: &str, args: Vec<Value>) -> Result<Vec<Value>, String> {
        let r = match name {
            "print" => {
                for arg in &args {
                    print!("{}", arg.to_string_display());
                }
                println!();
                vec![Value::Nil]
            }
            "print_int" => {
                if let Some(arg) = args.first() {
                    print!("{}", arg.to_int());
                }
                vec![Value::Nil]
            }
            "print_string" => {
                if let Some(arg) = args.first() {
                    print!("{}", arg.to_string_display());
                }
                vec![Value::Nil]
            }
            "to_int" => {
                let n = args.first().map_or(0, |a| a.to_int());
                vec![Value::Int(n)]
            }
            "to_string" => {
                let s = args
                    .first()
                    .map_or(String::new(), |a| a.to_string_display());
                vec![Value::String(s)]
            }
            "len" => {
                let n = if let Some(Value::String(s)) = args.first() {
                    s.len() as i64
                } else {
                    0
                };
                vec![Value::Int(n)]
            }
            "throw_error" => {
                let err_val = args
                    .into_iter()
                    .next()
                    .unwrap_or(Value::String("unhandled exception".to_string()));
                self.error_state = Some(err_val);
                vec![Value::Nil]
            }
            "str_concat" => {
                let mut iter = args.into_iter();
                let a = iter.next().unwrap_or(Value::Nil).to_string_display();
                let b = iter.next().unwrap_or(Value::Nil).to_string_display();
                vec![Value::String(a + &b)]
            }
            "str_eq" => {
                let mut iter = args.into_iter();
                let a = iter.next().unwrap_or(Value::Nil).to_string_display();
                let b = iter.next().unwrap_or(Value::Nil).to_string_display();
                vec![Value::Bool(a == b)]
            }
            "str_contains" => {
                let mut iter = args.into_iter();
                let haystack = iter.next().unwrap_or(Value::Nil).to_string_display();
                let needle = iter.next().unwrap_or(Value::Nil).to_string_display();
                vec![Value::Bool(haystack.contains(&needle))]
            }
            "str_starts_with" => {
                let mut iter = args.into_iter();
                let text = iter.next().unwrap_or(Value::Nil).to_string_display();
                let prefix = iter.next().unwrap_or(Value::Nil).to_string_display();
                vec![Value::Bool(text.starts_with(&prefix))]
            }
            "str_index_of" => {
                let mut iter = args.into_iter();
                let text = iter.next().unwrap_or(Value::Nil).to_string_display();
                let needle = iter.next().unwrap_or(Value::Nil).to_string_display();
                let index = text.find(&needle).map_or(-1, |idx| idx as i64);
                vec![Value::Int(index)]
            }
            "str_slice" => {
                let mut iter = args.into_iter();
                let text = iter.next().unwrap_or(Value::Nil).to_string_display();
                let start = iter.next().unwrap_or(Value::Nil).to_int().max(0) as usize;
                let end = iter.next().unwrap_or(Value::Nil).to_int().max(0) as usize;
                let len = text.len();
                let start = start.min(len);
                let end = end.min(len).max(start);
                vec![Value::String(text[start..end].to_string())]
            }
            "str_trim" => {
                let text = args.first().map_or(String::new(), Value::to_string_display);
                vec![Value::String(text.trim().to_string())]
            }
            "str_split_lines" => {
                let text = args.first().map_or(String::new(), Value::to_string_display);
                vec![Value::Array(
                    text.lines()
                        .map(|line| Value::String(line.to_string()))
                        .collect(),
                )]
            }
            "str_split_spaces" => {
                let text = args.first().map_or(String::new(), Value::to_string_display);
                vec![Value::Array(
                    text.split_whitespace()
                        .map(|part| Value::String(part.to_string()))
                        .collect(),
                )]
            }
            "str_tokenize_expr" => {
                let text = args.first().map_or(String::new(), Value::to_string_display);
                let mut tokens = Vec::new();
                let mut current = String::new();
                let mut chars = text.chars().peekable();
                while let Some(ch) = chars.next() {
                    if ch.is_whitespace() || matches!(ch, '(' | ')') {
                        if !current.is_empty() {
                            tokens.push(Value::String(std::mem::take(&mut current)));
                        }
                    } else if matches!(ch, '+' | '-' | '*' | '/') {
                        if !current.is_empty() {
                            tokens.push(Value::String(std::mem::take(&mut current)));
                        }
                        tokens.push(Value::String(ch.to_string()));
                    } else if matches!(ch, '=' | '!' | '<' | '>') {
                        if !current.is_empty() {
                            tokens.push(Value::String(std::mem::take(&mut current)));
                        }
                        if chars.peek() == Some(&'=') {
                            chars.next();
                            let mut op_str = String::with_capacity(2);
                            op_str.push(ch);
                            op_str.push('=');
                            tokens.push(Value::String(op_str));
                        } else {
                            tokens.push(Value::String(ch.to_string()));
                        }
                    } else {
                        current.push(ch);
                    }
                }
                if !current.is_empty() {
                    tokens.push(Value::String(current));
                }
                vec![Value::Array(tokens)]
            }
            "str_to_int" => {
                let text = args.first().map_or(String::new(), Value::to_string_display);
                vec![Value::Int(text.trim().parse::<i64>().unwrap_or_default())]
            }
            "str_is_int" => {
                let text = args.first().map_or(String::new(), Value::to_string_display);
                vec![Value::Bool(text.trim().parse::<i64>().is_ok())]
            }
            "str_table_has" => {
                let mut iter = args.into_iter();
                let names = iter.next().unwrap_or(Value::Nil).to_string_display();
                let needle = iter.next().unwrap_or(Value::Nil).to_string_display();
                vec![Value::Bool(names.split('|').any(|name| name == needle))]
            }
            "str_table_get_int" => {
                let mut iter = args.into_iter();
                let names = iter.next().unwrap_or(Value::Nil).to_string_display();
                let values = iter.next().unwrap_or(Value::Nil).to_string_display();
                let needle = iter.next().unwrap_or(Value::Nil).to_string_display();
                let value = names
                    .split('|')
                    .zip(values.split('|'))
                    .find_map(|(name, value)| {
                        if name == needle {
                            value.trim().parse::<i64>().ok()
                        } else {
                            None
                        }
                    })
                    .unwrap_or_default();
                vec![Value::Int(value)]
            }
            "json_stringify" => {
                let text = args.first().map_or(String::new(), Value::to_string_display);
                let mut out = String::from("\"");
                for ch in text.chars() {
                    match ch {
                        '"' => out.push_str("\\\""),
                        '\\' => out.push_str("\\\\"),
                        '\n' => out.push_str("\\n"),
                        '\r' => out.push_str("\\r"),
                        '\t' => out.push_str("\\t"),
                        _ => out.push(ch),
                    }
                }
                out.push('"');
                vec![Value::String(out)]
            }
            "path_join" => {
                let mut iter = args.into_iter();
                let base = iter.next().unwrap_or(Value::Nil).to_string_display();
                let child = iter.next().unwrap_or(Value::Nil).to_string_display();
                vec![Value::String(
                    std::path::PathBuf::from(base)
                        .join(child)
                        .to_string_lossy()
                        .to_string(),
                )]
            }
            "path_dirname" => {
                let path = args.first().map_or(String::new(), Value::to_string_display);
                let dirname = std::path::Path::new(&path)
                    .parent()
                    .map(|parent| parent.to_string_lossy().to_string())
                    .unwrap_or_default();
                vec![Value::String(dirname)]
            }
            "path_basename" => {
                let path = args.first().map_or(String::new(), Value::to_string_display);
                let basename = std::path::Path::new(&path)
                    .file_name()
                    .map(|name| name.to_string_lossy().to_string())
                    .unwrap_or_default();
                vec![Value::String(basename)]
            }
            "path_extname" => {
                let path = args.first().map_or(String::new(), Value::to_string_display);
                let extname = std::path::Path::new(&path)
                    .extension()
                    .map(|name| name.to_string_lossy().to_string())
                    .unwrap_or_default();
                vec![Value::String(extname)]
            }
            "env_get" => {
                let name = args.first().map_or(String::new(), Value::to_string_display);
                vec![Value::String(std::env::var(name).unwrap_or_default())]
            }
            "env_has" => {
                let name = args.first().map_or(String::new(), Value::to_string_display);
                vec![Value::Bool(std::env::var_os(name).is_some())]
            }
            "read_file" => {
                let path = args.first().map_or(String::new(), Value::to_string_display);
                vec![Value::String(
                    std::fs::read_to_string(&path)
                        .unwrap_or_else(|err| format!("read_file error for {path}: {err}")),
                )]
            }
            "write_file" => {
                let mut iter = args.into_iter();
                let path = iter.next().unwrap_or(Value::Nil).to_string_display();
                let text = iter.next().unwrap_or(Value::Nil).to_string_display();
                let ok = match std::fs::write(&path, text) {
                    Ok(()) => true,
                    Err(err) => {
                        eprintln!("write_file error for {path}: {err}");
                        false
                    }
                };
                vec![Value::Bool(ok)]
            }
            "array_push" => {
                let mut iter = args.into_iter();
                let arr = iter.next().unwrap_or(Value::Nil);
                let val = iter.next().unwrap_or(Value::Nil);
                if let Value::Array(mut values) = arr {
                    values.push(val);
                    vec![Value::Array(values)]
                } else {
                    vec![Value::Array(vec![val])]
                }
            }
            "array_pop" => {
                let mut iter = args.into_iter();
                let arr = iter.next().unwrap_or(Value::Nil);
                if let Value::Array(mut values) = arr {
                    let popped = values.pop().unwrap_or(Value::Nil);
                    vec![Value::Array(values), popped]
                } else {
                    vec![Value::Array(Vec::new()), Value::Nil]
                }
            }
            "array_len" => {
                let n = match args.first() {
                    Some(Value::Array(values)) => values.len() as i64,
                    _ => 0,
                };
                vec![Value::Int(n)]
            }
            "bool_to_int" => {
                let n = match args.first() {
                    Some(Value::Bool(true)) => 1,
                    _ => 0,
                };
                vec![Value::Int(n)]
            }
            "int_to_bool" => {
                let b = match args.first() {
                    Some(Value::Int(n)) => *n != 0,
                    Some(v) => v.to_bool(),
                    _ => false,
                };
                vec![Value::Bool(b)]
            }
            _ => return Err(format!("unknown builtin: {}", name)),
        };
        Ok(r)
    }

    /// Apply a binary operator.
    fn apply_binop(&self, op: &str, lhs: Value, rhs: Value) -> Result<Value, String> {
        match op {
            "==" => return Ok(Value::Bool(lhs == rhs)),
            "!=" => return Ok(Value::Bool(lhs != rhs)),
            "&&" => return Ok(Value::Bool(lhs.to_bool() && rhs.to_bool())),
            "||" => return Ok(Value::Bool(lhs.to_bool() || rhs.to_bool())),
            _ => {}
        }
        if op == "+" && matches!(lhs, Value::String(_)) && matches!(rhs, Value::String(_)) {
            let left = match lhs {
                Value::String(s) => s,
                _ => unreachable!("checked above"),
            };
            let right = match rhs {
                Value::String(s) => s,
                _ => unreachable!("checked above"),
            };
            return Ok(Value::String(format!("{left}{right}")));
        }
        if matches!(lhs, Value::Float(_)) || matches!(rhs, Value::Float(_)) {
            let l = lhs.to_float();
            let r = rhs.to_float();
            return match op {
                "+" => Ok(Value::Float(FloatVal(l + r))),
                "-" => Ok(Value::Float(FloatVal(l - r))),
                "*" => Ok(Value::Float(FloatVal(l * r))),
                "/" => {
                    if r == 0.0 {
                        Err("float division by zero".to_string())
                    } else {
                        Ok(Value::Float(FloatVal(l / r)))
                    }
                }
                "<" => Ok(Value::Bool(l < r)),
                ">" => Ok(Value::Bool(l > r)),
                "<=" => Ok(Value::Bool(l <= r)),
                ">=" => Ok(Value::Bool(l >= r)),
                _ => Err(format!("unknown float binop: {}", op)),
            };
        }
        let l = lhs.to_int();
        let r = rhs.to_int();
        match op {
            "+" => Ok(Value::Int(l + r)),
            "-" => Ok(Value::Int(l - r)),
            "*" => Ok(Value::Int(l * r)),
            "/" => {
                if r == 0 {
                    Err("division by zero".to_string())
                } else {
                    Ok(Value::Int(l / r))
                }
            }
            "%" => {
                if r == 0 {
                    Err("division by zero".to_string())
                } else {
                    Ok(Value::Int(l % r))
                }
            }
            "<" => Ok(Value::Bool(l < r)),
            ">" => Ok(Value::Bool(l > r)),
            "<=" => Ok(Value::Bool(l <= r)),
            ">=" => Ok(Value::Bool(l >= r)),
            _ => Err(format!("unknown binop: {}", op)),
        }
    }

    /// Apply a unary operator.
    fn apply_unop(&self, op: &str, val: Value) -> Result<Value, String> {
        match op {
            "-" => Ok(Value::Int(-val.to_int())),
            "!" => Ok(Value::Bool(!val.to_bool())),
            _ => Err(format!("unknown unop: {}", op)),
        }
    }
}

/// Build a map from label names to instruction indices.
fn build_label_map(instructions: &[Instruction]) -> HashMap<&str, usize> {
    let mut map = HashMap::new();
    for (i, inst) in instructions.iter().enumerate() {
        if let Instruction::Label(name) = inst {
            map.insert(name.as_str(), i);
        }
    }
    map
}

#[cfg(test)]
mod tests {
    use super::*;

    #[test]
    fn vm_simple_arithmetic() {
        let mut module = BytecodeModule::new("main".to_string());
        let func = BytecodeFunction {
            name: "main".to_string(),
            instructions: vec![
                Instruction::LoadInt(5),
                Instruction::LoadInt(3),
                Instruction::BinOp("+".to_string()),
                Instruction::Return,
            ],
            local_count: 0,
        };
        module.add_function(func);

        let mut vm = BytecodeVM::new(module);
        let result = vm.run().unwrap();
        assert_eq!(result, Value::Int(8));
    }

    #[test]
    fn vm_builtin_print() {
        let mut module = BytecodeModule::new("main".to_string());
        let func = BytecodeFunction {
            name: "main".to_string(),
            instructions: vec![
                Instruction::LoadInt(42),
                Instruction::CallBuiltin("print_int".to_string(), 1),
                Instruction::Return,
            ],
            local_count: 0,
        };
        module.add_function(func);

        let mut vm = BytecodeVM::new(module);
        let _ = vm.run(); // Should print "42"
    }

    #[test]
    fn vm_std_path_builtins_execute() {
        let mut module = BytecodeModule::new("main".to_string());
        module.add_function(BytecodeFunction {
            name: "main".to_string(),
            local_count: 0,
            instructions: vec![
                Instruction::LoadString("/tmp".to_string()),
                Instruction::LoadString("demo.in".to_string()),
                Instruction::CallBuiltin("path_join".to_string(), 2),
                Instruction::CallBuiltin("path_basename".to_string(), 1),
                Instruction::Return,
            ],
        });
        let mut vm = BytecodeVM::new(module);
        let result = vm.run().expect("run path builtins");
        assert_eq!(result, Value::String("demo.in".to_string()));
    }

    #[test]
    fn vm_std_env_builtins_execute() {
        let mut module = BytecodeModule::new("main".to_string());
        module.add_function(BytecodeFunction {
            name: "main".to_string(),
            local_count: 0,
            instructions: vec![
                Instruction::LoadString("PATH".to_string()),
                Instruction::CallBuiltin("env_has".to_string(), 1),
                Instruction::Return,
            ],
        });
        let mut vm = BytecodeVM::new(module);
        let result = vm.run().expect("run env builtins");
        assert_eq!(result, Value::Bool(true));
    }

    #[test]
    fn vm_std_fs_builtins_execute() {
        let path = std::env::temp_dir().join("in-stdlib-fs-test.txt");
        let path_text = path.to_string_lossy().to_string();
        let mut module = BytecodeModule::new("main".to_string());
        module.add_function(BytecodeFunction {
            name: "main".to_string(),
            local_count: 0,
            instructions: vec![
                Instruction::LoadString(path_text.clone()),
                Instruction::LoadString("hello compiler".to_string()),
                Instruction::CallBuiltin("write_file".to_string(), 2),
                Instruction::Pop,
                Instruction::LoadString(path_text),
                Instruction::CallBuiltin("read_file".to_string(), 1),
                Instruction::Return,
            ],
        });
        let mut vm = BytecodeVM::new(module);
        let result = vm.run().expect("run fs builtins");
        assert_eq!(result, Value::String("hello compiler".to_string()));
        let _ = std::fs::remove_file(path);
    }

    #[test]
    fn vm_std_json_stringify_builtin_executes() {
        let mut module = BytecodeModule::new("main".to_string());
        module.add_function(BytecodeFunction {
            name: "main".to_string(),
            local_count: 0,
            instructions: vec![
                Instruction::LoadString("a\"b".to_string()),
                Instruction::CallBuiltin("json_stringify".to_string(), 1),
                Instruction::Return,
            ],
        });
        let mut vm = BytecodeVM::new(module);
        let result = vm.run().expect("run json stringify builtin");
        assert_eq!(result, Value::String("\"a\\\"b\"".to_string()));
    }

    #[test]
    fn vm_std_string_trim_builtin_executes() {
        let mut module = BytecodeModule::new("main".to_string());
        module.add_function(BytecodeFunction {
            name: "main".to_string(),
            local_count: 0,
            instructions: vec![
                Instruction::LoadString("  answer  ".to_string()),
                Instruction::CallBuiltin("str_trim".to_string(), 1),
                Instruction::Return,
            ],
        });
        let mut vm = BytecodeVM::new(module);
        let result = vm.run().expect("run string trim builtin");
        assert_eq!(result, Value::String("answer".to_string()));
    }

    #[test]
    fn vm_std_string_split_lines_builtin_executes() {
        let mut module = BytecodeModule::new("main".to_string());
        module.add_function(BytecodeFunction {
            name: "main".to_string(),
            local_count: 0,
            instructions: vec![
                Instruction::LoadString("first\nsecond\n".to_string()),
                Instruction::CallBuiltin("str_split_lines".to_string(), 1),
                Instruction::CallBuiltin("array_len".to_string(), 1),
                Instruction::Return,
            ],
        });
        let mut vm = BytecodeVM::new(module);
        let result = vm.run().expect("run string split lines builtin");
        assert_eq!(result, Value::Int(2));
    }

    #[test]
    fn vm_std_string_split_spaces_builtin_executes() {
        let mut module = BytecodeModule::new("main".to_string());
        module.add_function(BytecodeFunction {
            name: "main".to_string(),
            local_count: 0,
            instructions: vec![
                Instruction::LoadString("let answer = 40 + 2".to_string()),
                Instruction::CallBuiltin("str_split_spaces".to_string(), 1),
                Instruction::LoadInt(1),
                Instruction::IndexAccess,
                Instruction::Return,
            ],
        });
        let mut vm = BytecodeVM::new(module);
        let result = vm.run().expect("run string split spaces builtin");
        assert_eq!(result, Value::String("answer".to_string()));
    }

    #[test]
    fn vm_std_string_to_int_builtin_executes() {
        let mut module = BytecodeModule::new("main".to_string());
        module.add_function(BytecodeFunction {
            name: "main".to_string(),
            local_count: 0,
            instructions: vec![
                Instruction::LoadString("42".to_string()),
                Instruction::CallBuiltin("str_to_int".to_string(), 1),
                Instruction::Return,
            ],
        });
        let mut vm = BytecodeVM::new(module);
        let result = vm.run().expect("run string to int builtin");
        assert_eq!(result, Value::Int(42));
    }

    #[test]
    fn vm_std_string_starts_with_builtin_executes() {
        let mut module = BytecodeModule::new("main".to_string());
        module.add_function(BytecodeFunction {
            name: "main".to_string(),
            local_count: 0,
            instructions: vec![
                Instruction::LoadString("# comment".to_string()),
                Instruction::LoadString("#".to_string()),
                Instruction::CallBuiltin("str_starts_with".to_string(), 2),
                Instruction::Return,
            ],
        });
        let mut vm = BytecodeVM::new(module);
        let result = vm.run().expect("run string starts with builtin");
        assert_eq!(result, Value::Bool(true));
    }

    #[test]
    fn vm_std_string_index_of_builtin_executes() {
        let mut module = BytecodeModule::new("main".to_string());
        module.add_function(BytecodeFunction {
            name: "main".to_string(),
            local_count: 0,
            instructions: vec![
                Instruction::LoadString("let answer = 42".to_string()),
                Instruction::LoadString("=".to_string()),
                Instruction::CallBuiltin("str_index_of".to_string(), 2),
                Instruction::Return,
            ],
        });
        let mut vm = BytecodeVM::new(module);
        let result = vm.run().expect("run string index of builtin");
        assert_eq!(result, Value::Int(11));
    }

    #[test]
    fn vm_std_string_slice_builtin_executes() {
        let mut module = BytecodeModule::new("main".to_string());
        module.add_function(BytecodeFunction {
            name: "main".to_string(),
            local_count: 0,
            instructions: vec![
                Instruction::LoadString("let answer".to_string()),
                Instruction::LoadInt(4),
                Instruction::LoadInt(10),
                Instruction::CallBuiltin("str_slice".to_string(), 3),
                Instruction::Return,
            ],
        });
        let mut vm = BytecodeVM::new(module);
        let result = vm.run().expect("run string slice builtin");
        assert_eq!(result, Value::String("answer".to_string()));
    }

    #[test]
    fn vm_std_string_is_int_builtin_executes() {
        let mut module = BytecodeModule::new("main".to_string());
        module.add_function(BytecodeFunction {
            name: "main".to_string(),
            local_count: 0,
            instructions: vec![
                Instruction::LoadString(" 42 ".to_string()),
                Instruction::CallBuiltin("str_is_int".to_string(), 1),
                Instruction::Return,
            ],
        });
        let mut vm = BytecodeVM::new(module);
        let result = vm.run().expect("run string is int builtin");
        assert_eq!(result, Value::Bool(true));
    }

    #[test]
    fn vm_std_string_tokenize_expr_builtin_executes() {
        let mut module = BytecodeModule::new("main".to_string());
        module.add_function(BytecodeFunction {
            name: "main".to_string(),
            local_count: 0,
            instructions: vec![
                Instruction::LoadString("let answer=base+1*2".to_string()),
                Instruction::CallBuiltin("str_tokenize_expr".to_string(), 1),
                Instruction::LoadInt(3),
                Instruction::IndexAccess,
                Instruction::Return,
            ],
        });
        let mut vm = BytecodeVM::new(module);
        let result = vm.run().expect("run string tokenize expr builtin");
        assert_eq!(result, Value::String("base".to_string()));
    }

    #[test]
    fn vm_std_symbol_table_has_builtin_executes() {
        let mut module = BytecodeModule::new("main".to_string());
        module.add_function(BytecodeFunction {
            name: "main".to_string(),
            local_count: 0,
            instructions: vec![
                Instruction::LoadString("base|ignored|".to_string()),
                Instruction::LoadString("base".to_string()),
                Instruction::CallBuiltin("str_table_has".to_string(), 2),
                Instruction::Return,
            ],
        });
        let mut vm = BytecodeVM::new(module);
        let result = vm.run().expect("run symbol table has builtin");
        assert_eq!(result, Value::Bool(true));
    }

    #[test]
    fn vm_std_symbol_table_get_int_builtin_executes() {
        let mut module = BytecodeModule::new("main".to_string());
        module.add_function(BytecodeFunction {
            name: "main".to_string(),
            local_count: 0,
            instructions: vec![
                Instruction::LoadString("base|ignored|".to_string()),
                Instruction::LoadString("40|7|".to_string()),
                Instruction::LoadString("base".to_string()),
                Instruction::CallBuiltin("str_table_get_int".to_string(), 3),
                Instruction::Return,
            ],
        });
        let mut vm = BytecodeVM::new(module);
        let result = vm.run().expect("run symbol table get int builtin");
        assert_eq!(result, Value::Int(40));
    }

    #[test]
    fn vm_local_storage() {
        let mut module = BytecodeModule::new("main".to_string());
        let func = BytecodeFunction {
            name: "main".to_string(),
            instructions: vec![
                Instruction::LoadInt(10),
                Instruction::Store(0),
                Instruction::Load(0),
                Instruction::Return,
            ],
            local_count: 1,
        };
        module.add_function(func);

        let mut vm = BytecodeVM::new(module);
        let result = vm.run().unwrap();
        assert_eq!(result, Value::Int(10));
    }

    #[test]
    fn vm_struct_field_access() {
        let mut module = BytecodeModule::new("main".to_string());
        let func = BytecodeFunction {
            name: "main".to_string(),
            instructions: vec![
                Instruction::LoadInt(2),
                Instruction::LoadInt(5),
                Instruction::StructInit(
                    "Point".to_string(),
                    vec!["x".to_string(), "y".to_string()],
                ),
                Instruction::FieldAccess("y".to_string()),
                Instruction::Return,
            ],
            local_count: 0,
        };
        module.add_function(func);

        let mut vm = BytecodeVM::new(module);
        let result = vm.run().unwrap();
        assert_eq!(result, Value::Int(5));
    }

    #[test]
    fn vm_try_catch_catches_throw() {
        let mut module = BytecodeModule::new("main".to_string());
        let func = BytecodeFunction {
            name: "main".to_string(),
            instructions: vec![
                Instruction::TryEnter("catch_0".to_string()),
                Instruction::LoadString("boom".to_string()),
                Instruction::CallBuiltin("throw_error".to_string(), 1),
                Instruction::TryEnd,
                Instruction::Jump("end_0".to_string()),
                Instruction::Label("catch_0".to_string()),
                Instruction::LoadInt(42),
                Instruction::Label("end_0".to_string()),
                Instruction::Return,
            ],
            local_count: 0,
        };
        module.add_function(func);
        let mut vm = BytecodeVM::new(module);
        let result = vm.run().unwrap();
        assert_eq!(result, Value::Int(42));
    }

    #[test]
    fn vm_uncaught_throw_produces_error() {
        let mut module = BytecodeModule::new("main".to_string());
        let func = BytecodeFunction {
            name: "main".to_string(),
            instructions: vec![
                Instruction::LoadString("unhandled".to_string()),
                Instruction::CallBuiltin("throw_error".to_string(), 1),
                Instruction::LoadInt(99),
                Instruction::Return,
            ],
            local_count: 0,
        };
        module.add_function(func);
        let mut vm = BytecodeVM::new(module);
        assert!(vm.run().is_err());
    }

    #[test]
    fn vm_throw_inside_nested_try_caught_by_outer() {
        let mut module = BytecodeModule::new("main".to_string());
        let func = BytecodeFunction {
            name: "main".to_string(),
            instructions: vec![
                Instruction::TryEnter("catch_outer".to_string()),
                Instruction::LoadInt(1),
                Instruction::Store(0),
                Instruction::TryEnter("catch_inner".to_string()),
                Instruction::LoadString("nested".to_string()),
                Instruction::CallBuiltin("throw_error".to_string(), 1),
                Instruction::TryEnd,
                Instruction::Jump("end_inner".to_string()),
                Instruction::Label("catch_inner".to_string()),
                Instruction::LoadString("rethrow".to_string()),
                Instruction::CallBuiltin("throw_error".to_string(), 1),
                Instruction::Label("end_inner".to_string()),
                Instruction::TryEnd,
                Instruction::Jump("end_outer".to_string()),
                Instruction::Label("catch_outer".to_string()),
                Instruction::LoadInt(7),
                Instruction::Store(0),
                Instruction::Label("end_outer".to_string()),
                Instruction::Load(0),
                Instruction::Return,
            ],
            local_count: 1,
        };
        module.add_function(func);
        let mut vm = BytecodeVM::new(module);
        let result = vm.run().unwrap();
        assert_eq!(result, Value::Int(7));
    }

    #[test]
    fn vm_float_add() {
        let mut module = BytecodeModule::new("main".to_string());
        let func = BytecodeFunction {
            name: "main".to_string(),
            instructions: vec![
                Instruction::LoadFloat(FloatVal(1.5)),
                Instruction::LoadFloat(FloatVal(2.5)),
                Instruction::FAdd,
                Instruction::Return,
            ],
            local_count: 0,
        };
        module.add_function(func);
        let mut vm = BytecodeVM::new(module);
        let result = vm.run().unwrap();
        assert_eq!(result, Value::Float(FloatVal(4.0)));
    }
}
