// -- API response types ------------------------------------------------------

///|
/// Flat row for the `GET /api/tasks` listing. One row per running task
/// instance (keyed by `task_uuid`), joined with its parent machine's display
/// info. `machine_id` groups tasks that belong to the same long-lived machine;
/// `task_uuid` is the identity used everywhere else on the wire.
pub(all) struct TaskEntry {
  task_uuid : @uuid.UUID
  machine_id : Int
  hostname : String
  cwd : String
  /// Latest terminal title (OSC 0/2) reported by the task's server, if any.
  title : String?
  owner : String
  online : Bool
  last_seen_at : Int64 // unix millis
} derive(Eq)

///|
pub impl ToJson for TaskEntry with fn to_json(self) {
  let obj : Map[String, Json] = {
    "task_uuid": self.task_uuid.to_string(),
    "machine_id": self.machine_id,
    "hostname": self.hostname,
    "cwd": self.cwd,
    "owner": self.owner,
    "online": self.online,
    "last_seen_at": Json::number(
      self.last_seen_at.to_double(),
      repr=self.last_seen_at.to_string(),
    ),
  }
  if self.title is Some(title) {
    obj["title"] = Json::string(title)
  }
  Json::object(obj)
}

///|
pub impl @json.FromJson for TaskEntry with fn from_json(json, path) {
  guard json is Object(fields) else {
    raise JsonDecodeError((path, "expected JSON object"))
  }
  guard fields.get("task_uuid") is Some(String(uuid_str)) else {
    raise JsonDecodeError((path, "missing field 'task_uuid'"))
  }
  let task_uuid = @uuid.from_hex(uuid_str) catch {
    _ => raise JsonDecodeError((path, "invalid task_uuid: \{uuid_str}"))
  }
  guard fields.get("machine_id") is Some(Number(machine_id, ..)) else {
    raise JsonDecodeError((path, "missing field 'machine_id'"))
  }
  guard fields.get("hostname") is Some(String(hostname)) else {
    raise JsonDecodeError((path, "missing field 'hostname'"))
  }
  guard fields.get("cwd") is Some(String(cwd)) else {
    raise JsonDecodeError((path, "missing field 'cwd'"))
  }
  guard fields.get("owner") is Some(String(owner)) else {
    raise JsonDecodeError((path, "missing field 'owner'"))
  }
  let online = match fields.get("online") {
    Some(True) => true
    Some(False) => false
    _ => raise JsonDecodeError((path, "missing field 'online'"))
  }
  guard fields.get("last_seen_at") is Some(Number(last_seen_at, ..)) else {
    raise JsonDecodeError((path, "missing field 'last_seen_at'"))
  }
  {
    task_uuid,
    machine_id: machine_id.to_int(),
    hostname,
    cwd,
    title: parse_optional_string(fields, "title", path),
    owner,
    online,
    last_seen_at: last_seen_at.to_int64(),
  }
}

///|
pub(all) struct MachineAccessGroup {
  group_id : Int
  name : String
}

///|
pub impl ToJson for MachineAccessGroup with fn to_json(self) {
  { "group_id": self.group_id, "name": self.name }
}

///|
pub impl @json.FromJson for MachineAccessGroup with fn from_json(json, path) {
  guard json is Object(fields) else {
    raise JsonDecodeError((path, "expected JSON object"))
  }
  guard fields.get("group_id") is Some(Number(group_id, ..)) else {
    raise JsonDecodeError((path, "missing field 'group_id'"))
  }
  guard fields.get("name") is Some(String(name)) else {
    raise JsonDecodeError((path, "missing field 'name'"))
  }
  { group_id: group_id.to_int(), name }
}

///|
pub(all) struct CreateMachineRequest {
  name : String
}

///|
pub impl ToJson for CreateMachineRequest with fn to_json(self) {
  { "name": self.name }
}

