///|
/// Opaque policy and interruption controls for the staged bounded-evaluation
/// facade. Runtime owns their invariants; the root package is the consumer API.
pub using @runtime {
  type ExecutionPolicy,
  type ExecutionPolicyError,
  type InterruptionHandle,
}

///|
pub fn run(
  source : String,
  annex_b? : Bool = false,
) -> (Array[String], String) raise Error {
  let interp = @interpreter.new_interpreter(annex_b~)
  let prog = @parser.parse(source)
  let result = interp.run(prog.stmts)
  // Drain microtask queue after script execution
  // This implements the event loop checkpoint per WHATWG spec
  interp.run_microtasks()
  // Process timer task queue with microtask checkpoints between each
  interp.run_timers()
  (interp.host.output, result.to_string())
}

///|
/// Run JavaScript source through the opt-in closure-conversion prototype.
///
/// The normal `run` facade remains the default interpreter path. This entry
/// point parses once, compiles the supported script subset to executable
/// closures, then runs it with the same event-loop drain behavior as `run`.
pub fn run_compiled(
  source : String,
  annex_b? : Bool = false,
) -> (Array[String], String) raise Error {
  let interp = @interpreter.new_interpreter(annex_b~)
  let prog = @parser.parse(source)
  let compiled = @compiler.compile_script_to_closure(prog.stmts)
  let result = compiled(interp)
  interp.run_microtasks()
  interp.run_timers()
  (interp.host.output, result.to_string())
}

///|
/// Run JavaScript source with event loop support
/// This version allows the host to control microtask and timer execution timing
pub fn run_with_event_loop(
  source : String,
  annex_b? : Bool = false,
) -> (@interpreter.Interpreter, @interpreter.Value) raise Error {
  let interp = @interpreter.new_interpreter(annex_b~)
  let prog = @parser.parse(source)
  let result = interp.run(prog.stmts)
  // Return interpreter to allow host to manage event loop
  (interp, result)
}

///|
/// Check if there are pending microtasks in the queue
pub fn has_pending_microtasks(interp : @interpreter.Interpreter) -> Bool {
  interp.host.microtask_queue.length() > 0
}

///|
/// Check if there are pending timers in the queue
pub fn has_pending_timers(interp : @interpreter.Interpreter) -> Bool {
  interp.host.timer_queue.length() > 0
}

///|
/// Run a single microtask checkpoint
/// Returns true if there are more microtasks to process
pub fn run_microtask_checkpoint(
  interp : @interpreter.Interpreter,
) -> Bool raise Error {
  interp.run_microtasks()
  interp.host.microtask_queue.length() > 0
}

///|
/// Run all pending timers with microtask draining between each
pub fn run_timer_checkpoint(
  interp : @interpreter.Interpreter,
) -> Unit raise Error {
  interp.run_timers()
}

///|
/// Stable errors raised by the stateful Engine facade.
pub(all) suberror EngineError {
  ParseError(String)
  JavaScriptException(String)
  MissingGlobal(String)
  NotCallable(String)
  JsonConversionError(String)
  InternalError(String)
} derive(Debug)

///|
pub impl Show for EngineError with fn output(self, logger) {
  let (name, message) = match self {
    ParseError(message) => ("ParseError", message)
    JavaScriptException(message) => ("JavaScriptException", message)
    MissingGlobal(message) => ("MissingGlobal", message)
    NotCallable(message) => ("NotCallable", message)
    JsonConversionError(message) => ("JsonConversionError", message)
    InternalError(message) => ("InternalError", message)
  }
  logger.write_string(name)
  logger.write_string(": ")
  logger.write_string(message)
}

///|
/// A persistent JavaScript realm for repeated evaluation and JSON calls.
pub struct Engine {
  interp : @interpreter.Interpreter
}

///|
pub fn Engine::Engine(annex_b? : Bool = false) -> Engine {
  { interp: @interpreter.new_interpreter(annex_b~) }
}

///|
fn classify_engine_runtime_error(err : Error) -> EngineError {
  match @errors.name_message_if_js_error(err) {
    Some(("InternalError", message)) => InternalError(message)
    Some((name, message)) => JavaScriptException(name + ": " + message)
    None => InternalError(err.to_string())
  }
}

