// -- Newtype ID serialization ------------------------------------------------

///|
pub impl ToJson for CodexThreadId with fn to_json(self) {
  self.0.to_json()
}

///|
pub impl @json.FromJson for CodexThreadId with fn from_json(json, path) {
  CodexThreadId(@json.FromJson::from_json(json, path))
}

///|
pub impl ToJson for CodexTurnId with fn to_json(self) {
  self.0.to_json()
}

///|
pub impl @json.FromJson for CodexTurnId with fn from_json(json, path) {
  CodexTurnId(@json.FromJson::from_json(json, path))
}

///|
pub impl ToJson for CodexItemId with fn to_json(self) {
  self.0.to_json()
}

///|
pub impl @json.FromJson for CodexItemId with fn from_json(json, path) {
  CodexItemId(@json.FromJson::from_json(json, path))
}

///|
pub impl ToJson for CodexRequestId with fn to_json(self) {
  self.0
}

///|
pub impl @json.FromJson for CodexRequestId with fn from_json(json, _path) {
  CodexRequestId(json)
}

// -- Status enum serialization -----------------------------------------------

///|
pub impl ToJson for CodexThreadStatus with fn to_json(self) {
  let s = match self {
    Open => "open"
    Archived => "archived"
    Closed => "closed"
  }
  s.to_json()
}

///|
pub impl @json.FromJson for CodexThreadStatus with fn from_json(json, path) {
  let s : String = @json.FromJson::from_json(json, path)
  match s {
    "open" => Open
    "archived" => Archived
    "closed" => Closed
    _ => Open
  }
}

///|
pub impl ToJson for CodexStatus with fn to_json(self) {
  let s = match self {
    InProgress => "in_progress"
    Completed => "completed"
    Failed => "failed"
  }
  s.to_json()
}

///|
pub impl @json.FromJson for CodexStatus with fn from_json(json, path) {
  let s : String = @json.FromJson::from_json(json, path)
  match s {
    "in_progress" => InProgress
    "completed" => Completed
    "failed" => Failed
    _ => InProgress
  }
}

///|
fn parse_activity_status(status : Map[String, Json]) -> CodexActivityStatus {
  match status.get("type") {
    Some(String("active")) => {
      let flags : Array[CodexActiveFlag] = []
      if status.get("activeFlags") is Some(Array(arr)) {
        for item in arr {
          match item {
            String("waitingOnApproval") => flags.push(WaitingOnApproval)
            String("waitingOnUserInput") => flags.push(WaitingOnUserInput)
            _ => ()
          }
        }
      }
      Active(flags)
    }
    Some(String("notLoaded")) => NotLoaded
    Some(String("systemError")) => SystemError
    _ => Idle
  }
}

// -- CodexClientInfo ---------------------------------------------------------

///|
pub impl ToJson for CodexClientInfo with fn to_json(self) {
  { "name": self.name, "title": self.title, "version": self.version }
}

// -- CodexOutgoing -----------------------------------------------------------

///|
pub impl ToJson for CodexOutgoing with fn to_json(self) {
  match self {
    Initialize(id~, client_info~) =>
      {
        "method": "initialize",
        "id": id,
        "params": { "clientInfo": client_info.to_json() },
      }
    ThreadStart(id~, cwd~) =>
      { "method": "thread/start", "id": id, "params": { "cwd": cwd } }
    ThreadList(id~, cwd~) =>
      { "method": "thread/list", "id": id, "params": { "cwd": cwd } }
    ThreadRead(id~, thread_id~, include_turns~) =>
      {
        "method": "thread/read",
        "id": id,
        "params": { "threadId": thread_id.0, "includeTurns": include_turns },
      }
    ThreadResume(id~, thread_id~) =>
      {
        "method": "thread/resume",
        "id": id,
        "params": { "threadId": thread_id.0 },
      }
    TurnStart(id~, thread_id~, input~) =>
      {
        "method": "turn/start",
        "id": id,
        "params": {
          "threadId": thread_id.0,
          "input": [{ "type": "text", "text": input }],
        },
      }
    TurnInterrupt(id~, thread_id~, turn_id~) =>
      {
        "method": "turn/interrupt",
        "id": id,
        "params": { "threadId": thread_id.0, "turnId": turn_id.0 },
      }
    Initialized => { "method": "initialized", "params": Json::empty_object() }
    Approval(id~, decision~) => {
      let d = match decision {
        Accept => "accept"
        Decline => "decline"
      }
      { "id": id.0, "result": { "decision": d } }
    }
  }
}

