///|
/// `/model quick-pick` support.
///
/// Quick-pick aggregates quota readings across every provider that
/// contributed slots (slot presence is the login signal), through
/// host-injected sources first, then the devkit registry. Readings are
/// facts, never estimated: Err and timeout simply omit the provider.

///|
/// Reserved `/model` slot-argument keyword; wins over any real slot id.
let quick_pick_keyword : String = "quick-pick"

///|
/// Per-source read deadline; tasks run concurrently so command latency is
/// bounded by one timeout.
let quick_pick_source_timeout_ms : Int = 2500

///|
/// Distinct provider ids behind the current slots, in first-seen slot order.
fn RouterModelPort::distinct_provider_ids(
  self : RouterModelPort,
) -> Array[String] {
  let provider_ids : Array[String] = []
  let seen : Map[String, Unit] = Map::from_array([])
  for slot_id in self.slot_order {
    match self.slots.get(slot_id) {
      Some(slot) =>
        if !seen.contains(slot.provider_id) {
          seen[slot.provider_id] = ()
          provider_ids.push(slot.provider_id)
        }
      None => ()
    }
  }
  provider_ids
}

///|
/// Pull quota readings for every provider that contributed a slot, in
/// first-seen slot order. Each source is read concurrently under its own
/// timeout; only successful reads land in the returned map, so a failing or
/// slow provider is omitted rather than estimated.
async fn RouterModelPort::collect_provider_quota(
  self : RouterModelPort,
) -> Map[String, Array[@devkit.QuotaReading]] noraise {
  let provider_ids = self.distinct_provider_ids()
  let sources : Array[(String, &@devkit.QuotaSource)] = []
  for id in provider_ids {
    match self.extra_quota_sources.get(id) {
      Some(source) => sources.push((id, source))
      None =>
        match @devkit.quota_registry_lookup(id) {
          Some(source) => sources.push((id, source))
          None => ()
        }
    }
  }
  let readings : Map[String, Array[@devkit.QuotaReading]] = Map::from_array([])
  @async.with_task_group(group => {
    for (id, source) in sources {
      ignore(
        group.spawn(
          () => {
            let result = @async.with_timeout_opt(quick_pick_source_timeout_ms, () => {
              source.read() catch {
                _ => Err("quota pull failed")
              }
            })
            match result {
              Some(Ok(list)) => readings[id] = list
              _ => ()
            }
          },
          allow_failure=true,
        ),
      )
    }
  }) catch {
    _ => ()
  }
  readings
}

///|
/// Encode one quota reading verbatim: provider-stated fields only, absent
/// optionals as JSON null, no invented values.
fn quota_reading_json(reading : @devkit.QuotaReading) -> Json {
  Json::object(
    Map::from_array([
      ("window", Json::string(reading.window)),
      (
        "used_percent",
        match reading.used_percent {
          Some(value) => Json::number(value)
          None => Json::null()
        },
      ),
      (
        "amount",
        match reading.amount {
          Some((value, currency)) =>
            Json::object(
              Map::from_array([
                ("value", Json::string(value)),
                ("currency", Json::string(currency)),
              ]),
            )
          None => Json::null()
        },
      ),
      (
        "available",
        match reading.available {
          Some(value) => Json::boolean(value)
          None => Json::null()
        },
      ),
      (
        "reset_at_ms",
        match reading.reset_at_ms {
          Some(value) => Json::number(value.to_double())
          None => Json::null()
        },
      ),
      ("fetched_at_ms", Json::number(reading.fetched_at_ms.to_double())),
    ]),
  )
}