///|
/// User-prompt bridge between the BiDi JS realm and MoonBit-side BiDi event
/// emission. Two responsibilities:
///
/// 1. Push the active `unhandledPromptBehavior` snapshot into the JS realm
///    so `globalThis.alert/confirm/prompt` can synchronously resolve a
///    return value matching the driver-configured policy.
/// 2. Drain the JS-side `__bidiPendingUserPrompts` queue at every
///    script.evaluate boundary and emit one `browsingContext.userPromptOpened`
///    BiDi event per queued entry, so consumers like Playwright's
///    `page.on('dialog')` observe the dialog.
///
/// Resolution semantics: Crater's headless realm cannot synchronously park
/// the page while it waits for an out-of-realm decision. The JS-side
/// alert/confirm/prompt therefore RESOLVE IMMEDIATELY based on the snapshot,
/// then queue a deferred event. Drivers that need control over the
/// resolution set unhandledPromptBehavior on session.new before any
/// page-side prompt fires.

///|
/// Build the JSON snapshot pushed to `globalThis.__bidiPromptHandlers` for
/// the given browsing context. The result is the resolved per-type handler
/// (alert / confirm / prompt) plus a `default` fallback, all selected via
/// `resolve_unhandled_prompt_handler` so the JS side never has to walk the
/// MoonBit-side user-context lookup itself.
fn BidiProtocol::serialize_prompt_handlers_snapshot_for_runtime(
  self : BidiProtocol,
  ctx_id : String,
) -> String {
  let entries : Map[String, Json] = Map([], capacity=4)
  entries["alert"] = Json::string(
    self.resolve_unhandled_prompt_handler(ctx_id, "alert"),
  )
  entries["confirm"] = Json::string(
    self.resolve_unhandled_prompt_handler(ctx_id, "confirm"),
  )
  entries["prompt"] = Json::string(
    self.resolve_unhandled_prompt_handler(ctx_id, "prompt"),
  )
  entries["default"] = Json::string(
    self.resolve_unhandled_prompt_handler(ctx_id, "default"),
  )
  make_object(entries).stringify()
}

///|
/// Push the per-context prompt-handler snapshot to the JS realm.
fn BidiProtocol::push_prompt_handlers_snapshot(
  self : BidiProtocol,
  ctx_id : String,
) -> Unit {
  let snapshot = self.serialize_prompt_handlers_snapshot_for_runtime(ctx_id)
  set_runtime_prompt_handlers(snapshot)
}

///|
/// Drain the JS-side pending-user-prompt buffer and emit one
/// `browsingContext.userPromptOpened` event per queued entry. Called at
/// every script.evaluate boundary so consumers see dialog observations
/// promptly (BiDi events are emitted on the next message dispatch).
///
/// The page-side return value was already resolved synchronously when
/// alert/confirm/prompt fired; this drain is purely observational.
fn BidiProtocol::flush_pending_user_prompts(self : BidiProtocol) -> Unit {
  let drained = drain_pending_user_prompts()
  if drained.length() == 0 || drained == "[]" {
    return
  }
  let parsed = @json.parse(drained) catch { _ => return }
  let entries = match parsed {
    Json::Array(items) => items
    _ => return
  }
  for entry in entries {
    let map = match entry {
      Json::Object(map) => map
      _ => continue
    }
    let ctx_id = match map.get("ctx") {
      Some(Json::String(value)) => value
      _ => continue
    }
    if !self.manager.has_session(ctx_id) {
      continue
    }
    let prompt_type = match map.get("type") {
      Some(Json::String(value)) => value
      _ => continue
    }
    let message = match map.get("message") {
      Some(Json::String(value)) => value
      _ => ""
    }
    let default_value : String? = match map.get("defaultValue") {
      Some(Json::String(value)) => Some(value)
      Some(Json::Null) => None
      _ => None
    }
    let handler = match map.get("handler") {
      Some(Json::String(value)) => value
      _ => self.resolve_unhandled_prompt_handler(ctx_id, prompt_type)
    }
    self.emit_user_prompt_opened(
      ctx_id, prompt_type, handler, message, default_value,
    )
  }
}