// -- CodexIncoming -----------------------------------------------------------

///|
pub impl @json.FromJson for CodexIncoming with fn from_json(json, path) {
  guard json is Object(fields) else {
    raise JsonDecodeError((path, "expected JSON object"))
  }
  let has_method = fields.contains("method")
  let has_id = fields.contains("id")
  let has_result = fields.contains("result")
  let has_error = fields.contains("error")
  if !has_method && has_id && (has_result || has_error) {
    return parse_codex_response(fields, path)
  }
  guard fields.get("method") is Some(String(meth)) else {
    raise JsonDecodeError((path, "missing method"))
  }
  let params = match fields.get("params") {
    Some(Object(p)) => p
    _ => Map([])
  }
  if has_id {
    let id = match fields.get("id") {
      Some(v) => v
      _ => raise JsonDecodeError((path, "missing id"))
    }
    return parse_codex_server_request(meth, CodexRequestId(id), params, path)
  }
  parse_codex_notification(meth, params, path)
}

///|
fn parse_codex_response(
  fields : Map[String, Json],
  path : @json.JsonPath,
) -> CodexIncoming raise @json.JsonDecodeError {
  let id = match fields.get("id") {
    Some(v) => v
    _ => raise JsonDecodeError((path, "missing id"))
  }
  guard fields.get("result") is Some(Object(result)) else {
    return OtherResponse(id~)
  }
  // thread/list response: result.data is an array of threads
  if result.get("data") is Some(Array(data)) {
    let threads : Array[CodexThreadResult] = []
    for item in data {
      if item is Object(t) {
        if t.get("id") is Some(String(tid)) {
          threads.push(parse_thread_info(t, tid))
        }
      }
    }
    return ThreadListResult(threads~)
  }
  // thread/start or thread/read response: result.thread
  guard result.get("thread") is Some(Object(thread)) else {
    return OtherResponse(id~)
  }
  guard thread.get("id") is Some(String(thread_id)) else {
    return OtherResponse(id~)
  }
  let thread_info = parse_thread_info(thread, thread_id)
  // If turns are present and non-empty, treat as thread/read response
  if thread.get("turns") is Some(Array(turns_json)) && turns_json.length() > 0 {
    let turns = parse_read_turns(turns_json)
    return ThreadReadResult(thread=thread_info, turns~)
  }
  ThreadResult(thread=thread_info)
}

///|
fn parse_thread_info(
  thread : Map[String, Json],
  thread_id : String,
) -> CodexThreadResult {
  let preview = match thread.get("preview") {
    Some(String(p)) => p
    Some(Null) | None =>
      match thread.get("name") {
        Some(String(n)) => n
        _ => "New thread"
      }
    _ => "New thread"
  }
  let (status, activity) = match thread.get("status") {
    Some(String("archived")) => (Archived, Idle)
    Some(String("closed")) => (Closed, Idle)
    Some(Object(obj)) => {
      let activity = parse_activity_status(obj)
      (Open, activity)
    }
    _ => (Open, Idle)
  }
  { id: CodexThreadId(thread_id), preview, status, activity }
}

///|
fn parse_read_turns(turns_json : Array[Json]) -> Array[CodexReadTurn] {
  let turns : Array[CodexReadTurn] = []
  for turn_json in turns_json {
    guard turn_json is Object(t) else { continue }
    guard t.get("id") is Some(String(turn_id)) else { continue }
    let status : CodexStatus = match t.get("status") {
      Some(String("completed")) => Completed
      Some(String("failed"))
      | Some(String("systemError"))
      | Some(String("interrupted"))
      | Some(String("cancelled")) => Failed
      _ => Completed
    }
    let items : Array[CodexReadItem] = []
    if t.get("items") is Some(Array(items_json)) {
      for item_json in items_json {
        guard item_json is Object(item) else { continue }
        guard item.get("id") is Some(String(item_id)) else { continue }
        let kind_str = match item.get("type") {
          Some(String(s)) => s
          _ => continue
        }
        let (kind, text, command, output) = parse_read_item(kind_str, item)
        items.push({ id: CodexItemId(item_id), kind, text, command, output })
      }
    }
    turns.push({ id: CodexTurnId(turn_id), items, status })
  }
  turns
}

