///| Interpreter-specific error handling helpers

///|

///| Re-exports JsError from shared errors package and provides

///| Value conversion helpers for JavaScript try-catch handling.

///|
/// Create a JavaScript Error object Value with proper prototype chain
fn make_error_value_with_env(
  name : String,
  msg : String,
  env : Environment?,
) -> Value {
  let err_desc : PropDescriptor = {
    writable: true,
    enumerable: false,
    configurable: true,
    getter: None,
    setter: None,
    is_accessor: false,
  }
  let err_props : Map[String, Value] = Map([])
  err_props["message"] = String_(msg)
  err_props["stack"] = String_(name + ": " + msg)
  let err_descs : Map[String, PropDescriptor] = {
    "message": err_desc,
    "stack": err_desc,
  }
  // Try to look up the proper prototype from the environment
  let proto : Value = match env {
    Some(e) => {
      let ctor : Value = e.get(name) catch { _ => Null }
      match ctor {
        Object(ctor_data) =>
          match ctor_data.bag.properties.get("prototype") {
            Some(p) => p
            None => Null
          }
        _ => Null
      }
    }
    None => Null
  }
  // Fallback: attach `name` as an own data property whenever the resolved
  // prototype cannot be trusted to carry it. Covers three cases:
  //   1. Env is None or the binding is missing (e.g. InternalError has no
  //      registered constructor).
  //   2. The binding was rebound by user code (`TypeError = function(){}`),
  //      so `proto` is some user-owned object whose `name` is empty/unrelated.
  //   3. The resolved prototype is missing its own `name` entry for any other
  //      reason.
  // Without this, engine-thrown errors would report `err.name === undefined`
  // after user rebinding — a user-visible regression of internal error shape.
  if !proto_has_name(proto, name) {
    err_props["name"] = String_(name)
    err_descs["name"] = err_desc
  }
  Object({
    bag: {
      properties: err_props,
      symbol_properties: Map([]),
      descriptors: err_descs,
      symbol_descriptors: Map([]),
      internal_slots: Map([]),
      host_slots: Map([]),
    },
    prototype: proto,
    callable: None,
    class_name: name,
    extensible: true,
    arraybuffer_state: None,
  })
}

///|
/// True when `proto` is an object whose own `name` property matches `expected`.
/// Used to decide whether engine-thrown errors can rely on the prototype chain
/// for `err.name` (genuine native error prototype) or must carry `name` as an
/// own property (rebound constructor, missing binding).
fn proto_has_name(proto : Value, expected : String) -> Bool {
  match proto {
    Object(proto_data) =>
      match proto_data.bag.properties.get("name") {
        Some(String_(s)) => s == expected
        _ => false
      }
    _ => false
  }
}

///|
/// Create an AggregateError with the errors array property
fn make_aggregate_error_value(
  msg : String,
  errors : Array[Error],
  env : Environment?,
) -> Value {
  let err_desc : PropDescriptor = {
    writable: true,
    enumerable: false,
    configurable: true,
    getter: None,
    setter: None,
    is_accessor: false,
  }
  let err_props : Map[String, Value] = Map([])
  err_props["message"] = String_(msg)
  err_props["stack"] = String_("AggregateError: " + msg)
  // Convert errors array to JavaScript array
  let js_errors : Array[Value] = errors.map(fn(e) {
    js_error_to_value_with_env(e, env)
  })
  err_props["errors"] = make_array(js_errors)
  let err_descs : Map[String, PropDescriptor] = {
    "message": err_desc,
    "stack": err_desc,
    "errors": err_desc,
  }
  // Try to look up the proper prototype from the environment
  let proto : Value = match env {
    Some(e) => {
      let ctor : Value = e.get("AggregateError") catch { _ => Null }
      match ctor {
        Object(ctor_data) =>
          match ctor_data.bag.properties.get("prototype") {
            Some(p) => p
            None => Null
          }
        _ => Null
      }
    }
    None => Null
  }
  // Same fallback as make_error_value_with_env: if the resolved prototype
  // cannot be trusted (missing binding, rebound constructor, missing `name`),
  // attach `name` as an own property so `err.name` is still defined.
  if !proto_has_name(proto, "AggregateError") {
    err_props["name"] = String_("AggregateError")
    err_descs["name"] = err_desc
  }
  Object({
    bag: {
      properties: err_props,
      symbol_properties: Map([]),
      descriptors: err_descs,
      symbol_descriptors: Map([]),
      internal_slots: Map([]),
      host_slots: Map([]),
    },
    prototype: proto,
    callable: None,
    class_name: "AggregateError",
    extensible: true,
    arraybuffer_state: None,
  })
}

///|
/// Convert any catchable error to a JavaScript Error object Value
pub fn js_error_to_value(err : Error) -> Value {
  js_error_to_value_with_env(err, None)
}

///|
/// Convert any catchable error to a JavaScript Error object Value with proper prototype
pub fn js_error_to_value_with_env(err : Error, env : Environment?) -> Value {
  match err {
    @errors.AggregateError(message~, errors~) =>
      make_aggregate_error_value(message, errors, env)
    JsException(val) => val // JavaScript throw statement value
    ExecutionControlError(StackDepthLimit) =>
      mark_engine_stack_depth_error(
        make_error_value_with_env("RangeError", STACK_DEPTH_LIMIT_MESSAGE, env),
      )
    e =>
      match @errors.name_message_if_js_error(e) {
        Some((name, msg)) => make_error_value_with_env(name, msg, env)
        None => make_error_value_with_env("Error", e.to_string(), env)
      }
  }
}

///|
/// Convert a catchable error to the JS throw value used by this interpreter.
/// Runtime engine errors are surfaced as strings like "TypeError: ...".
pub fn js_error_to_throw_value(err : Error) -> Value {
  match err {
    @errors.AggregateError(message~, errors~) => {
      let error_texts = errors.map(fn(e) {
        js_error_to_throw_value(e).to_string()
      })
      let errors_str = error_texts.join(", ")
      String_("AggregateError: " + message + " (errors: " + errors_str + ")")
    }
    JsException(val) => val
    e =>
      match @errors.format_if_js_error(e) {
        Some(s) => String_(s)
        None => String_("Error: " + e.to_string())
      }
  }
}

///|
/// Raise a JavaScript exception with the given value.
/// This is the only way for external packages to raise JsException since
/// suberror constructors from other packages are read-only in MoonBit.
pub fn raise_js_exception(value : Value) -> Unit raise Error {
  raise JsException(value)
}

///|
/// Check if an error is a JavaScript catchable error
pub fn is_js_catchable_error(err : Error) -> Bool {
  match err {
    JsException(_) => true
    ExecutionControlError(StackDepthLimit) => true
    e => @errors.name_message_if_js_error(e) != None
  }
}