///|
priv enum CredentialStatus {
  Valid
  Expired
  NotFound
  ParseError
} derive(Eq)

///|
fn CredentialStatus::to_string(self : CredentialStatus) -> String {
  match self {
    Valid => "valid"
    Expired => "expired"
    NotFound => "not_found"
    ParseError => "parse_error"
  }
}

///|
priv struct UsageData {
  plan_name : String?
  extra : String?
  is_valid : Bool?
  invalid_message : String?
  total : Double?
  used : Double?
  remaining : Double?
  unit : String?
}

///|
fn UsageData::new(
  plan_name? : String? = None,
  extra? : String? = None,
  is_valid? : Bool? = None,
  invalid_message? : String? = None,
  total? : Double? = None,
  used? : Double? = None,
  remaining? : Double? = None,
  unit? : String? = None,
) -> UsageData {
  { plan_name, extra, is_valid, invalid_message, total, used, remaining, unit }
}

///|
fn UsageData::to_json(self : UsageData) -> Json {
  let obj : Map[String, Json] = Map([], capacity=0)
  match self.plan_name {
    Some(value) => obj.set("planName", Json::string(value))
    None => ()
  }
  match self.extra {
    Some(value) => obj.set("extra", Json::string(value))
    None => ()
  }
  match self.is_valid {
    Some(value) => obj.set("isValid", Json::boolean(value))
    None => ()
  }
  match self.invalid_message {
    Some(value) => obj.set("invalidMessage", Json::string(value))
    None => ()
  }
  match self.total {
    Some(value) => obj.set("total", Json::number(value))
    None => ()
  }
  match self.used {
    Some(value) => obj.set("used", Json::number(value))
    None => ()
  }
  match self.remaining {
    Some(value) => obj.set("remaining", Json::number(value))
    None => ()
  }
  match self.unit {
    Some(value) => obj.set("unit", Json::string(value))
    None => ()
  }
  Json::object(obj)
}

///|
fn usage_data_from_json(json : Json) -> UsageData? {
  guard json is Object(obj) else { return None }
  Some(
    UsageData::new(
      plan_name=usage_cache_json_string(obj, "planName"),
      extra=usage_cache_json_string(obj, "extra"),
      is_valid=usage_cache_json_bool(obj, "isValid"),
      invalid_message=usage_cache_json_string(obj, "invalidMessage"),
      total=usage_cache_json_number(obj, "total"),
      used=usage_cache_json_number(obj, "used"),
      remaining=usage_cache_json_number(obj, "remaining"),
      unit=usage_cache_json_string(obj, "unit"),
    ),
  )
}

///|
fn usage_data_json_has_moon_field(obj : Map[String, Json]) -> Bool {
  [
    "planName", "extra", "isValid", "invalidMessage", "total", "used", "remaining",
    "unit",
  ].any(key => obj.contains(key))
}

///|
fn usage_script_output_result_from_json(json : Json) -> UsageResult? {
  match json {
    Array(items) => {
      let rows : Array[UsageData] = []
      for item in items {
        guard item is Object(obj) else { return None }
        if !usage_data_json_has_moon_field(obj) {
          return None
        }
        match usage_data_from_json(item) {
          Some(row) => rows.push(row)
          None => return None
        }
      }
      Some(UsageResult::new(true, data=Some(rows)))
    }
    Object(obj) => {
      if !usage_data_json_has_moon_field(obj) {
        return None
      }
      match usage_data_from_json(json) {
        Some(row) => {
          let rows : Array[UsageData] = []
          rows.push(row)
          Some(UsageResult::new(true, data=Some(rows)))
        }
        None => None
      }
    }
    _ => None
  }
}

///|
fn usage_result_or_script_output_from_json(json : Json) -> UsageResult? {
  match usage_result_from_json(json) {
    Some(result) => Some(result)
    None => usage_script_output_result_from_json(json)
  }
}

///|
priv struct UsageResult {
  success : Bool
  data : Array[UsageData]?
  error : String?
}

///|
fn UsageResult::new(
  success : Bool,
  data? : Array[UsageData]? = None,
  error? : String? = None,
) -> UsageResult {
  { success, data, error }
}

///|
fn UsageResult::to_json(self : UsageResult) -> Json {
  let obj : Map[String, Json] = { "success": Json::boolean(self.success) }
  match self.data {
    Some(rows) =>
      obj.set("data", Json::array(rows.map(fn(row) { row.to_json() })))
    None => ()
  }
  match self.error {
    Some(error) => obj.set("error", Json::string(error))
    None => ()
  }
  Json::object(obj)
}

///|
fn usage_result_from_json(json : Json) -> UsageResult? {
  guard json is Object(obj) else { return None }
  guard usage_cache_json_bool(obj, "success") is Some(success) else {
    return None
  }
  let rows : Array[UsageData] = []
  match obj.get("data") {
    Some(Array(items)) =>
      for item in items {
        match usage_data_from_json(item) {
          Some(row) => rows.push(row)
          None => return None
        }
      }
    Some(Null) | None => ()
    _ => return None
  }
  Some(
    UsageResult::new(
      success,
      data=if rows.is_empty() { None } else { Some(rows) },
      error=usage_cache_json_string(obj, "error"),
    ),
  )
}

///|
priv struct SubscriptionQuota {
  tool : String
  credential_status : CredentialStatus
  credential_message : String?
  success : Bool
  tiers : Array[Json]
  extra_usage : Json?
  error : String?
  queried_at : Int64?
}

