///|
let event_session_id_lens : @lens.Lens[Json] = @lens.root().json("sessionID")

///|
let event_part_text_lens : @lens.Lens[Json] = @lens.root()
  .object("part")
  .json("text")

///|
let event_part_call_id_lens : @lens.Lens[Json] = @lens.root()
  .object("part")
  .json("callID")

///|
let event_part_tool_lens : @lens.Lens[Json] = @lens.root()
  .object("part")
  .json("tool")

///|
let event_part_status_lens : @lens.Lens[Json] = @lens.root()
  .object("part")
  .object("state")
  .json("status")

///|
let usage_input_lens : @lens.Lens[Json] = @lens.root().json("input")

///|
let usage_output_lens : @lens.Lens[Json] = @lens.root().json("output")

///|
let usage_reasoning_lens : @lens.Lens[Json] = @lens.root().json("reasoning")

///|
let usage_cache_read_lens : @lens.Lens[Json] = @lens.root()
  .object("cache")
  .json("read")

///|
let usage_cache_write_lens : @lens.Lens[Json] = @lens.root()
  .object("cache")
  .json("write")

///|
let event_part_reason_lens : @lens.Lens[Json] = @lens.root()
  .object("part")
  .json("reason")

///|
let event_part_cost_lens : @lens.Lens[Json] = @lens.root()
  .object("part")
  .json("cost")

///|
let event_part_tokens_lens : @lens.Lens[Json] = @lens.root()
  .object("part")
  .json("tokens")

///|
let event_error_name_lens : @lens.Lens[Json] = @lens.root()
  .object("error")
  .json("name")

///|
let event_error_message_lens : @lens.Lens[Json] = @lens.root()
  .object("error")
  .object("data")
  .json("message")

///|
let thread_event_type_lens : @lens.Lens[Json] = @lens.root().json("type")

///|
/// A completed text part emitted by OpenCode.
pub(all) struct TextEvent {
  session_id : String
  text : String
} derive(Debug, Eq)

///|
pub impl @json.FromJson for TextEvent with fn from_json(json, path) {
  let session_id_path = event_session_id_lens.add_to_json_path(path)
  let text_path = event_part_text_lens.add_to_json_path(path)
  let session_id_value = event_session_id_lens.get_or_json_decode_error(
    json, path,
  )
  let text_value = event_part_text_lens.get_or_json_decode_error(json, path)
  {
    session_id: @json.FromJson::from_json(session_id_value, session_id_path),
    text: @json.FromJson::from_json(text_value, text_path),
  }
}

///|
/// A completed reasoning part emitted when thinking output is enabled.
pub(all) struct ReasoningEvent {
  session_id : String
  text : String
} derive(Debug, Eq)

///|
pub impl @json.FromJson for ReasoningEvent with fn from_json(json, path) {
  let session_id_path = event_session_id_lens.add_to_json_path(path)
  let text_path = event_part_text_lens.add_to_json_path(path)
  let session_id_value = event_session_id_lens.get_or_json_decode_error(
    json, path,
  )
  let text_value = event_part_text_lens.get_or_json_decode_error(json, path)
  {
    session_id: @json.FromJson::from_json(session_id_value, session_id_path),
    text: @json.FromJson::from_json(text_value, text_path),
  }
}

///|
/// The terminal or active state of an OpenCode tool call.
pub(all) enum ToolUseStatus {
  Completed
  Failed
} derive(Debug, Eq)

///|
pub impl @json.FromJson for ToolUseStatus with fn from_json(json, path) {
  match @json.FromJson::from_json(json, path) {
    "completed" => Completed
    "error" => Failed
    status =>
      raise @json.JsonDecodeError(
        (path, "ToolUseStatus::from_json: unsupported status: \{status}"),
      )
  }
}

///|
/// A tool-use part emitted by OpenCode.
pub(all) struct ToolUseEvent {
  session_id : String
  call_id : String
  tool : String
  status : ToolUseStatus
} derive(Debug, Eq)

///|
pub impl @json.FromJson for ToolUseEvent with fn from_json(json, path) {
  let session_id_path = event_session_id_lens.add_to_json_path(path)
  let call_id_path = event_part_call_id_lens.add_to_json_path(path)
  let tool_path = event_part_tool_lens.add_to_json_path(path)
  let status_path = event_part_status_lens.add_to_json_path(path)
  let session_id_value = event_session_id_lens.get_or_json_decode_error(
    json, path,
  )
  let call_id_value = event_part_call_id_lens.get_or_json_decode_error(
    json, path,
  )
  let tool_value = event_part_tool_lens.get_or_json_decode_error(json, path)
  let status_value = event_part_status_lens.get_or_json_decode_error(json, path)
  {
    session_id: @json.FromJson::from_json(session_id_value, session_id_path),
    call_id: @json.FromJson::from_json(call_id_value, call_id_path),
    tool: @json.FromJson::from_json(tool_value, tool_path),
    status: @json.FromJson::from_json(status_value, status_path),
  }
}

///|
/// A marker emitted when OpenCode begins a model step.
pub(all) struct StepStartEvent {
  session_id : String
} derive(Debug, Eq)

///|
pub impl @json.FromJson for StepStartEvent with fn from_json(json, path) {
  let session_id_path = event_session_id_lens.add_to_json_path(path)
  let session_id_value = event_session_id_lens.get_or_json_decode_error(
    json, path,
  )
  { session_id: @json.FromJson::from_json(session_id_value, session_id_path) }
}

