///|
/// An explicitly catchable guest exception with owned payload roots.
/// `throw_into_guest` transfers this one-shot handle to the invocation. Close discarded
/// handles explicitly on wasm (native RC also retires their root owner).
pub struct GuestException {
  priv mut state : HostExceptionState?
}

///|
priv struct HostExceptionState {
  binding : ResourceBinding
  tag : Int32
  values : Array[@types.Value]
  roots : GcRootLease
  owned : Ref[Bool]
  cleanup : @jit_ffi.CleanupOwner
}

///|
/// A transport signal, distinct from ordinary RuntimeError and engine traps.
pub(all) suberror HostGuestException {
  HostThrows(GuestException)
} derive(Debug)

///|
pub impl Debug for GuestException with fn to_repr(_self) {
  Repr::literal("")
}

///|
pub extend GuestException with Debug::{to_repr}

///|
pub extend HostGuestException with Debug::{to_repr}

///|
pub fn HostGuestException::from_error(error : Error) -> GuestException? {
  match error {
    HostThrows(exception) => Some(exception)
    _ => None
  }
}

///|
pub fn GuestException::close(self : GuestException) -> Unit {
  if self.state is Some(state) {
    state.cleanup.close()
  }
  self.state = None
}

///|
/// Move the payload owner into the signal before host defers run. Closing an
/// alias of the original handle while unwinding therefore cannot unroot it.
pub fn[T] GuestException::throw_into_guest(
  self : GuestException,
) -> T raise HostGuestException {
  let state = self.state
  self.state = None
  raise HostThrows({ state, })
}

///|
/// Create a new exception for a Store-local tag address. Rooted inputs check
/// reference provenance; relative concrete types use the tag's owning module.
pub fn Store::guest_exception(
  self : Store,
  tag : Int32,
  payload : ArrayView[RootedValue],
) -> GuestException raise RuntimeError {
  let signature = self.get_tag_type(tag)
  guard self.tag_lifetimes.get(tag) is Some(active) && active.val else {
    raise InvalidHandle
  }
  guard payload.length() == signature.params.length() else {
    raise TypeMismatch
  }
  let owner = self.get_tag_owner(tag)
  let values = payload.mapi((index, root) => {
    self.exception_payload_value(root, signature.params[index], owner)
  })
  let roots = self.pin_gc_roots(values)
  let owned = Ref(true)
  let cleanup = @jit_ffi.CleanupOwner::new(fn() {
    if owned.val {
      owned.val = false
      roots.release()
    }
  })
  // Match rooted host handles until handoff. Once an executor owns the lease,
  // its live guest roots and normal invocation cleanup govern that lifetime.
  self.on_instantiation_rollback(fn() { if owned.val { cleanup.close() } })
  {
    state: Some({
      binding: { store: self.closed, active, },
      tag,
      values,
      roots,
      owned,
      cleanup,
    }),
  }
}

///|
fn Store::exception_payload_value(
  self : Store,
  root : RootedValue,
  expected : @types.ValueType,
  owner : ModuleInstance?,
) -> @types.Value raise RuntimeError {
  let value = match owner {
    Some(instance) => self.borrow_typed_rooted_value(root, instance, expected)
    None => self.borrow_rooted_value(root)
  }
  guard value_fits_kind(value, expected) else { raise TypeMismatch }
  match expected {
    RefStruct(index) | RefNullStruct(index) if index >= 0 => {
      guard owner is Some(instance) &&
        instance.types.get(index) is Some({ composite: Struct(_), .. }) else {
        raise TypeMismatch
      }
      if value is StructRef(reference) &&
        !self.gc_type_is_subtype(reference, instance, index) {
        raise TypeMismatch
      }
    }
    RefArray(index) | RefNullArray(index) if index >= 0 => {
      guard owner is Some(instance) &&
        instance.types.get(index) is Some({ composite: Array(_), .. }) else {
        raise TypeMismatch
      }
      if value is ArrayRef(reference) &&
        !self.gc_type_is_subtype(reference, instance, index) {
        raise TypeMismatch
      }
    }
    RefFuncTyped(index) | RefNullFuncTyped(index) => {
      guard owner is Some(instance) &&
        instance.types.get(index) is Some({ composite: Func(_), .. }) else {
        raise TypeMismatch
      }
      if value is FuncRef(address) &&
        !self.func_type_is_subtype(address, instance, index) {
        raise TypeMismatch
      }
    }
    RefContTyped(index) | RefNullContTyped(index) => {
      guard owner is Some(instance) &&
        instance.types.get(index) is Some({ composite: Cont(_), .. }) else {
        raise TypeMismatch
      }
    }
    _ => ()
  }
  value
}