///|
fn parse_read_item(
  kind_str : String,
  item : Map[String, Json],
) -> (CodexItemKind, String, String, String) {
  match kind_str {
    "userMessage" => {
      let text = match item.get("content") {
        Some(Array(content)) =>
          content
          .iter()
          .filter_map(fn(c) {
            guard c is Object(obj) else { return None }
            guard obj.get("type") is Some(String("text")) else { return None }
            match obj.get("text") {
              Some(String(t)) => Some(t)
              _ => None
            }
          })
          .collect()
          .join("\n")
        _ => ""
      }
      (UserMessage, text, "", "")
    }
    "agentMessage" => {
      let text = match item.get("text") {
        Some(String(s)) => s
        _ => ""
      }
      (AgentMessage, text, "", "")
    }
    "commandExecution" => {
      let command = match item.get("command") {
        Some(String(s)) => s
        _ => ""
      }
      let output = match item.get("aggregatedOutput") {
        Some(String(s)) => s
        _ => ""
      }
      (CommandExecution, "", command, output)
    }
    "fileChange" => {
      let text = match item.get("changes") {
        Some(Array(changes)) => {
          let parts : Array[String] = []
          for c in changes {
            guard c is Object(change) else { continue }
            match change.get("path") {
              Some(String(p)) => parts.push(p)
              _ => ()
            }
          }
          if parts.is_empty() {
            "File change"
          } else {
            parts.join(", ")
          }
        }
        _ => "File change"
      }
      (FileChange, text, "", "")
    }
    "reasoning" => {
      let text = match item.get("summary") {
        Some(Array(parts)) =>
          parts
          .iter()
          .filter_map(fn(p) {
            guard p is String(s) else { return None }
            Some(s)
          })
          .collect()
          .join("\n")
        _ =>
          match item.get("content") {
            Some(Array(parts)) =>
              parts
              .iter()
              .filter_map(fn(p) {
                guard p is String(s) else { return None }
                Some(s)
              })
              .collect()
              .join("\n")
            _ => ""
          }
      }
      (Reasoning, text, "", "")
    }
    _ => (AgentMessage, "", "", "")
  }
}