///|
pub impl @json.FromJson for CreateMachineRequest with fn from_json(json, path) {
  guard json is Object(fields) else {
    raise JsonDecodeError((path, "expected JSON object"))
  }
  guard fields.get("name") is Some(String(name)) else {
    raise JsonDecodeError((path, "missing field 'name'"))
  }
  { name, }
}

///|
pub(all) struct CreateMachineResponse {
  token : String
  machine_id : Int
}

///|
pub impl ToJson for CreateMachineResponse with fn to_json(self) {
  { "token": self.token, "server_id": self.machine_id }
}

///|
pub impl @json.FromJson for CreateMachineResponse with fn from_json(json, path) {
  guard json is Object(fields) else {
    raise JsonDecodeError((path, "expected JSON object"))
  }
  guard fields.get("token") is Some(String(token)) else {
    raise JsonDecodeError((path, "missing field 'token'"))
  }
  guard fields.get("server_id") is Some(Number(server_id, ..)) else {
    raise JsonDecodeError((path, "missing field 'server_id'"))
  }
  { token, machine_id: server_id.to_int() }
}

///|
pub(all) struct GroupInfo {
  id : Int
  name : String
  owner_id : Int64
}

///|
pub impl ToJson for GroupInfo with fn to_json(self) {
  {
    "id": self.id,
    "name": self.name,
    "owner_id": Json::number(
      self.owner_id.to_double(),
      repr=self.owner_id.to_string(),
    ),
  }
}

///|
pub impl @json.FromJson for GroupInfo with fn from_json(json, path) {
  guard json is Object(fields) else {
    raise JsonDecodeError((path, "expected JSON object"))
  }
  guard fields.get("id") is Some(Number(id, ..)) else {
    raise JsonDecodeError((path, "missing field 'id'"))
  }
  guard fields.get("name") is Some(String(name)) else {
    raise JsonDecodeError((path, "missing field 'name'"))
  }
  guard fields.get("owner_id") is Some(Number(owner_id, ..)) else {
    raise JsonDecodeError((path, "missing field 'owner_id'"))
  }
  { id: id.to_int(), name, owner_id: owner_id.to_int64() }
}

///|
pub(all) struct CreatedGroupResponse {
  id : Int
}

///|
pub impl ToJson for CreatedGroupResponse with fn to_json(self) {
  { "id": self.id }
}

///|
pub impl @json.FromJson for CreatedGroupResponse with fn from_json(json, path) {
  guard json is Object(fields) else {
    raise JsonDecodeError((path, "expected JSON object"))
  }
  guard fields.get("id") is Some(Number(id, ..)) else {
    raise JsonDecodeError((path, "missing field 'id'"))
  }
  { id: id.to_int() }
}

///|
pub(all) struct GroupMember {
  github_id : Int64
  login : String
  avatar_url : String
}

///|
pub impl ToJson for GroupMember with fn to_json(self) {
  {
    "github_id": Json::number(
      self.github_id.to_double(),
      repr=self.github_id.to_string(),
    ),
    "login": self.login,
    "avatar_url": self.avatar_url,
  }
}

///|
pub impl @json.FromJson for GroupMember with fn from_json(json, path) {
  guard json is Object(fields) else {
    raise JsonDecodeError((path, "expected JSON object"))
  }
  guard fields.get("github_id") is Some(Number(github_id, ..)) else {
    raise JsonDecodeError((path, "missing field 'github_id'"))
  }
  guard fields.get("login") is Some(String(login)) else {
    raise JsonDecodeError((path, "missing field 'login'"))
  }
  guard fields.get("avatar_url") is Some(String(avatar_url)) else {
    raise JsonDecodeError((path, "missing field 'avatar_url'"))
  }
  { github_id: github_id.to_int64(), login, avatar_url }
}

// -- API request types -------------------------------------------------------

///|
pub(all) struct GrantAccessRequest {
  group_id : Int
}

///|
pub impl ToJson for GrantAccessRequest with fn to_json(self) {
  { "group_id": self.group_id }
}