///|
/// Token usage reported when OpenCode finishes a model step.
pub(all) struct Usage {
  input_tokens : Int
  output_tokens : Int
  reasoning_tokens : Int
  cache_read_tokens : Int
  cache_write_tokens : Int
} derive(Debug, Eq)

///|
pub impl @json.FromJson for Usage with fn from_json(json, path) {
  let input_path = usage_input_lens.add_to_json_path(path)
  let output_path = usage_output_lens.add_to_json_path(path)
  let reasoning_path = usage_reasoning_lens.add_to_json_path(path)
  let cache_read_path = usage_cache_read_lens.add_to_json_path(path)
  let cache_write_path = usage_cache_write_lens.add_to_json_path(path)
  let input_value = usage_input_lens.get_or_json_decode_error(json, path)
  let output_value = usage_output_lens.get_or_json_decode_error(json, path)
  let reasoning_value = usage_reasoning_lens.get_or_json_decode_error(
    json, path,
  )
  let cache_read_value = usage_cache_read_lens.get_or_json_decode_error(
    json, path,
  )
  let cache_write_value = usage_cache_write_lens.get_or_json_decode_error(
    json, path,
  )
  {
    input_tokens: @json.FromJson::from_json(input_value, input_path),
    output_tokens: @json.FromJson::from_json(output_value, output_path),
    reasoning_tokens: @json.FromJson::from_json(reasoning_value, reasoning_path),
    cache_read_tokens: @json.FromJson::from_json(
      cache_read_value, cache_read_path,
    ),
    cache_write_tokens: @json.FromJson::from_json(
      cache_write_value, cache_write_path,
    ),
  }
}

///|
/// A marker and usage payload emitted when OpenCode finishes a model step.
pub(all) struct StepFinishEvent {
  session_id : String
  reason : String
  cost : Double
  usage : Usage
} derive(Debug, Eq)

///|
pub impl @json.FromJson for StepFinishEvent with fn from_json(json, path) {
  let session_id_path = event_session_id_lens.add_to_json_path(path)
  let reason_path = event_part_reason_lens.add_to_json_path(path)
  let cost_path = event_part_cost_lens.add_to_json_path(path)
  let usage_path = event_part_tokens_lens.add_to_json_path(path)
  let session_id_value = event_session_id_lens.get_or_json_decode_error(
    json, path,
  )
  let reason_value = event_part_reason_lens.get_or_json_decode_error(json, path)
  let cost_value = event_part_cost_lens.get_or_json_decode_error(json, path)
  let usage_value = event_part_tokens_lens.get_or_json_decode_error(json, path)
  {
    session_id: @json.FromJson::from_json(session_id_value, session_id_path),
    reason: @json.FromJson::from_json(reason_value, reason_path),
    cost: @json.FromJson::from_json(cost_value, cost_path),
    usage: @json.FromJson::from_json(usage_value, usage_path),
  }
}

///|
/// A fatal error emitted by the OpenCode event stream.
pub(all) struct ThreadErrorEvent {
  session_id : String?
  message : String
} derive(Debug, Eq)

///|
pub impl @json.FromJson for ThreadErrorEvent with fn from_json(json, path) {
  let name_path = event_error_name_lens.add_to_json_path(path)
  let session_id_path = event_session_id_lens.add_to_json_path(path)
  let name = event_error_name_lens.get_or_json_decode_error(json, path)
  let data_message = event_error_message_lens.get(json) catch {
    @lens.LensError::LensError(_) => name
  }
  let session_id_value = event_session_id_lens.get_or_json_decode_error(
    json, path,
  )
  {
    session_id: Some(
      @json.FromJson::from_json(session_id_value, session_id_path),
    ),
    message: match data_message {
      String(message) => message
      _ => @json.FromJson::from_json(name, name_path)
    },
  }
}

///|
/// One typed JSONL event emitted by `opencode run --format json`.
pub(all) enum ThreadEvent {
  Text(TextEvent)
  Reasoning(ReasoningEvent)
  ToolUse(ToolUseEvent)
  StepStart(StepStartEvent)
  StepFinish(StepFinishEvent)
  StreamError(ThreadErrorEvent)
} derive(Debug, Eq)

///|
pub impl @json.FromJson for ThreadEvent with fn from_json(json, path) {
  let type_path = thread_event_type_lens.add_to_json_path(path)
  let type_value = thread_event_type_lens.get_or_json_decode_error(json, path)
  let event_type : String = @json.FromJson::from_json(type_value, type_path)
  match event_type {
    "text" => Text(@json.FromJson::from_json(json, path))
    "reasoning" => Reasoning(@json.FromJson::from_json(json, path))
    "tool_use" => ToolUse(@json.FromJson::from_json(json, path))
    "step_start" => StepStart(@json.FromJson::from_json(json, path))
    "step_finish" => StepFinish(@json.FromJson::from_json(json, path))
    "error" => StreamError(@json.FromJson::from_json(json, path))
    _ =>
      raise thread_event_type_lens.json_decode_error(
        path,
        "unsupported thread event type: \{event_type}",
      )
  }
}

///|
/// Return the session identifier carried by an event, when present.
fn ThreadEvent::session_id(self : ThreadEvent) -> String? {
  match self {
    Text(event) => Some(event.session_id)
    Reasoning(event) => Some(event.session_id)
    ToolUse(event) => Some(event.session_id)
    StepStart(event) => Some(event.session_id)
    StepFinish(event) => Some(event.session_id)
    StreamError(event) => event.session_id
  }
}