// rate_limits.mbt — upstream `src/rate-limits.ts` mirror: the rate-limit
// snapshot shared by `_lody/rate_limits/get` (request + result) and
// `_lody/rate_limits/update` (notification payload).
//
// Time units: `*EpochSeconds` are Unix epoch seconds, `windowDurationSeconds`
// is seconds. `usedPercent` is a normalized 0..100 inclusive percentage;
// out-of-range values are decode errors, never clamped.

///|
/// `RateLimitsGetRequest` — `_lody/rate_limits/get` params. Not session
/// bound: every field is an optional filter.
pub(all) struct RateLimitsGetRequest {
  session_id : String?
  account_id : String?
  model_id : String?
} derive(Eq, Debug)

///|
pub fn RateLimitsGetRequest::to_json(self : RateLimitsGetRequest) -> Json {
  let pairs : Array[(String, Json)] = []
  match self.session_id {
    Some(v) => pairs.push(("sessionId", Json::string(v)))
    None => ()
  }
  match self.account_id {
    Some(v) => pairs.push(("accountId", Json::string(v)))
    None => ()
  }
  match self.model_id {
    Some(v) => pairs.push(("modelId", Json::string(v)))
    None => ()
  }
  Json::object(Map::from_array(pairs))
}

///|
pub fn RateLimitsGetRequest::from_json(
  raw : Json,
) -> Result[RateLimitsGetRequest, String] {
  try {
    let ctx = "lody.rateLimits.get.request"
    let fields = object_fields(raw, ctx)
    Ok(RateLimitsGetRequest::{
      session_id: opt_string(fields, ctx, "sessionId"),
      account_id: opt_string(fields, ctx, "accountId"),
      model_id: opt_string(fields, ctx, "modelId"),
    })
  } catch {
    DecodeError::Msg(message) => Err(message)
  }
}

///|
/// `RateLimitScope` — which provider/account/model a limit applies to.
pub(all) struct RateLimitScope {
  provider_id : String
  account_id : String?
  model_id : String?
} derive(Eq, Debug)

///|
pub fn RateLimitScope::to_json(self : RateLimitScope) -> Json {
  let pairs : Array[(String, Json)] = [
    ("providerId", Json::string(self.provider_id)),
  ]
  match self.account_id {
    Some(v) => pairs.push(("accountId", Json::string(v)))
    None => ()
  }
  match self.model_id {
    Some(v) => pairs.push(("modelId", Json::string(v)))
    None => ()
  }
  Json::object(Map::from_array(pairs))
}

///|
pub fn RateLimitScope::from_json(raw : Json) -> Result[RateLimitScope, String] {
  try {
    let ctx = "lody.rateLimits.scope"
    let fields = object_fields(raw, ctx)
    Ok(RateLimitScope::{
      provider_id: req_string(fields, ctx, "providerId"),
      account_id: opt_string(fields, ctx, "accountId"),
      model_id: opt_string(fields, ctx, "modelId"),
    })
  } catch {
    DecodeError::Msg(message) => Err(message)
  }
}

///|
/// `RateLimitWindow` — one usage window; `usedPercent` is 0..100 inclusive.
pub(all) struct RateLimitWindow {
  used_percent : Double
  window_duration_seconds : Int64?
  resets_at_epoch_seconds : Int64?
} derive(Eq, Debug)

///|
pub fn RateLimitWindow::to_json(self : RateLimitWindow) -> Json {
  let pairs : Array[(String, Json)] = [
    ("usedPercent", Json::number(self.used_percent)),
  ]
  match self.window_duration_seconds {
    Some(v) =>
      pairs.push(("windowDurationSeconds", Json::number(v.to_double())))
    None => ()
  }
  match self.resets_at_epoch_seconds {
    Some(v) => pairs.push(("resetsAtEpochSeconds", Json::number(v.to_double())))
    None => ()
  }
  Json::object(Map::from_array(pairs))
}

///|
pub fn RateLimitWindow::from_json(
  raw : Json,
) -> Result[RateLimitWindow, String] {
  try {
    let ctx = "lody.rateLimits.window"
    let fields = object_fields(raw, ctx)
    let used_percent = req_double(fields, ctx, "usedPercent")
    guard 0.0 <= used_percent && used_percent <= 100.0 else {
      raise DecodeError::Msg(
        "\{ctx}: field \"usedPercent\" must be within 0..100, got \{used_percent}",
      )
    }
    Ok(RateLimitWindow::{
      used_percent,
      window_duration_seconds: opt_int64(fields, ctx, "windowDurationSeconds"),
      resets_at_epoch_seconds: opt_int64(fields, ctx, "resetsAtEpochSeconds"),
    })
  } catch {
    DecodeError::Msg(message) => Err(message)
  }
}

///|
/// `RateLimitWallet` — credit-balance state; money in cents, ISO currency
/// string (upstream constrains the format no further).
pub(all) struct RateLimitWallet {
  balance_cents : Int64
  total_cents : Int64
  monthly_charge_limit_enabled : Bool
  monthly_charge_limit_cents : Int64
  monthly_used_cents : Int64
  currency : String
} derive(Eq, Debug)

