///|
/// Read one JSONL line from the engine's stdout stream back into an `Event`.
///
/// The inverse of `emit`: `parse(json) == Some(event)` for every line `emit`
/// writes, which `emit/emit_test.mbt` pins per variant — it needs both halves,
/// so it lives with the writer. Fields are read from the top level: `emit`
/// writes each event's fields flat, so there is no nesting and no envelope at
/// all (events do not go through `@xlog`).
///
/// `None` means "not an event this engine emits" — an unknown `event` name, a
/// missing `event` key, or a payload whose required fields are absent or
/// mistyped. Clients stay tolerant of a newer engine by treating `None` as
/// "ignore this line" rather than as a failure.
///
/// ## When a field may be absent
///
/// A field is defaulted only when one of two things is true of it:
///
/// 1. **It was added after its event already existed**, so engines older than
///    the field still emit the event without it — and the TUI and desktop
///    launch whichever `openseek` is on PATH, making a new reader against an
///    old engine a real pairing. Every event field defaulted here is this case.
/// 2. **The engine has treated it as optional since the event shipped** — a
///    flag whose absence has always meant something definite, rather than a
///    value gone missing. No *event* field is this case; `Command`'s `auto` is,
///    which is why that rule is stated separately in `command.mbt` rather than
///    by pointing here.
///
/// Everything else is required. "No reader uses it" is *not* a reason to
/// default a field: `tool_call_id`, `usage`'s cache counters and
/// `mcp_tools_registered`'s `names` are all unread by every current client, yet
/// every engine that ever emitted those events emitted those fields, so their
/// absence means the line is not what it claims to be. Guessing there would
/// hide a real wire break behind a fabricated value.
///
/// Three fields qualify today, all under (1), each verified against `git log -S`
/// for the field versus its event's introducing commit — the check to run
/// before adding a fourth, and the one that catches a field wrongly made
/// required, as `workspace_root` was:
///
/// - `steer_applied`'s `kind` — added 9b2f6c43 (2026-06-25) to an event from
///   56140618.
/// - `session_started`'s `workspace_root` — added d5d0b208 (2026-06-13, with
///   `--dir`) to an event from d11e04c2 (2026-06-12).
/// - `tool_result`'s `brief` — added 4eacdc95 to an event from 1f761102.
pub fn parse(line : Json) -> Event? {
  guard line is Object(fields) else { return None }
  guard text(fields, "event") is Some(name) else { return None }
  match name {
    "agent_setup_failed" =>
      text(fields, "error").map(error => AgentSetupFailed(error~))
    "agent_step" => int(fields, "step").map(step => AgentStep(step~))
    "agent_aborted" =>
      text(fields, "reason").map(reason => AgentAborted(reason~))
    "agent_finished" =>
      text(fields, "answer").map(answer => AgentFinished(answer~))
    "max_steps_exhausted" => Some(MaxStepsExhausted)
    "turn_failed" => text(fields, "error").map(error => TurnFailed(error~))
    "assistant_delta" =>
      text(fields, "content").map(content => AssistantDelta(content~))
    "reasoning_delta" =>
      text(fields, "content").map(content => ReasoningDelta(content~))
    "assistant_message" =>
      text(fields, "content").map(content => AssistantMessage(content~))
    "reasoning_message" =>
      text(fields, "content").map(content => ReasoningMessage(content~))
    // Decoded field by field rather than through `Usage`'s derived `FromJson`,
    // which reads a fractional number as an `Int` by truncating it: a counter
    // of `1.5` is not a token count, and every other integer on the wire is
    // rejected for exactly that. `int` is the same check used at the top level.
    "usage" => {
      guard fields.get("usage") is Some(Object(usage)) &&
        int(usage, "prompt_tokens") is Some(prompt_tokens) &&
        int(usage, "completion_tokens") is Some(completion_tokens) &&
        int(usage, "total_tokens") is Some(total_tokens) &&
        int(usage, "prompt_cache_hit_tokens") is Some(prompt_cache_hit_tokens) &&
        int(usage, "prompt_cache_miss_tokens") is Some(prompt_cache_miss_tokens) else {
        return None
      }
      Some(
        Usage(usage={
          prompt_tokens,
          completion_tokens,
          total_tokens,
          prompt_cache_hit_tokens,
          prompt_cache_miss_tokens,
        }),
      )
    }
    "tool_result" => {
      guard text(fields, "tool_call_id") is Some(tool_call_id) &&
        text(fields, "tool_name") is Some(tool_name) &&
        bool(fields, "is_error") is Some(is_error) &&
        text(fields, "content") is Some(content) else {
        return None
      }
      Some(
        ToolResult(
          tool_call_id~,
          tool_name~,
          is_error~,
          content~,
          brief=text(fields, "brief"),
        ),
      )
    }
    "tool_call_decode_error" => {
      guard text(fields, "tool_call_id") is Some(tool_call_id) &&
        text(fields, "tool_name") is Some(tool_name) &&
        text(fields, "error") is Some(error) else {
        return None
      }
      Some(ToolCallDecodeError(tool_call_id~, tool_name~, error~))
    }
    "approval_requested" => {
      guard text(fields, "id") is Some(id) &&
        text(fields, "tool_name") is Some(tool_name) &&
        text(fields, "detail") is Some(detail) else {
        return None
      }
      Some(
        ApprovalRequested(id~, tool_name~, detail~, body=text(fields, "body")),
      )
    }
    // `outcome` is carried as the wire's own string rather than decoded into a
    // closed type: a controller renders it, and an engine newer than the
    // controller must be able to settle a prompt with a word this build has
    // never heard of instead of having the whole event dropped as unreadable.
    "approval_resolved" => {
      guard text(fields, "id") is Some(id) &&
        text(fields, "outcome") is Some(outcome) else {
        return None
      }
      Some(ApprovalResolved(id~, outcome~))
    }
    // `kind` is defaulted, not required: engines older than 2026-06-25
    // (9b2f6c43, which added it) emit `steer_applied` with only `content`, and
    // the TUI launches whichever `openseek` is on PATH — so a new reader against
    // an old engine is a real pairing. "prompt" reproduces what those engines
    // meant, and is what every client defaulted to before this package existed.
    "steer_applied" =>
      text(fields, "content").map(content => {
        SteerApplied(kind=text(fields, "kind").unwrap_or("prompt"), content~)
      })
    "steer_dropped" =>
      text(fields, "content").map(content => SteerDropped(content~))
    "background_notice" =>
      text(fields, "content").map(content => BackgroundNotice(content~))
    // `goal` is null when the goal was cleared: absent and null are the same
    // value here, so the key's presence is not itself required.
    "goal_updated" => Some(GoalUpdated(goal=text(fields, "goal")))
    "goal_blocked" => text(fields, "reason").map(reason => GoalBlocked(reason~))
    "goal_unblocked" => Some(GoalUnblocked)
    "goal_check" => text(fields, "content").map(content => GoalCheck(content~))
    "goal_reminder" =>
      text(fields, "content").map(content => GoalReminder(content~))
    "goal_continue" =>
      int(fields, "remaining").map(remaining => GoalContinue(remaining~))
    "goal_budget_exhausted" =>
      int(fields, "turns").map(turns => GoalBudgetExhausted(turns~))
    "plan_reminder" =>
      text(fields, "content").map(content => PlanReminder(content~))
    "compaction_started" => {
      guard sequences(fields) is Some((from_sequence, to_sequence)) else {
        return None
      }
      Some(CompactionStarted(from_sequence~, to_sequence~))
    }
    "compaction_finished" => {
      guard sequences(fields) is Some((from_sequence, to_sequence)) &&
        text(fields, "summary") is Some(summary) else {
        return None
      }
      Some(CompactionFinished(from_sequence~, to_sequence~, summary~))
    }
    "compaction_failed" =>
      text(fields, "error").map(error => CompactionFailed(error~))
    "subrun_started" => {
      guard text(fields, "id") is Some(id) &&
        text(fields, "kind") is Some(kind) &&
        text(fields, "label") is Some(label) else {
        return None
      }
      Some(SubrunStarted(id~, kind~, label~))
    }
    "subrun_finished" => {
      guard text(fields, "id") is Some(id) &&
        text(fields, "status") is Some(status) &&
        int(fields, "steps") is Some(steps) &&
        int(fields, "prompt_tokens") is Some(prompt_tokens) &&
        int(fields, "completion_tokens") is Some(completion_tokens) else {
        return None
      }
      Some(
        SubrunFinished(id~, status~, steps~, prompt_tokens~, completion_tokens~),
      )
    }
    "auto_compaction_started" => {
      guard sequences(fields) is Some((from_sequence, to_sequence)) else {
        return None
      }
      Some(AutoCompactionStarted(from_sequence~, to_sequence~))
    }
    "auto_compaction_finished" => {
      guard sequences(fields) is Some((from_sequence, to_sequence)) &&
        text(fields, "summary") is Some(summary) else {
        return None
      }
      Some(AutoCompactionFinished(from_sequence~, to_sequence~, summary~))
    }
    "auto_compaction_failed" =>
      text(fields, "error").map(error => AutoCompactionFailed(error~))
    "context_yield" => {
      guard int(fields, "to_sequence") is Some(to_sequence) &&
        text(fields, "answer") is Some(answer) else {
        return None
      }
      Some(ContextYield(to_sequence~, answer~))
    }
    "session_started" => {
      guard text(fields, "session") is Some(session) &&
        text(fields, "session_root") is Some(session_root) else {
        return None
      }
      Some(
        SessionStarted(
          session~,
          session_root~,
          workspace_root=text(fields, "workspace_root"),
        ),
      )
    }
    "session_error" => text(fields, "error").map(error => SessionError(error~))
    "workspace_created" =>
      text(fields, "dir").map(dir => WorkspaceCreated(dir~))
    "command_error" => text(fields, "error").map(error => CommandError(error~))
    "fleet_started" => {
      guard int(fields, "runs") is Some(runs) &&
        text(fields, "task") is Some(task) else {
        return None
      }
      Some(FleetStarted(runs~, task~))
    }
    "mcp_config_ignored" =>
      text(fields, "reason").map(reason => McpConfigIgnored(reason~))
    "mcp_config_unreadable" => {
      guard text(fields, "path") is Some(path) &&
        text(fields, "error") is Some(error) else {
        return None
      }
      Some(McpConfigUnreadable(path~, error~))
    }
    "mcp_config_invalid" => {
      guard text(fields, "path") is Some(path) &&
        text(fields, "error") is Some(error) else {
        return None
      }
      Some(McpConfigInvalid(path~, error~))
    }
    "mcp_tools_registered" => {
      guard int(fields, "servers") is Some(servers) &&
        int(fields, "tools") is Some(tools) &&
        fields.get("names") is Some(Array(names)) else {
        return None
      }
      let decoded = []
      for name in names {
        guard name is String(name) else { return None }
        decoded.push(name)
      }
      Some(McpToolsRegistered(servers~, tools~, names=decoded))
    }
    "mcp_tool_duplicate" => {
      guard text(fields, "server") is Some(server) &&
        text(fields, "tool") is Some(tool) else {
        return None
      }
      Some(McpToolDuplicate(server~, tool~))
    }
    "mcp_tool_renamed" => {
      guard text(fields, "from") is Some(from) && text(fields, "to") is Some(to) else {
        return None
      }
      Some(McpToolRenamed(from~, to~))
    }
    "mcp_tools_capped" => {
      guard text(fields, "server") is Some(server) &&
        int(fields, "kept") is Some(kept) else {
        return None
      }
      Some(McpToolsCapped(server~, kept~))
    }
    "mcp_server_skipped_over_cap" =>
      text(fields, "server").map(server => McpServerSkippedOverCap(server~))
    // `error` is null when the connection yielded no error value.
    "mcp_connect_failed" =>
      text(fields, "server").map(server => {
        McpConnectFailed(server~, error=text(fields, "error"))
      })
    "mcp_list_tools_failed" => {
      guard text(fields, "server") is Some(server) &&
        text(fields, "error") is Some(error) else {
        return None
      }
      Some(McpListToolsFailed(server~, error~))
    }
    "mcp_list_tools_timeout" =>
      text(fields, "server").map(server => McpListToolsTimeout(server~))
    "mcp_no_tools" => text(fields, "server").map(server => McpNoTools(server~))
    _ => None
  }
}

///|
fn text(fields : Map[String, Json], key : String) -> String? {
  guard fields.get(key) is Some(String(value)) else { return None }
  Some(value)
}

///|
/// JSON has one number type, so an `Int` field arrives as a `Number`; reject a
/// fractional value rather than silently truncating it.
fn int(fields : Map[String, Json], key : String) -> Int? {
  guard fields.get(key) is Some(Number(value, ..)) else { return None }
  let rounded = value.to_int()
  guard rounded.to_double() == value else { return None }
  Some(rounded)
}

///|
fn bool(fields : Map[String, Json], key : String) -> Bool? {
  match fields.get(key) {
    Some(True) => Some(true)
    Some(False) => Some(false)
    _ => None
  }
}

///|
/// The `from_sequence`/`to_sequence` pair every compaction event carries.
fn sequences(fields : Map[String, Json]) -> (Int, Int)? {
  guard int(fields, "from_sequence") is Some(from_sequence) &&
    int(fields, "to_sequence") is Some(to_sequence) else {
    return None
  }
  Some((from_sequence, to_sequence))
}