// -- RFC 8628 Device Authorization Grant --------------------------------------
//
// Wire types shared between the relay (which hosts the endpoints) and the
// server (which consumes them during first-run pairing). See
// `docs/plans/2026-04-10-device-flow-typed-proto-design.md` for context.

// -- DeviceAuthorizationRequest -----------------------------------------------

///|
pub(all) struct DeviceAuthorizationRequest {
  hostname : String
}

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

///|
pub impl @json.FromJson for DeviceAuthorizationRequest 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, }
}

// -- DeviceAuthorizationResponse ----------------------------------------------

///|
pub(all) struct DeviceAuthorizationResponse {
  device_code : String
  user_code : String
  verification_uri : String
  verification_uri_complete : String?
  expires_in : Int
  interval : Int
}

///|
pub impl ToJson for DeviceAuthorizationResponse with fn to_json(self) {
  // Emit `verification_uri_complete` only when present so the wire format is
  // byte-identical to the pre-refactor hand-rolled relay response when the
  // caller passes None.
  match self.verification_uri_complete {
    Some(complete) =>
      {
        "device_code": self.device_code,
        "user_code": self.user_code,
        "verification_uri": self.verification_uri,
        "verification_uri_complete": complete,
        "expires_in": self.expires_in,
        "interval": self.interval,
      }
    None =>
      {
        "device_code": self.device_code,
        "user_code": self.user_code,
        "verification_uri": self.verification_uri,
        "expires_in": self.expires_in,
        "interval": self.interval,
      }
  }
}

///|
pub impl @json.FromJson for DeviceAuthorizationResponse with fn from_json(
  json,
  path,
) {
  guard json is Object(fields) else {
    raise JsonDecodeError((path, "expected JSON object"))
  }
  guard fields.get("device_code") is Some(String(device_code)) else {
    raise JsonDecodeError((path, "missing field 'device_code'"))
  }
  guard fields.get("user_code") is Some(String(user_code)) else {
    raise JsonDecodeError((path, "missing field 'user_code'"))
  }
  guard fields.get("verification_uri") is Some(String(verification_uri)) else {
    raise JsonDecodeError((path, "missing field 'verification_uri'"))
  }
  let verification_uri_complete : String? = match
    fields.get("verification_uri_complete") {
    Some(String(s)) => Some(s)
    _ => None
  }
  guard fields.get("expires_in") is Some(Number(expires_in, ..)) else {
    raise JsonDecodeError((path, "missing field 'expires_in'"))
  }
  guard fields.get("interval") is Some(Number(interval, ..)) else {
    raise JsonDecodeError((path, "missing field 'interval'"))
  }
  {
    device_code,
    user_code,
    verification_uri,
    verification_uri_complete,
    expires_in: expires_in.to_int(),
    interval: interval.to_int(),
  }
}

// -- DeviceTokenRequest -------------------------------------------------------

///|
pub(all) struct DeviceTokenRequest {
  grant_type : String
  device_code : String
}

///|
pub impl ToJson for DeviceTokenRequest with fn to_json(self) {
  { "grant_type": self.grant_type, "device_code": self.device_code }
}

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

// -- DeviceTokenError ---------------------------------------------------------

///|
/// RFC 8628 §3.5 token-endpoint error codes, plus `Unknown(String)` as a
/// forward-compatibility hatch for codes added by a future relay against
/// an older server. Wire format is a bare JSON string (see
/// `DeviceTokenResponse::Error`).
pub(all) enum DeviceTokenError {
  AuthorizationPending
  SlowDown
  ExpiredToken
  AccessDenied
  InvalidRequest
  UnsupportedGrantType
  Unknown(String)
}

///|
pub impl ToJson for DeviceTokenError with fn to_json(self) {
  let s = match self {
    AuthorizationPending => "authorization_pending"
    SlowDown => "slow_down"
    ExpiredToken => "expired_token"
    AccessDenied => "access_denied"
    InvalidRequest => "invalid_request"
    UnsupportedGrantType => "unsupported_grant_type"
    Unknown(s) => s
  }
  Json::string(s)
}

///|
pub impl @json.FromJson for DeviceTokenError with fn from_json(json, path) {
  guard json is String(s) else {
    raise JsonDecodeError((path, "expected JSON string for error code"))
  }
  match s {
    "authorization_pending" => AuthorizationPending
    "slow_down" => SlowDown
    "expired_token" => ExpiredToken
    "access_denied" => AccessDenied
    "invalid_request" => InvalidRequest
    "unsupported_grant_type" => UnsupportedGrantType
    other => Unknown(other)
  }
}

// -- DeviceTokenResponse ------------------------------------------------------

///|
/// RFC 8628 §3.5 token-endpoint response. Discriminated on the wire by
/// which field is present: `access_token` → Success, `error` → Error.
pub(all) enum DeviceTokenResponse {
  Success(access_token~ : String, token_type~ : String)
  Error(error~ : DeviceTokenError)
}

///|
pub impl ToJson for DeviceTokenResponse with fn to_json(self) {
  match self {
    Success(access_token~, token_type~) =>
      { "access_token": access_token, "token_type": token_type }
    Error(error~) => { "error": error.to_json() }
  }
}

///|
pub impl @json.FromJson for DeviceTokenResponse with fn from_json(json, path) {
  guard json is Object(fields) else {
    raise JsonDecodeError((path, "expected JSON object"))
  }
  // Presence of `access_token` wins over `error` if both are somehow set —
  // matches the server's previous ordering in main.mbt (checked access_token
  // first, error second).
  match fields.get("access_token") {
    Some(String(access_token)) => {
      let token_type = match fields.get("token_type") {
        Some(String(s)) => s
        _ => "bearer"
      }
      return Success(access_token~, token_type~)
    }
    _ => ()
  }
  match fields.get("error") {
    Some(error_json) => {
      let error : DeviceTokenError = @json.from_json(error_json)
      return Error(error~)
    }
    _ => ()
  }
  raise JsonDecodeError(
    (path, "device token response missing both 'access_token' and 'error'"),
  )
}