///|
pub impl ToJson for FileStat with fn to_json(self) {
  {
    "file_type": self.file_type,
    "size": Json::number(self.size.to_double(), repr=self.size.to_string()),
    "mtime": Json::number(self.mtime.to_double(), repr=self.mtime.to_string()),
    "ctime": Json::number(self.ctime.to_double(), repr=self.ctime.to_string()),
  }
}

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

///|
pub impl @json.FromJson for FileStat with fn from_json(json, path) {
  guard json is Object(m) else {
    raise JsonDecodeError((path, "expected object for FileStat"))
  }
  guard m.get("file_type") is Some(Number(ft, ..)) else {
    raise JsonDecodeError((path, "missing field 'file_type'"))
  }
  {
    file_type: ft.to_int(),
    size: get_int64_field(m, "size", path),
    mtime: get_int64_field(m, "mtime", path),
    ctime: get_int64_field(m, "ctime", path),
  }
}

///|
pub impl ToJson for FsMessage with fn to_json(self) {
  match self {
    WatchFile(req_id~, client_id~, path~) =>
      {
        "ch": "fs",
        "type": "watch_file",
        "req_id": req_id,
        "client_id": client_id,
        "path": path,
      }
    UnwatchFile(req_id~, client_id~) =>
      {
        "ch": "fs",
        "type": "unwatch_file",
        "req_id": req_id,
        "client_id": client_id,
      }
    WatchFileReady(req_id~, client_id~, path~, resolved_path~) =>
      {
        "ch": "fs",
        "type": "watch_file_ready",
        "req_id": req_id,
        "client_id": client_id,
        "path": path,
        "resolved_path": resolved_path,
      }
    FileChanged(req_id~, client_id~, path~, resolved_path~) =>
      {
        "ch": "fs",
        "type": "file_changed",
        "req_id": req_id,
        "client_id": client_id,
        "path": path,
        "resolved_path": resolved_path,
      }
    Stat(req_id~, client_id~, path~) =>
      {
        "ch": "fs",
        "type": "stat",
        "req_id": req_id,
        "client_id": client_id,
        "path": path,
      }
    StatResult(req_id~, client_id~, stat~) =>
      {
        "ch": "fs",
        "type": "stat_result",
        "req_id": req_id,
        "client_id": client_id,
        "stat": stat.to_json(),
      }
    Readdir(req_id~, client_id~, path~) =>
      {
        "ch": "fs",
        "type": "readdir",
        "req_id": req_id,
        "client_id": client_id,
        "path": path,
      }
    ReaddirResult(req_id~, client_id~, entries~) => {
      let arr : Json = entries
        .map(fn(e) {
          let j : Json = [e.0, e.1]
          j
        })
        .to_json()
      {
        "ch": "fs",
        "type": "readdir_result",
        "req_id": req_id,
        "client_id": client_id,
        "entries": arr,
      }
    }
    ReadFile(req_id~, client_id~, path~) =>
      {
        "ch": "fs",
        "type": "read_file",
        "req_id": req_id,
        "client_id": client_id,
        "path": path,
      }
    ReadFileResult(req_id~, client_id~, data~) =>
      {
        "ch": "fs",
        "type": "read_file_result",
        "req_id": req_id,
        "client_id": client_id,
        "data": data,
      }
    WriteFile(req_id~, client_id~, path~, data~, create~, overwrite~) =>
      {
        "ch": "fs",
        "type": "write_file",
        "req_id": req_id,
        "client_id": client_id,
        "path": path,
        "data": data,
        "create": create,
        "overwrite": overwrite,
      }
    Mkdir(req_id~, client_id~, path~) =>
      {
        "ch": "fs",
        "type": "mkdir",
        "req_id": req_id,
        "client_id": client_id,
        "path": path,
      }
    FsDelete(req_id~, client_id~, path~, recursive~) =>
      {
        "ch": "fs",
        "type": "delete",
        "req_id": req_id,
        "client_id": client_id,
        "path": path,
        "recursive": recursive,
      }
    FsRename(req_id~, client_id~, from~, to~, overwrite~) =>
      {
        "ch": "fs",
        "type": "rename",
        "req_id": req_id,
        "client_id": client_id,
        "from": from,
        "to": to,
        "overwrite": overwrite,
      }
    FsOk(req_id~, client_id~) =>
      { "ch": "fs", "type": "ok", "req_id": req_id, "client_id": client_id }
    FsError(req_id~, client_id~, code~, message~) =>
      {
        "ch": "fs",
        "type": "error",
        "req_id": req_id,
        "client_id": client_id,
        "code": code,
        "message": message,
      }
  }
}

