///|
fn last_seen_id_to_json(id : Int64?) -> Json {
  match id {
    Some(n) => Json::number(n.to_double(), repr=n.to_string())
    None => Json::null()
  }
}

///|
fn id_to_json(id : Int64) -> Json {
  Json::number(id.to_double(), repr=id.to_string())
}

///|
fn parse_optional_int64(
  m : Map[String, Json],
  key : String,
  path : @json.JsonPath,
) -> Int64? raise @json.JsonDecodeError {
  match m.get(key) {
    None | Some(Null) => None
    Some(Number(n, ..)) => Some(n.to_int64())
    Some(String(s)) =>
      Some(
        @string.parse_int64(s) catch {
          _ => raise @json.JsonDecodeError((path, "invalid int64 for '\{key}'"))
        },
      )
    _ => raise @json.JsonDecodeError((path, "invalid type for '\{key}'"))
  }
}

///|
pub impl ToJson for SysMessage with to_json(self) {
  match self {
    Register(task_uuid~, hostname~, cwd~) =>
      {
        "ch": "sys",
        "type": "register",
        "task_uuid": task_uuid.to_string(),
        "hostname": hostname,
        "cwd": cwd,
      }
    Registered(task_uuid~) =>
      { "ch": "sys", "type": "registered", "task_uuid": task_uuid.to_string() }
    RegisterRejected(reason~) =>
      { "ch": "sys", "type": "register_rejected", "reason": reason.to_wire() }
    TaskExited(exit_code~) =>
      { "ch": "sys", "type": "task_exited", "exit_code": exit_code }
    Connect(task_uuid~, last_seen_id~) =>
      {
        "ch": "sys",
        "type": "connect",
        "task_uuid": task_uuid.to_string(),
        "last_seen_id": last_seen_id_to_json(last_seen_id),
      }
    Connected => { "ch": "sys", "type": "connected" }
    TaskOnline => { "ch": "sys", "type": "task_online" }
    NewClient(client_id~, last_seen_id~) =>
      {
        "ch": "sys",
        "type": "new_client",
        "client_id": client_id,
        "last_seen_id": last_seen_id_to_json(last_seen_id),
      }
    ClientDisconnected(client_id~) =>
      { "ch": "sys", "type": "client_disconnected", "client_id": client_id }
    Error(code) => { "ch": "sys", "type": "error", "code": code.to_wire() }
    AuthError(message~) =>
      { "ch": "sys", "type": "auth_error", "message": message }
  }
}

///|
pub impl @json.FromJson for SysMessage with from_json(json, path) {
  guard json is Object(fields) else {
    raise @json.JsonDecodeError((path, "expected JSON object"))
  }
  let t = match fields.get("type") {
    Some(String(s)) => s
    _ => raise @json.JsonDecodeError((path, "missing field 'type'"))
  }
  match t {
    "register" => {
      guard fields.get("task_uuid") is Some(String(uuid_str)) else {
        raise @json.JsonDecodeError((path, "missing field 'task_uuid'"))
      }
      let task_uuid = @uuid.from_hex(uuid_str) catch {
        _ =>
          raise @json.JsonDecodeError((path, "invalid task_uuid: \{uuid_str}"))
      }
      let hostname = match fields.get("hostname") {
        Some(String(s)) => s
        _ => ""
      }
      let cwd = match fields.get("cwd") {
        Some(String(s)) => s
        _ => ""
      }
      Register(task_uuid~, hostname~, cwd~)
    }
    "registered" => {
      guard fields.get("task_uuid") is Some(String(uuid_str)) else {
        raise @json.JsonDecodeError((path, "missing field 'task_uuid'"))
      }
      let task_uuid = @uuid.from_hex(uuid_str) catch {
        _ =>
          raise @json.JsonDecodeError((path, "invalid task_uuid: \{uuid_str}"))
      }
      Registered(task_uuid~)
    }
    "register_rejected" => {
      guard fields.get("reason") is Some(String(reason_str)) else {
        raise @json.JsonDecodeError((path, "missing field 'reason'"))
      }
      let reason = match RegisterRejectReason::from_wire(reason_str) {
        Some(r) => r
        None =>
          raise @json.JsonDecodeError(
            (path, "unknown register_rejected reason: \{reason_str}"),
          )
      }
      RegisterRejected(reason~)
    }
    "task_exited" => {
      guard fields.get("exit_code") is Some(Number(n, ..)) else {
        raise @json.JsonDecodeError((path, "missing field 'exit_code'"))
      }
      TaskExited(exit_code=n.to_int())
    }
    "connect" => {
      guard fields.get("task_uuid") is Some(String(uuid_str)) else {
        raise @json.JsonDecodeError((path, "missing field 'task_uuid'"))
      }
      let task_uuid = @uuid.from_hex(uuid_str) catch {
        _ =>
          raise @json.JsonDecodeError((path, "invalid task_uuid: \{uuid_str}"))
      }
      let last_seen_id = parse_optional_int64(fields, "last_seen_id", path)
      Connect(task_uuid~, last_seen_id~)
    }
    "connected" => Connected
    "task_online" => TaskOnline
    "new_client" => {
      guard fields.get("client_id") is Some(String(id)) else {
        raise @json.JsonDecodeError((path, "missing field 'client_id'"))
      }
      let last_seen_id = parse_optional_int64(fields, "last_seen_id", path)
      NewClient(client_id=id, last_seen_id~)
    }
    "client_disconnected" => {
      guard fields.get("client_id") is Some(String(id)) else {
        raise @json.JsonDecodeError((path, "missing field 'client_id'"))
      }
      ClientDisconnected(client_id=id)
    }
    "error" => {
      guard fields.get("code") is Some(String(code_str)) else {
        raise @json.JsonDecodeError((path, "missing field 'code'"))
      }
      let code = match ErrorCode::from_wire(code_str) {
        Some(c) => c
        None =>
          raise @json.JsonDecodeError((path, "unknown error code: \{code_str}"))
      }
      Error(code)
    }
    "auth_error" => {
      guard fields.get("message") is Some(String(msg)) else {
        raise @json.JsonDecodeError((path, "missing field 'message'"))
      }
      AuthError(message=msg)
    }
    _ => raise @json.JsonDecodeError((path, "unknown sys message type: \{t}"))
  }
}