///|
pub fn RateLimitWallet::to_json(self : RateLimitWallet) -> Json {
  Json::object(
    Map::from_array([
      ("balanceCents", Json::number(self.balance_cents.to_double())),
      ("totalCents", Json::number(self.total_cents.to_double())),
      (
        "monthlyChargeLimitEnabled",
        Json::boolean(self.monthly_charge_limit_enabled),
      ),
      (
        "monthlyChargeLimitCents",
        Json::number(self.monthly_charge_limit_cents.to_double()),
      ),
      ("monthlyUsedCents", Json::number(self.monthly_used_cents.to_double())),
      ("currency", Json::string(self.currency)),
    ]),
  )
}

///|
pub fn RateLimitWallet::from_json(
  raw : Json,
) -> Result[RateLimitWallet, String] {
  try {
    let ctx = "lody.rateLimits.wallet"
    let fields = object_fields(raw, ctx)
    Ok(RateLimitWallet::{
      balance_cents: req_int64(fields, ctx, "balanceCents"),
      total_cents: req_int64(fields, ctx, "totalCents"),
      monthly_charge_limit_enabled: req_bool(
        fields, ctx, "monthlyChargeLimitEnabled",
      ),
      monthly_charge_limit_cents: req_int64(
        fields, ctx, "monthlyChargeLimitCents",
      ),
      monthly_used_cents: req_int64(fields, ctx, "monthlyUsedCents"),
      currency: req_string(fields, ctx, "currency"),
    })
  } catch {
    DecodeError::Msg(message) => Err(message)
  }
}

///|
/// `RateLimit` — one limit with its windows and optional wallet. `limitName`,
/// `planName`, and `wallet` are nullable-but-optional upstream without a
/// distinct absent meaning, so null and absent both decode to None.
pub(all) struct RateLimit {
  limit_id : String
  scope : RateLimitScope
  limit_name : String?
  plan_name : String?
  windows : Array[RateLimitWindow]
  wallet : RateLimitWallet?
} derive(Eq, Debug)

///|
pub fn RateLimit::to_json(self : RateLimit) -> Json {
  let pairs : Array[(String, Json)] = [
    ("limitId", Json::string(self.limit_id)),
    ("scope", self.scope.to_json()),
  ]
  match self.limit_name {
    Some(v) => pairs.push(("limitName", Json::string(v)))
    None => ()
  }
  match self.plan_name {
    Some(v) => pairs.push(("planName", Json::string(v)))
    None => ()
  }
  pairs.push(
    ("windows", Json::array(self.windows.map(fn(window) { window.to_json() }))),
  )
  match self.wallet {
    Some(v) => pairs.push(("wallet", v.to_json()))
    None => ()
  }
  Json::object(Map::from_array(pairs))
}

///|
pub fn RateLimit::from_json(raw : Json) -> Result[RateLimit, String] {
  try {
    let ctx = "lody.rateLimits.limit"
    let fields = object_fields(raw, ctx)
    let windows : Array[RateLimitWindow] = req_array(fields, ctx, "windows").map(item => {
        match RateLimitWindow::from_json(item) {
          Ok(window) => window
          Err(message) => raise DecodeError::Msg(message)
        }
      },
    )
    Ok(RateLimit::{
      limit_id: req_string(fields, ctx, "limitId"),
      scope: req_sub(fields, ctx, "scope", RateLimitScope::from_json),
      limit_name: opt_string(fields, ctx, "limitName"),
      plan_name: opt_string(fields, ctx, "planName"),
      windows,
      wallet: opt_sub(fields, ctx, "wallet", RateLimitWallet::from_json),
    })
  } catch {
    DecodeError::Msg(message) => Err(message)
  }
}

///|
/// `RateLimitsSnapshot` — the shape of `_lody/rate_limits/get`'s result and
/// of the `_lody/rate_limits/update` notification payload.
pub(all) struct RateLimitsSnapshot {
  rate_limits : Array[RateLimit]
  fetched_at_epoch_seconds : Int64?
} derive(Eq, Debug)

///|
pub fn RateLimitsSnapshot::to_json(self : RateLimitsSnapshot) -> Json {
  let pairs : Array[(String, Json)] = [
    (
      "rateLimits",
      Json::array(self.rate_limits.map(fn(limit) { limit.to_json() })),
    ),
  ]
  match self.fetched_at_epoch_seconds {
    Some(v) =>
      pairs.push(("fetchedAtEpochSeconds", Json::number(v.to_double())))
    None => ()
  }
  Json::object(Map::from_array(pairs))
}

///|
pub fn RateLimitsSnapshot::from_json(
  raw : Json,
) -> Result[RateLimitsSnapshot, String] {
  try {
    let ctx = "lody.rateLimits.snapshot"
    let fields = object_fields(raw, ctx)
    let rate_limits : Array[RateLimit] = req_array(fields, ctx, "rateLimits").map(item => {
        match RateLimit::from_json(item) {
          Ok(limit) => limit
          Err(message) => raise DecodeError::Msg(message)
        }
      },
    )
    Ok(RateLimitsSnapshot::{
      rate_limits,
      fetched_at_epoch_seconds: opt_int64(fields, ctx, "fetchedAtEpochSeconds"),
    })
  } catch {
    DecodeError::Msg(message) => Err(message)
  }
}