///|
fn parse_codex_notification(
  meth : String,
  params : Map[String, Json],
  path : @json.JsonPath,
) -> CodexIncoming raise @json.JsonDecodeError {
  match meth {
    "turn/started" => {
      guard params.get("turn") is Some(Object(turn)) else {
        raise JsonDecodeError((path, "missing turn"))
      }
      guard turn.get("id") is Some(String(turn_id)) else {
        raise JsonDecodeError((path, "missing turn.id"))
      }
      TurnStarted(turn_id=CodexTurnId(turn_id))
    }
    "turn/completed" => {
      guard params.get("turn") is Some(Object(turn)) else {
        raise JsonDecodeError((path, "missing turn"))
      }
      guard turn.get("id") is Some(String(turn_id)) else {
        raise JsonDecodeError((path, "missing turn.id"))
      }
      let status = match turn.get("status") {
        Some(String("completed")) => Completed
        Some(String("failed")) => Failed
        _ => Completed
      }
      TurnCompleted(turn_id=CodexTurnId(turn_id), status~)
    }
    "item/started" => {
      guard params.get("turnId") is Some(String(turn_id)) else {
        raise JsonDecodeError((path, "missing turnId"))
      }
      guard params.get("item") is Some(Object(item)) else {
        raise JsonDecodeError((path, "missing item"))
      }
      guard item.get("id") is Some(String(item_id)) else {
        raise JsonDecodeError((path, "missing item.id"))
      }
      let kind = match item.get("type") {
        Some(String("agentMessage")) => AgentMessage
        Some(String("userMessage")) => UserMessage
        Some(String("commandExecution")) => CommandExecution
        Some(String("fileChange")) => FileChange
        Some(String("reasoning")) => Reasoning
        _ => AgentMessage
      }
      let command = match item.get("command") {
        Some(String(s)) => s
        _ => ""
      }
      // Extract text from userMessage content array
      let text = match kind {
        UserMessage => {
          let (_, t, _, _) = parse_read_item("userMessage", item)
          t
        }
        _ => ""
      }
      ItemStarted(
        turn_id=CodexTurnId(turn_id),
        item_id=CodexItemId(item_id),
        kind~,
        text~,
        command~,
      )
    }
    "item/completed" => {
      guard params.get("turnId") is Some(String(turn_id)) else {
        raise JsonDecodeError((path, "missing turnId"))
      }
      guard params.get("item") is Some(Object(item)) else {
        raise JsonDecodeError((path, "missing item"))
      }
      guard item.get("id") is Some(String(item_id)) else {
        raise JsonDecodeError((path, "missing item.id"))
      }
      let status = match item.get("status") {
        Some(String("completed")) => Completed
        Some(String("failed")) => Failed
        _ => Completed
      }
      ItemCompleted(
        turn_id=CodexTurnId(turn_id),
        item_id=CodexItemId(item_id),
        status~,
      )
    }
    "item/agentMessage/delta" => parse_codex_delta(params, path, AgentMessage)
    "item/commandExecution/outputDelta" =>
      parse_codex_delta(params, path, CommandExecution)
    "item/fileChange/outputDelta" => parse_codex_delta(params, path, FileChange)
    "thread/status/changed" => {
      guard params.get("threadId") is Some(String(thread_id)) else {
        raise JsonDecodeError((path, "missing threadId"))
      }
      let activity = match params.get("status") {
        Some(Object(obj)) => parse_activity_status(obj)
        _ => Idle
      }
      ThreadStatusChanged(thread_id=CodexThreadId(thread_id), activity~)
    }
    _ => raise JsonDecodeError((path, "unknown codex notification: \{meth}"))
  }
}

///|
fn parse_codex_delta(
  params : Map[String, Json],
  path : @json.JsonPath,
  kind : CodexItemKind,
) -> CodexIncoming raise @json.JsonDecodeError {
  guard params.get("turnId") is Some(String(turn_id)) else {
    raise JsonDecodeError((path, "missing turnId"))
  }
  guard params.get("itemId") is Some(String(item_id)) else {
    raise JsonDecodeError((path, "missing itemId"))
  }
  guard params.get("delta") is Some(String(delta)) else {
    raise JsonDecodeError((path, "missing delta"))
  }
  Delta(
    turn_id=CodexTurnId(turn_id),
    item_id=CodexItemId(item_id),
    delta~,
    kind~,
  )
}

///|
fn parse_codex_server_request(
  meth : String,
  id : CodexRequestId,
  params : Map[String, Json],
  path : @json.JsonPath,
) -> CodexIncoming raise @json.JsonDecodeError {
  let thread_id = match params.get("threadId") {
    Some(String(s)) => CodexThreadId(s)
    _ => CodexThreadId("")
  }
  let turn_id = match params.get("turnId") {
    Some(String(s)) => CodexTurnId(s)
    _ => CodexTurnId("")
  }
  let item_id = match params.get("itemId") {
    Some(String(s)) => CodexItemId(s)
    _ => CodexItemId("")
  }
  let approval = match meth {
    "item/commandExecution/requestApproval" => {
      let command = match params.get("command") {
        Some(String(s)) => s
        _ => "(unknown command)"
      }
      CodexApproval::Command(command~)
    }
    "item/fileChange/requestApproval" => {
      let reason = match params.get("reason") {
        Some(String(s)) => s
        _ => "File change requested"
      }
      FileChange(reason~)
    }
    _ => raise JsonDecodeError((path, "unknown codex request: \{meth}"))
  }
  Approval(id~, thread_id~, turn_id~, item_id~, approval~)
}