///|
pub impl ToJson for TermMessage with to_json(self) {
  match self {
    Data(client_id~, id~, data~) =>
      {
        "ch": "t",
        "type": "data",
        "client_id": client_id,
        "id": id_to_json(id),
        "data": data,
      }
    Exited(client_id~, id~) =>
      {
        "ch": "t",
        "type": "exited",
        "client_id": client_id,
        "id": id_to_json(id),
      }
  }
}

///|
pub impl @json.FromJson for TermMessage with from_json(json, path) {
  guard json is Object(fields) else {
    raise @json.JsonDecodeError((path, "expected JSON object"))
  }
  let t = match fields.get("type") {
    Some(String(s)) => s
    _ => raise @json.JsonDecodeError((path, "missing field 'type'"))
  }
  match t {
    "data" => {
      let client_id = match fields.get("client_id") {
        Some(String(s)) => s
        _ => ""
      }
      let id = parse_optional_int64(fields, "id", path).unwrap_or(0L)
      guard fields.get("data") is Some(String(data)) else {
        raise @json.JsonDecodeError((path, "missing field 'data'"))
      }
      Data(client_id~, id~, data~)
    }
    "exited" => {
      let client_id = match fields.get("client_id") {
        Some(String(s)) => s
        _ => ""
      }
      let id = parse_optional_int64(fields, "id", path).unwrap_or(0L)
      Exited(client_id~, id~)
    }
    _ => raise @json.JsonDecodeError((path, "unknown term message type: \{t}"))
  }
}

///|
pub impl ToJson for Message with to_json(self) {
  match self {
    Sys(msg) => msg.to_json()
    Fs(msg) => msg.to_json()
    Term(msg) => msg.to_json()
    Codex(client_id~, data~) =>
      { "ch": "codex", "client_id": client_id, "data": data }
  }
}

///|
pub impl @json.FromJson for Message with from_json(json, path) {
  guard json is Object(fields) else {
    raise @json.JsonDecodeError((path, "expected JSON object"))
  }
  let ch = match fields.get("ch") {
    Some(String(c)) => c
    _ => "sys"
  }
  match ch {
    "sys" => Sys(@json.from_json(json, path~))
    "t" => Term(@json.from_json(json, path~))
    "fs" => Fs(@json.from_json(json, path~))
    "codex" => {
      let client_id = match fields.get("client_id") {
        Some(String(s)) => s
        _ => ""
      }
      guard fields.get("data") is Some(String(data)) else {
        raise @json.JsonDecodeError((path, "missing field 'data'"))
      }
      Codex(client_id~, data~)
    }
    _ => raise @json.JsonDecodeError((path, "unknown channel: \{ch}"))
  }
}