///|
pub fn Engine::eval(self : Engine, source : String) -> Unit raise EngineError {
  let program = @parser.parse(source) catch {
    err => raise ParseError(err.to_string())
  }
  ignore(self.interp.run(program.stmts)) catch {
    @runtime.JsException(value) => raise JavaScriptException(value.to_string())
    err => raise classify_engine_runtime_error(err)
  }
}

///|
fn Engine::get_global_export(
  self : Engine,
  name : String,
) -> @runtime.Value raise EngineError {
  let has_binding = self.interp.global.has(name) catch {
    err => raise classify_engine_runtime_error(err)
  }
  if has_binding {
    return self.interp.global.get(name) catch {
      err => raise classify_engine_runtime_error(err)
    }
  }
  let own_property = self.interp.get_own_property(
    self.interp.global_this,
    @runtime.String_(name),
  ) catch {
    @runtime.JsException(value) => raise JavaScriptException(value.to_string())
    err => raise classify_engine_runtime_error(err)
  }
  guard own_property is Some(_) else { raise MissingGlobal(name) }
  self.interp.get_property(self.interp.global_this, name, @token.Loc::default()) catch {
    @runtime.JsException(value) => raise JavaScriptException(value.to_string())
    err => raise classify_engine_runtime_error(err)
  }
}

///|
pub fn Engine::call_json(
  self : Engine,
  name : String,
  args : Array[Json],
) -> Json raise EngineError {
  let callee = self.get_global_export(name)
  guard @runtime.is_callable(callee) else { raise NotCallable(name) }
  let runtime_args : Array[@runtime.Value] = []
  for arg in args {
    let converted = @runtime.json_to_realm_value(self.interp.realm_state, arg) catch {
      @runtime.JsonBridgeFailure(message) => raise JsonConversionError(message)
    }
    runtime_args.push(converted)
  }
  let result = self.interp.call_value(
    callee,
    @runtime.Undefined,
    runtime_args,
    @token.Loc::default(),
  ) catch {
    @runtime.JsException(value) => raise JavaScriptException(value.to_string())
    err => raise classify_engine_runtime_error(err)
  }
  @runtime.realm_value_to_json(self.interp.realm_state, result) catch {
    @runtime.JsonBridgeFailure(message) => raise JsonConversionError(message)
  }
}

///|
pub fn Engine::take_output(self : Engine) -> Array[String] {
  let output = self.interp.host.output.copy()
  self.interp.host.output.clear()
  output
}

///|
pub fn Engine::has_pending_microtasks(self : Engine) -> Bool {
  self.interp.host.microtask_queue.length() > 0
}

///|
pub fn Engine::has_pending_timers(self : Engine) -> Bool {
  self.interp.host.timer_queue.length() > 0
}

///|
pub fn Engine::run_microtask_checkpoint(
  self : Engine,
) -> Bool raise EngineError {
  self.interp.run_microtasks() catch {
    @runtime.JsException(value) => raise JavaScriptException(value.to_string())
    err => raise classify_engine_runtime_error(err)
  }
  self.has_pending_microtasks()
}

///|
pub fn Engine::run_timer_checkpoint(self : Engine) -> Unit raise EngineError {
  self.interp.run_timers() catch {
    @runtime.JsException(value) => raise JavaScriptException(value.to_string())
    err => raise classify_engine_runtime_error(err)
  }
}

///|
/// Run a JavaScript module source and return its exports
/// The module is executed in strict mode and its exports are collected
pub fn run_module(
  source : String,
  annex_b? : Bool = false,
) -> (Array[String], Map[String, @interpreter.Value]) raise Error {
  let interp = @interpreter.new_interpreter(annex_b~)
  let prog = @parser.parse(source)
  let exports = interp.run_module(prog.stmts)
  interp.run_microtasks()
  interp.run_timers()
  (interp.host.output, exports)
}

///|
/// Run multiple modules with dependency resolution.
/// The runtime graph runner pre-registers every module specifier before
/// instantiation/evaluation, so callers do not need to order modules by
/// dependency. Returns the exports of the last module.
pub fn run_modules(
  modules : Array[(String, String)],
  annex_b? : Bool = false,
) -> (Array[String], Map[String, @interpreter.Value]) raise Error {
  let interp = @interpreter.new_interpreter(annex_b~)
  let last_exports = interp.run_modules(modules)
  interp.run_microtasks()
  interp.run_timers()
  (interp.host.output, last_exports)
}