///|
/// Execution bridge: consume a validated signal and transfer its root lease.
/// The caller must release the lease after guest roots incorporate the payload,
/// or on failure/cancellation. Invalid deliveries close their payload owner.
pub fn Store::take_guest_exception(
  self : Store,
  exception : GuestException,
) -> (Int32, Array[@types.Value], GcRootLease) raise RuntimeError {
  errdefer exception.close()
  self.ensure_open()
  guard exception.state is Some(state) else { raise InvalidHandle }
  guard physical_equal(state.binding.store, self.closed) else {
    raise ForeignStore
  }
  check_resource_open(Some(state.binding))
  guard state.roots.active.val else { raise InvalidHandle }
  self.get_tag_type(state.tag) |> ignore
  exception.state = None
  state.owned.val = false
  state.cleanup.close()
  (state.tag, state.values, state.roots)
}

///|
/// Register a host callback that can explicitly throw HostGuestException.
/// Ordinary RuntimeError and unrelated errors remain invocation failures.
pub fn Store::alloc_throwing_host_func(
  self : Store,
  func : (Array[@types.Value]) -> Array[@types.Value] raise Error,
  func_type~ : @types.FuncType = { params: [], results: [], },
  type_idx~ : Int32 = -1,
) -> Int32 raise RuntimeError {
  self.alloc_throwing_host_func_with_caller(
    (_, args) => func(args),
    func_type~,
    type_idx~,
  )
}

///|
pub fn Store::alloc_throwing_host_func_with_caller(
  self : Store,
  func : ThrowingHostFunction,
  func_type~ : @types.FuncType = { params: [], results: [], },
  type_idx~ : Int32 = -1,
) -> Int32 raise RuntimeError {
  self.alloc_func_inst(ThrowingHostFunc(func), func_type, type_idx, HostFunc)
}

///|
pub fn normalize_host_error(error : Error) -> RuntimeError {
  RuntimeError::from_error(error).unwrap_or_else(() => {
    HostCallbackError(error.to_string())
  })
}

///|
/// Synchronous execution bridge. A thrown payload owns a root lease until the
/// interpreter/native caller incorporates it into guest roots or abandons it.
pub(all) enum HostCallOutcome {
  HostCallReturned(Array[@types.Value])
  HostCallThrown(Int32, Array[@types.Value], GcRootLease)
  HostCallFailed(RuntimeError)
} derive(Debug)

///|
pub extend HostCallOutcome with Debug::{to_repr}

///|
pub fn FuncInst::invoke_sync_host(
  self : FuncInst,
  caller : Caller,
  args : Array[@types.Value],
) -> HostCallOutcome {
  match self {
    HostFunc(func) =>
      HostCallReturned(func(caller, args)) catch {
        error => HostCallFailed(error)
      }
    ThrowingHostFunc(func) =>
      HostCallReturned(func(caller, args)) catch {
        error =>
          if HostGuestException::from_error(error) is Some(exception) {
            let (tag, values, roots) = caller
              .store()
              .take_guest_exception(exception) catch {
                error => return HostCallFailed(error)
              }
            HostCallThrown(tag, values, roots)
          } else {
            HostCallFailed(normalize_host_error(error))
          }
      }
    WasmFunc(_) | AsyncHostFunc(_) => HostCallFailed(TypeMismatch)
  }
}