///|
pub impl @json.FromJson for FsMessage with fn from_json(json, path) {
  guard json is Object(fields) else {
    raise JsonDecodeError((path, "expected JSON object"))
  }
  guard fields.get("type") is Some(String(t)) else {
    raise JsonDecodeError((path, "missing field 'type'"))
  }
  guard fields.get("req_id") is Some(String(req_id)) else {
    raise JsonDecodeError((path, "missing field 'req_id'"))
  }
  let client_id = match fields.get("client_id") {
    Some(String(s)) => s
    _ => ""
  }
  match t {
    "watch_file" => {
      guard fields.get("path") is Some(String(p)) else {
        raise JsonDecodeError((path, "missing field 'path'"))
      }
      WatchFile(req_id~, client_id~, path=p)
    }
    "unwatch_file" => UnwatchFile(req_id~, client_id~)
    "watch_file_ready" => {
      guard fields.get("path") is Some(String(p)) else {
        raise JsonDecodeError((path, "missing field 'path'"))
      }
      // Older protocol-v4 peers sent only `path`. Treat it as the effective
      // path so the additive canonical identity stays wire-compatible.
      let resolved_path = match fields.get("resolved_path") {
        Some(String(value)) => value
        _ => p
      }
      WatchFileReady(req_id~, client_id~, path=p, resolved_path~)
    }
    "file_changed" => {
      guard fields.get("path") is Some(String(p)) else {
        raise JsonDecodeError((path, "missing field 'path'"))
      }
      let resolved_path = match fields.get("resolved_path") {
        Some(String(value)) => value
        _ => p
      }
      FileChanged(req_id~, client_id~, path=p, resolved_path~)
    }
    "stat" => {
      guard fields.get("path") is Some(String(p)) else {
        raise JsonDecodeError((path, "missing field 'path'"))
      }
      Stat(req_id~, client_id~, path=p)
    }
    "stat_result" => {
      guard fields.get("stat") is Some(stat_json) else {
        raise JsonDecodeError((path, "missing field 'stat'"))
      }
      let stat : FileStat = @json.from_json(
        stat_json,
        path=path.add_key("stat"),
      )
      StatResult(req_id~, client_id~, stat~)
    }
    "readdir" => {
      guard fields.get("path") is Some(String(p)) else {
        raise JsonDecodeError((path, "missing field 'path'"))
      }
      Readdir(req_id~, client_id~, path=p)
    }
    "readdir_result" => {
      guard fields.get("entries") is Some(Array(arr)) else {
        raise JsonDecodeError((path, "missing field 'entries'"))
      }
      let entries : Array[(String, Int)] = []
      for item in arr {
        guard item is Array([String(name), Number(ft, ..)]) else {
          raise JsonDecodeError((path, "invalid entry in entries"))
        }
        entries.push((name, ft.to_int()))
      }
      ReaddirResult(req_id~, client_id~, entries~)
    }
    "read_file" => {
      guard fields.get("path") is Some(String(p)) else {
        raise JsonDecodeError((path, "missing field 'path'"))
      }
      ReadFile(req_id~, client_id~, path=p)
    }
    "read_file_result" => {
      guard fields.get("data") is Some(String(data)) else {
        raise JsonDecodeError((path, "missing field 'data'"))
      }
      ReadFileResult(req_id~, client_id~, data~)
    }
    "write_file" => {
      guard fields.get("path") is Some(String(p)) else {
        raise JsonDecodeError((path, "missing field 'path'"))
      }
      guard fields.get("data") is Some(String(data)) else {
        raise JsonDecodeError((path, "missing field 'data'"))
      }
      let create = match fields.get("create") {
        Some(True) => true
        _ => false
      }
      let overwrite = match fields.get("overwrite") {
        Some(True) => true
        _ => false
      }
      WriteFile(req_id~, client_id~, path=p, data~, create~, overwrite~)
    }
    "mkdir" => {
      guard fields.get("path") is Some(String(p)) else {
        raise JsonDecodeError((path, "missing field 'path'"))
      }
      Mkdir(req_id~, client_id~, path=p)
    }
    "delete" => {
      guard fields.get("path") is Some(String(p)) else {
        raise JsonDecodeError((path, "missing field 'path'"))
      }
      let recursive = match fields.get("recursive") {
        Some(True) => true
        _ => false
      }
      FsDelete(req_id~, client_id~, path=p, recursive~)
    }
    "rename" => {
      guard fields.get("from") is Some(String(from)) else {
        raise JsonDecodeError((path, "missing field 'from'"))
      }
      guard fields.get("to") is Some(String(to)) else {
        raise JsonDecodeError((path, "missing field 'to'"))
      }
      let overwrite = match fields.get("overwrite") {
        Some(True) => true
        _ => false
      }
      FsRename(req_id~, client_id~, from~, to~, overwrite~)
    }
    "ok" => FsOk(req_id~, client_id~)
    "error" => {
      guard fields.get("code") is Some(String(code)) else {
        raise JsonDecodeError((path, "missing field 'code'"))
      }
      guard fields.get("message") is Some(String(message)) else {
        raise JsonDecodeError((path, "missing field 'message'"))
      }
      FsError(req_id~, client_id~, code~, message~)
    }
    _ => raise JsonDecodeError((path, "unknown FsMessage type: \{t}"))
  }
}