///|
pub impl @json.FromJson for GrantAccessRequest with fn from_json(json, path) {
  guard json is Object(fields) else {
    raise JsonDecodeError((path, "expected JSON object"))
  }
  guard fields.get("group_id") is Some(Number(group_id, ..)) else {
    raise JsonDecodeError((path, "missing field 'group_id'"))
  }
  { group_id: group_id.to_int() }
}

///|
pub(all) struct CreateGroupRequest {
  name : String
}

///|
pub impl ToJson for CreateGroupRequest with fn to_json(self) {
  { "name": self.name }
}

///|
pub impl @json.FromJson for CreateGroupRequest with fn from_json(json, path) {
  guard json is Object(fields) else {
    raise JsonDecodeError((path, "expected JSON object"))
  }
  guard fields.get("name") is Some(String(name)) else {
    raise JsonDecodeError((path, "missing field 'name'"))
  }
  { name, }
}

///|
pub(all) struct TransferGroupRequest {
  new_owner_id : Int64
}

///|
pub impl @json.FromJson for TransferGroupRequest with fn from_json(json, path) {
  guard json is Object(fields) else {
    raise JsonDecodeError((path, "expected JSON object"))
  }
  guard fields.get("new_owner_id") is Some(Number(n, ..)) else {
    raise JsonDecodeError((path, "missing field 'new_owner_id'"))
  }
  { new_owner_id: n.to_int64() }
}

///|
pub(all) struct AddMemberRequest {
  login : String
}

///|
pub impl ToJson for AddMemberRequest with fn to_json(self) {
  { "login": self.login }
}

///|
pub impl @json.FromJson for AddMemberRequest with fn from_json(json, path) {
  guard json is Object(fields) else {
    raise JsonDecodeError((path, "expected JSON object"))
  }
  guard fields.get("login") is Some(String(login)) else {
    raise JsonDecodeError((path, "missing field 'login'"))
  }
  { login, }
}

// -- Device-code approval types ----------------------------------------------

///|
/// Response body of `GET /device/pending?code=`. Returned to the
/// SPA approval page so it can render the hostname the agent reported.
pub(all) struct DevicePendingResponse {
  hostname : String
}

///|
pub impl ToJson for DevicePendingResponse with fn to_json(self) {
  { "hostname": self.hostname }
}

///|
pub impl @json.FromJson for DevicePendingResponse with fn from_json(json, path) {
  guard json is Object(fields) else {
    raise JsonDecodeError((path, "expected JSON object"))
  }
  guard fields.get("hostname") is Some(String(hostname)) else {
    raise JsonDecodeError((path, "missing field 'hostname'"))
  }
  { hostname, }
}

///|
/// Request body of `POST /device/approve`. Sent by the SPA when the user
/// clicks Approve on the pairing page.
pub(all) struct DeviceApproveRequest {
  code : String
}

///|
pub impl ToJson for DeviceApproveRequest with fn to_json(self) {
  { "code": self.code }
}

///|
pub impl @json.FromJson for DeviceApproveRequest with fn from_json(json, path) {
  guard json is Object(fields) else {
    raise JsonDecodeError((path, "expected JSON object"))
  }
  guard fields.get("code") is Some(String(code)) else {
    raise JsonDecodeError((path, "missing field 'code'"))
  }
  { code, }
}

///|
/// Request body of `POST /device/deny`. Sent by the SPA when the user clicks
/// Deny on the pairing page.
pub(all) struct DeviceDenyRequest {
  code : String
}

///|
pub impl ToJson for DeviceDenyRequest with fn to_json(self) {
  { "code": self.code }
}

///|
pub impl @json.FromJson for DeviceDenyRequest with fn from_json(json, path) {
  guard json is Object(fields) else {
    raise JsonDecodeError((path, "expected JSON object"))
  }
  guard fields.get("code") is Some(String(code)) else {
    raise JsonDecodeError((path, "missing field 'code'"))
  }
  { code, }
}