///|
fn SubscriptionQuota::new(
  tool : String,
  credential_status? : CredentialStatus = Valid,
  credential_message? : String? = None,
  success? : Bool = true,
  tiers? : Array[Json] = [],
  extra_usage? : Json? = None,
  error? : String? = None,
  queried_at? : Int64? = None,
) -> SubscriptionQuota {
  {
    tool,
    credential_status,
    credential_message,
    success,
    tiers,
    extra_usage,
    error,
    queried_at,
  }
}

///|
fn SubscriptionQuota::not_found(tool : String) -> SubscriptionQuota {
  SubscriptionQuota::new(tool, credential_status=NotFound, success=false)
}

///|
fn SubscriptionQuota::error(
  tool : String,
  status : CredentialStatus,
  message : String,
) -> SubscriptionQuota {
  SubscriptionQuota::new(
    tool,
    credential_status=status,
    credential_message=Some(message),
    success=false,
  )
}

///|
fn SubscriptionQuota::to_json(self : SubscriptionQuota) -> Json {
  let obj : Map[String, Json] = {
    "tool": Json::string(self.tool),
    "credentialStatus": Json::string(self.credential_status.to_string()),
    "success": Json::boolean(self.success),
    "tiers": Json::array(self.tiers),
  }
  match self.credential_message {
    Some(value) => obj.set("credentialMessage", Json::string(value))
    None => ()
  }
  match self.extra_usage {
    Some(value) => obj.set("extraUsage", value)
    None => ()
  }
  match self.error {
    Some(value) => obj.set("error", Json::string(value))
    None => ()
  }
  match self.queried_at {
    Some(value) => obj.set("queriedAt", Json::number(value.to_double()))
    None => ()
  }
  Json::object(obj)
}

///|
priv struct UsageCache {
  subscription : Map[String, SubscriptionQuota]
  script : Map[String, UsageResult]
}

///|
const MOONGATE_USAGE_KEEP_LAST_GOOD_MS : Int64 = 10L * 60L * 1000L

///|
fn UsageCache::new() -> UsageCache {
  { subscription: Map([], capacity=0), script: Map([], capacity=0) }
}

///|
fn UsageCache::put_subscription(
  self : UsageCache,
  app_type : String,
  quota : SubscriptionQuota,
) -> Unit {
  self.subscription.set(app_type, quota)
}

///|
fn SubscriptionQuota::is_transient_failure(self : SubscriptionQuota) -> Bool {
  if self.success || self.credential_status != Valid {
    return false
  }
  let message = self.error.unwrap_or("").to_lower()
  if message.contains("network error") ||
    message.contains("failed to read response") ||
    message.contains("request failed") ||
    message.contains("timed out") ||
    message.contains("timeout") ||
    message.contains("http 429") {
    return true
  }
  for status in 500..<=599 {
    if message.contains("http \{status}") {
      return true
    }
  }
  false
}

///|
fn UsageCache::resolve_subscription(
  self : UsageCache,
  key : String,
  fresh : SubscriptionQuota,
  now_ms? : Int64 = moon_quota_now_millis(),
) -> SubscriptionQuota {
  if fresh.success || !fresh.is_transient_failure() {
    self.put_subscription(key, fresh)
    return fresh
  }
  match self.get_subscription(key) {
    Some(cached) if cached.success =>
      match cached.queried_at {
        Some(queried_at) if now_ms >= queried_at &&
          now_ms - queried_at < MOONGATE_USAGE_KEEP_LAST_GOOD_MS => cached
        _ => fresh
      }
    _ => fresh
  }
}

///|
fn UsageCache::put_script(
  self : UsageCache,
  app_type : String,
  provider_id : String,
  result : UsageResult,
) -> Unit {
  self.script.set(usage_cache_script_key(app_type, provider_id), result)
}

///|
fn UsageCache::get_subscription(
  self : UsageCache,
  app_type : String,
) -> SubscriptionQuota? {
  self.subscription.get(app_type)
}

///|
fn UsageCache::get_script(
  self : UsageCache,
  app_type : String,
  provider_id : String,
) -> UsageResult? {
  self.script.get(usage_cache_script_key(app_type, provider_id))
}

///|
fn[R] UsageCache::with_subscription(
  self : UsageCache,
  app_type : String,
  f : (SubscriptionQuota) -> R,
) -> R? {
  match self.get_subscription(app_type) {
    Some(quota) => Some(f(quota))
    None => None
  }
}

///|
fn[R] UsageCache::with_script(
  self : UsageCache,
  app_type : String,
  provider_id : String,
  f : (UsageResult) -> R,
) -> R? {
  match self.get_script(app_type, provider_id) {
    Some(result) => Some(f(result))
    None => None
  }
}

///|
fn UsageCache::invalidate_script(
  self : UsageCache,
  app_type : String,
  provider_id : String,
) -> Unit {
  self.script.remove(usage_cache_script_key(app_type, provider_id))
}

///|
fn UsageCache::invalidate_subscription(
  self : UsageCache,
  app_type : String,
) -> Unit {
  self.subscription.remove(app_type)
}

///|
fn usage_cache_script_key(app_type : String, provider_id : String) -> String {
  "\{app_type}\t\{provider_id}"
}

///|
fn usage_cache_json_string(obj : Map[String, Json], key : String) -> String? {
  match obj.get(key) {
    Some(String(value)) => Some(value)
    _ => None
  }
}

///|
fn usage_cache_json_bool(obj : Map[String, Json], key : String) -> Bool? {
  match obj.get(key) {
    Some(True) => Some(true)
    Some(False) => Some(false)
    _ => None
  }
}

///|
fn usage_cache_json_number(obj : Map[String, Json], key : String) -> Double? {
  match obj.get(key) {
    Some(Number(value, ..)) => Some(value)
    _ => None
  }
}