///|
pub(all) struct QuotaKey {
  scope : String
  subject : String
  resource : String
} derive(Debug, Eq)

///|
pub(all) struct Decision {
  allowed : Bool
  reason : String
  cost : Int
  remaining : Int
  retry_after_ms : Int64
  reset_after_ms : Int64
  trace : Array[String]
} derive(Debug, Eq)

///|
pub(all) struct LimitSpec {
  name : String
  capacity : Int
  refill : Int
  window_ms : Int64
  burst : Int
} derive(Debug, Eq)

///|
pub(all) struct BucketState {
  tokens : Int
  last_refill_ms : Int64
} derive(Debug, Eq)

///|
pub(all) struct WindowCounter {
  window_start_ms : Int64
  used : Int
} derive(Debug, Eq)

///|
pub(all) struct GcraState {
  theoretical_arrival_ms : Int64
} derive(Debug, Eq)

///|
pub(all) struct EvaluationInput {
  key : QuotaKey
  now_ms : Int64
  cost : Int
} derive(Debug, Eq)

///|
pub(all) struct QuotaCharge {
  key : QuotaKey
  limit_name : String
  cost : Int
} derive(Debug, Eq)

///|
pub(all) struct AtomicQuotaReport {
  allowed : Bool
  committed : Bool
  failed_index : Int
  decisions : Array[Decision]
} derive(Debug)

///|
pub(all) struct BatchReport {
  total : Int
  allowed : Int
  denied : Int
  decisions : Array[Decision]
} derive(Debug)

///|
pub(all) struct ValidationIssue {
  code : String
  message : String
} derive(Debug, Eq)

///|
pub(all) struct QuotaStats {
  limits : Int
  token_buckets : Int
  windows : Int
  gcra_flows : Int
  subjects : Int
} derive(Debug, Eq)

///|
/// A deterministic in-memory snapshot for application-managed persistence.
/// The library deliberately does not perform I/O; adapters may serialize this
/// value to a database, distributed cache, or file without changing policy.
pub(all) struct QuotaSnapshot {
  limits : Array[LimitSpec]
  buckets : Array[TrackedBucket]
  windows : Array[TrackedWindow]
  gcra_flows : Array[TrackedGcra]
  links : Array[QuotaLink]
  subjects : Array[SubjectBudget]
} derive(Debug)

///|
pub(all) struct TrackedBucket {
  key : QuotaKey
  limit_name : String
  state : BucketState
} derive(Debug, Eq)

///|
pub(all) struct TrackedWindow {
  key : QuotaKey
  limit_name : String
  state : WindowCounter
} derive(Debug, Eq)

///|
pub(all) struct TrackedGcra {
  key : QuotaKey
  limit_name : String
  state : GcraState
} derive(Debug, Eq)

///|
pub(all) struct QuotaLink {
  child : QuotaKey
  parent : QuotaKey
  limit_name : String
  ratio : Int
} derive(Debug, Eq)

///|
pub(all) struct SubjectBudget {
  subject : String
  weight : Int
  used : Int
} derive(Debug, Eq)

///|
pub(all) struct FairShareItem {
  subject : String
  weight : Int
  used : Int
  score : Int
} derive(Debug, Eq)

///|
pub(all) struct QuotaEngine {
  limits : Array[LimitSpec]
  buckets : Array[TrackedBucket]
  windows : Array[TrackedWindow]
  gcra_flows : Array[TrackedGcra]
  links : Array[QuotaLink]
  subjects : Array[SubjectBudget]
} derive(Debug)

///|
fn max_int(a : Int, b : Int) -> Int {
  if a > b {
    a
  } else {
    b
  }
}

///|
fn min_int(a : Int, b : Int) -> Int {
  if a < b {
    a
  } else {
    b
  }
}

///|
fn non_negative(value : Int) -> Int {
  if value < 0 {
    0
  } else {
    value
  }
}

///|
fn positive_cost(cost : Int) -> Int {
  max_int(1, cost)
}

///|
pub fn QuotaKey::new(
  scope : String,
  subject : String,
  resource : String,
) -> QuotaKey {
  { scope, subject, resource }
}

///|
pub fn QuotaKey::key(self : QuotaKey) -> String {
  "\{self.scope}:\{self.subject}:\{self.resource}"
}

///|
pub fn EvaluationInput::new(
  key : QuotaKey,
  now_ms : Int64,
  cost : Int,
) -> EvaluationInput {
  { key, now_ms, cost }
}

///|
pub fn QuotaCharge::new(
  key : QuotaKey,
  limit_name : String,
  cost : Int,
) -> QuotaCharge {
  { key, limit_name, cost: positive_cost(cost) }
}

///|
pub fn AtomicQuotaReport::to_json(self : AtomicQuotaReport) -> String {
  "{ \"allowed\": \{self.allowed}, \"committed\": \{self.committed}, \"failed_index\": \{self.failed_index}, \"decisions\": \{self.decisions.length()} }"
}

///|
pub fn LimitSpec::new(
  name : String,
  capacity : Int,
  refill : Int,
  window_ms : Int64,
  burst : Int,
) -> LimitSpec {
  { name, capacity, refill, window_ms, burst }
}

///|
pub fn LimitSpec::per_second(name : String, capacity : Int) -> LimitSpec {
  { name, capacity, refill: capacity, window_ms: 1000L, burst: capacity }
}

///|
pub fn LimitSpec::per_minute(name : String, capacity : Int) -> LimitSpec {
  { name, capacity, refill: capacity, window_ms: 60000L, burst: capacity }
}

///|
pub fn Decision::allow(
  reason : String,
  cost : Int,
  remaining : Int,
  reset_after_ms : Int64,
  trace : Array[String],
) -> Decision {
  {
    allowed: true,
    reason,
    cost,
    remaining,
    retry_after_ms: 0L,
    reset_after_ms,
    trace,
  }
}

///|
pub fn Decision::deny(
  reason : String,
  cost : Int,
  remaining : Int,
  retry_after_ms : Int64,
  reset_after_ms : Int64,
  trace : Array[String],
) -> Decision {
  {
    allowed: false,
    reason,
    cost,
    remaining,
    retry_after_ms,
    reset_after_ms,
    trace,
  }
}

///|
pub fn Decision::to_json(self : Decision) -> String {
  "{ \"allowed\": \{self.allowed}, \"reason\": \"\{self.reason}\", \"cost\": \{self.cost}, \"remaining\": \{self.remaining}, \"retry_after_ms\": \{self.retry_after_ms}, \"reset_after_ms\": \{self.reset_after_ms} }"
}

///|
pub fn BatchReport::to_json(self : BatchReport) -> String {
  "{ \"total\": \{self.total}, \"allowed\": \{self.allowed}, \"denied\": \{self.denied} }"
}

///|
pub fn QuotaStats::to_json(self : QuotaStats) -> String {
  "{ \"limits\": \{self.limits}, \"token_buckets\": \{self.token_buckets}, \"windows\": \{self.windows}, \"gcra_flows\": \{self.gcra_flows}, \"subjects\": \{self.subjects} }"
}

///|
pub fn ValidationIssue::to_json(self : ValidationIssue) -> String {
  "{ \"code\": \"\{self.code}\", \"message\": \"\{self.message}\" }"
}

///|
pub fn BucketState::new(tokens : Int, last_refill_ms : Int64) -> BucketState {
  { tokens: non_negative(tokens), last_refill_ms }
}

///|
pub fn TrackedBucket::new(
  key : QuotaKey,
  limit_name : String,
  state : BucketState,
) -> TrackedBucket {
  { key, limit_name, state }
}

///|
pub fn QuotaEngine::new() -> QuotaEngine {
  {
    limits: [],
    buckets: [],
    windows: [],
    gcra_flows: [],
    links: [],
    subjects: [],
  }
}

///|
/// Copies all deterministic state for persistence, hand-off, or replication.
pub fn QuotaEngine::snapshot(self : QuotaEngine) -> QuotaSnapshot {
  {
    limits: self.limits.copy(),
    buckets: self.buckets.copy(),
    windows: self.windows.copy(),
    gcra_flows: self.gcra_flows.copy(),
    links: self.links.copy(),
    subjects: self.subjects.copy(),
  }
}

///|
/// Restores a snapshot atomically at the engine boundary. Callers are expected
/// to validate application-level serialization before restoring it.
pub fn QuotaEngine::restore(
  self : QuotaEngine,
  snapshot : QuotaSnapshot,
) -> Unit {
  self.limits.clear()
  self.buckets.clear()
  self.windows.clear()
  self.gcra_flows.clear()
  self.links.clear()
  self.subjects.clear()
  for item in snapshot.limits {
    self.limits.push(item)
  }
  for item in snapshot.buckets {
    self.buckets.push(item)
  }
  for item in snapshot.windows {
    self.windows.push(item)
  }
  for item in snapshot.gcra_flows {
    self.gcra_flows.push(item)
  }
  for item in snapshot.links {
    self.links.push(item)
  }
  for item in snapshot.subjects {
    self.subjects.push(item)
  }
}

///|
/// Exports a compact, deterministic operational summary for adapter health
/// checks. Full state remains available through `snapshot`.
pub fn QuotaSnapshot::to_json(self : QuotaSnapshot) -> String {
  "{\"limits\":\{self.limits.length()},\"token_buckets\":\{self.buckets.length()},\"windows\":\{self.windows.length()},\"gcra_flows\":\{self.gcra_flows.length()},\"links\":\{self.links.length()},\"subjects\":\{self.subjects.length()}}"
}

///|
pub fn QuotaEngine::add_limit(self : QuotaEngine, limit : LimitSpec) -> Bool {
  for current in self.limits {
    if current.name == limit.name {
      return false
    }
  }
  self.limits.push(limit)
  true
}

///|
pub fn QuotaEngine::limit_count(self : QuotaEngine) -> Int {
  self.limits.length()
}

///|
pub fn QuotaEngine::bucket_count(self : QuotaEngine) -> Int {
  self.buckets.length()
}

///|
pub fn QuotaEngine::window_count(self : QuotaEngine) -> Int {
  self.windows.length()
}

///|
pub fn QuotaEngine::gcra_count(self : QuotaEngine) -> Int {
  self.gcra_flows.length()
}

///|
pub fn QuotaEngine::link_count(self : QuotaEngine) -> Int {
  self.links.length()
}

///|
pub fn QuotaEngine::subject_count(self : QuotaEngine) -> Int {
  self.subjects.length()
}

///|
pub fn QuotaEngine::add_link(self : QuotaEngine, link : QuotaLink) -> Bool {
  for current in self.links {
    if current == link {
      return false
    }
  }
  self.links.push(link)
  true
}

///|
pub fn SubjectBudget::new(subject : String, weight : Int) -> SubjectBudget {
  { subject, weight: max_int(1, weight), used: 0 }
}

///|
fn find_subject_index(subjects : Array[SubjectBudget], subject : String) -> Int {
  for i = 0; i < subjects.length(); i = i + 1 {
    if subjects[i].subject == subject {
      return i
    }
  }
  -1
}

///|
pub fn QuotaEngine::add_subject(
  self : QuotaEngine,
  subject : String,
  weight : Int,
) -> Bool {
  if find_subject_index(self.subjects, subject) >= 0 {
    return false
  }
  self.subjects.push(SubjectBudget::new(subject, weight))
  true
}

///|
fn find_limit(limits : Array[LimitSpec], name : String) -> LimitSpec? {
  for limit in limits {
    if limit.name == name {
      return Some(limit)
    }
  }
  None
}

///|
fn find_bucket_index(
  buckets : Array[TrackedBucket],
  key : QuotaKey,
  limit_name : String,
) -> Int {
  for i = 0; i < buckets.length(); i = i + 1 {
    if buckets[i].key == key && buckets[i].limit_name == limit_name {
      return i
    }
  }
  -1
}

///|
fn find_window_index(
  windows : Array[TrackedWindow],
  key : QuotaKey,
  limit_name : String,
) -> Int {
  for i = 0; i < windows.length(); i = i + 1 {
    if windows[i].key == key && windows[i].limit_name == limit_name {
      return i
    }
  }
  -1
}

///|
fn find_gcra_index(
  flows : Array[TrackedGcra],
  key : QuotaKey,
  limit_name : String,
) -> Int {
  for i = 0; i < flows.length(); i = i + 1 {
    if flows[i].key == key && flows[i].limit_name == limit_name {
      return i
    }
  }
  -1
}

///|
fn refill_bucket(
  limit : LimitSpec,
  state : BucketState,
  now_ms : Int64,
) -> BucketState {
  if now_ms <= state.last_refill_ms || limit.window_ms <= 0L {
    return state
  }
  let elapsed = now_ms - state.last_refill_ms
  let periods = elapsed / limit.window_ms
  if periods <= 0L {
    return state
  }
  let refill_amount = periods.to_int() * max_int(1, limit.refill)
  {
    tokens: min_int(limit.capacity, state.tokens + refill_amount),
    last_refill_ms: state.last_refill_ms + periods * limit.window_ms,
  }
}

///|
pub fn QuotaEngine::check_token_bucket(
  self : QuotaEngine,
  key : QuotaKey,
  limit_name : String,
  now_ms : Int64,
  cost : Int,
) -> Decision {
  let normalized_cost = positive_cost(cost)
  let trace : Array[String] = []
  match find_limit(self.limits, limit_name) {
    None => {
      trace.push("missing limit \{limit_name}")
      Decision::deny("missing-limit", normalized_cost, 0, 0L, 0L, trace)
    }
    Some(limit) => {
      let index = find_bucket_index(self.buckets, key, limit_name)
      let original = if index >= 0 {
        self.buckets[index].state
      } else {
        BucketState::new(limit.capacity, now_ms)
      }
      let refilled = refill_bucket(limit, original, now_ms)
      trace.push(
        "bucket \{key.key()} tokens \{original.tokens}->\{refilled.tokens}",
      )
      if refilled.tokens >= normalized_cost {
        let next = {
          tokens: refilled.tokens - normalized_cost,
          last_refill_ms: refilled.last_refill_ms,
        }
        if index >= 0 {
          self.buckets[index] = { key, limit_name, state: next }
        } else {
          self.buckets.push({ key, limit_name, state: next })
        }
        Decision::allow(
          "token-bucket-allow",
          normalized_cost,
          next.tokens,
          limit.window_ms,
          trace,
        )
      } else {
        if index >= 0 {
          self.buckets[index] = { key, limit_name, state: refilled }
        } else {
          self.buckets.push({ key, limit_name, state: refilled })
        }
        let missing = normalized_cost - refilled.tokens
        let periods = (missing + max_int(1, limit.refill) - 1) /
          max_int(1, limit.refill)
        Decision::deny(
          "token-bucket-deny",
          normalized_cost,
          refilled.tokens,
          periods.to_int64() * limit.window_ms,
          limit.window_ms,
          trace,
        )
      }
    }
  }
}

///|
fn QuotaEngine::clone_engine(self : QuotaEngine) -> QuotaEngine {
  {
    limits: self.limits.copy(),
    buckets: self.buckets.copy(),
    windows: self.windows.copy(),
    gcra_flows: self.gcra_flows.copy(),
    links: self.links.copy(),
    subjects: self.subjects.copy(),
  }
}

///|
fn replace_buckets(
  target : Array[TrackedBucket],
  source : Array[TrackedBucket],
) -> Unit {
  target.clear()
  for bucket in source {
    target.push(bucket)
  }
}

///|
/// Atomically evaluates token-bucket charges against one logical instant.
/// No bucket is changed unless every charge is allowed.
pub fn QuotaEngine::check_atomic_token_buckets(
  self : QuotaEngine,
  charges : Array[QuotaCharge],
  now_ms : Int64,
) -> AtomicQuotaReport {
  let staging = self.clone_engine()
  let decisions : Array[Decision] = []
  for i = 0; i < charges.length(); i = i + 1 {
    let charge = charges[i]
    let decision = staging.check_token_bucket(
      charge.key,
      charge.limit_name,
      now_ms,
      charge.cost,
    )
    decisions.push(decision)
    if !decision.allowed {
      return { allowed: false, committed: false, failed_index: i, decisions }
    }
  }
  replace_buckets(self.buckets, staging.buckets)
  { allowed: true, committed: true, failed_index: -1, decisions }
}

///|
fn window_start(now_ms : Int64, window_ms : Int64) -> Int64 {
  if window_ms <= 0L {
    0L
  } else {
    now_ms / window_ms * window_ms
  }
}

///|
pub fn QuotaEngine::check_window(
  self : QuotaEngine,
  key : QuotaKey,
  limit_name : String,
  now_ms : Int64,
  cost : Int,
) -> Decision {
  let normalized_cost = positive_cost(cost)
  let trace : Array[String] = []
  match find_limit(self.limits, limit_name) {
    None => {
      trace.push("missing limit \{limit_name}")
      Decision::deny("missing-limit", normalized_cost, 0, 0L, 0L, trace)
    }
    Some(limit) => {
      let start = window_start(now_ms, limit.window_ms)
      let index = find_window_index(self.windows, key, limit_name)
      let original = if index >= 0 &&
        self.windows[index].state.window_start_ms == start {
        self.windows[index].state
      } else {
        { window_start_ms: start, used: 0 }
      }
      trace.push("window \{key.key()} used \{original.used}/\{limit.capacity}")
      let next_used = original.used + normalized_cost
      if next_used <= limit.capacity {
        let next = { window_start_ms: start, used: next_used }
        if index >= 0 {
          self.windows[index] = { key, limit_name, state: next }
        } else {
          self.windows.push({ key, limit_name, state: next })
        }
        Decision::allow(
          "window-allow",
          normalized_cost,
          limit.capacity - next_used,
          start + limit.window_ms - now_ms,
          trace,
        )
      } else {
        if index >= 0 {
          self.windows[index] = { key, limit_name, state: original }
        } else {
          self.windows.push({ key, limit_name, state: original })
        }
        Decision::deny(
          "window-deny",
          normalized_cost,
          max_int(0, limit.capacity - original.used),
          start + limit.window_ms - now_ms,
          start + limit.window_ms - now_ms,
          trace,
        )
      }
    }
  }
}

///|
fn emission_interval_ms(limit : LimitSpec) -> Int64 {
  if limit.capacity <= 0 || limit.window_ms <= 0L {
    0L
  } else {
    limit.window_ms / limit.capacity.to_int64()
  }
}

///|
pub fn QuotaEngine::check_gcra(
  self : QuotaEngine,
  key : QuotaKey,
  limit_name : String,
  now_ms : Int64,
  cost : Int,
) -> Decision {
  let normalized_cost = positive_cost(cost)
  let trace : Array[String] = []
  match find_limit(self.limits, limit_name) {
    None => {
      trace.push("missing limit \{limit_name}")
      Decision::deny("missing-limit", normalized_cost, 0, 0L, 0L, trace)
    }
    Some(limit) => {
      let interval = emission_interval_ms(limit)
      if interval <= 0L {
        trace.push("invalid gcra interval")
        return Decision::deny(
          "invalid-limit", normalized_cost, 0, 0L, 0L, trace,
        )
      }
      let tolerance = max_int(0, limit.burst).to_int64() * interval
      let index = find_gcra_index(self.gcra_flows, key, limit_name)
      let old_tat = if index >= 0 {
        self.gcra_flows[index].state.theoretical_arrival_ms
      } else {
        now_ms
      }
      let allowed_at = old_tat - tolerance
      trace.push("gcra \{key.key()} tat=\{old_tat} allowed_at=\{allowed_at}")
      if now_ms >= allowed_at {
        let base = if old_tat > now_ms { old_tat } else { now_ms }
        let next_tat = base + normalized_cost.to_int64() * interval
        let state = { theoretical_arrival_ms: next_tat }
        if index >= 0 {
          self.gcra_flows[index] = { key, limit_name, state }
        } else {
          self.gcra_flows.push({ key, limit_name, state })
        }
        let remaining = max_int(
          0,
          limit.burst - ((next_tat - now_ms) / interval).to_int(),
        )
        Decision::allow(
          "gcra-allow",
          normalized_cost,
          remaining,
          next_tat - now_ms,
          trace,
        )
      } else {
        Decision::deny(
          "gcra-deny",
          normalized_cost,
          0,
          allowed_at - now_ms,
          old_tat - now_ms,
          trace,
        )
      }
    }
  }
}

///|
pub fn QuotaLink::new(
  child : QuotaKey,
  parent : QuotaKey,
  limit_name : String,
  ratio : Int,
) -> QuotaLink {
  { child, parent, limit_name, ratio: max_int(1, ratio) }
}

///|
fn append_trace(target : Array[String], source : Array[String]) -> Unit {
  for item in source {
    target.push(item)
  }
}

///|
pub fn QuotaEngine::check_hierarchy(
  self : QuotaEngine,
  key : QuotaKey,
  limit_name : String,
  now_ms : Int64,
  cost : Int,
) -> Decision {
  let normalized_cost = positive_cost(cost)
  let trace : Array[String] = []
  let charges : Array[QuotaCharge] = [
    QuotaCharge::new(key, limit_name, normalized_cost),
  ]
  for link in self.links {
    if link.child == key && link.limit_name == limit_name {
      let parent_cost = normalized_cost * max_int(1, link.ratio)
      charges.push(QuotaCharge::new(link.parent, limit_name, parent_cost))
    }
  }
  let report = self.check_atomic_token_buckets(charges, now_ms)
  for decision in report.decisions {
    append_trace(trace, decision.trace)
  }
  if !report.allowed {
    let failed = report.decisions[report.failed_index]
    let reason = if report.failed_index == 0 {
      trace.push("child quota blocked")
      "hierarchy-child-deny"
    } else {
      trace.push("parent quota blocked")
      "hierarchy-parent-deny"
    }
    return Decision::deny(
      reason,
      normalized_cost,
      failed.remaining,
      failed.retry_after_ms,
      failed.reset_after_ms,
      trace,
    )
  }
  let child_decision = report.decisions[0]
  Decision::allow(
    "hierarchy-allow",
    normalized_cost,
    child_decision.remaining,
    child_decision.reset_after_ms,
    trace,
  )
}

///|
pub fn QuotaEngine::check_many_token_bucket(
  self : QuotaEngine,
  limit_name : String,
  requests : Array[EvaluationInput],
) -> BatchReport {
  let decisions : Array[Decision] = []
  let mut allowed = 0
  let mut denied = 0
  for request in requests {
    let decision = self.check_token_bucket(
      request.key,
      limit_name,
      request.now_ms,
      request.cost,
    )
    if decision.allowed {
      allowed = allowed + 1
    } else {
      denied = denied + 1
    }
    decisions.push(decision)
  }
  { total: requests.length(), allowed, denied, decisions }
}

///|
fn fair_score(item : SubjectBudget) -> Int {
  item.used * 1000 / max_int(1, item.weight)
}

///|
pub fn QuotaEngine::record_usage(
  self : QuotaEngine,
  subject : String,
  cost : Int,
) -> Bool {
  let index = find_subject_index(self.subjects, subject)
  if index < 0 {
    return false
  }
  let old = self.subjects[index]
  self.subjects[index] = {
    subject: old.subject,
    weight: old.weight,
    used: old.used + positive_cost(cost),
  }
  true
}

///|
pub fn QuotaEngine::next_fair_subject(self : QuotaEngine) -> String {
  if self.subjects.length() == 0 {
    return ""
  }
  let mut best = self.subjects[0]
  let mut best_score = fair_score(best)
  for item in self.subjects {
    let score = fair_score(item)
    if score < best_score {
      best = item
      best_score = score
    }
  }
  best.subject
}

///|
pub fn QuotaEngine::fair_snapshot(self : QuotaEngine) -> Array[FairShareItem] {
  let result : Array[FairShareItem] = []
  for item in self.subjects {
    result.push({
      subject: item.subject,
      weight: item.weight,
      used: item.used,
      score: fair_score(item),
    })
  }
  result
}

///|
fn push_unique(items : Array[String], value : String) -> Unit {
  for item in items {
    if item == value {
      return
    }
  }
  items.push(value)
}

///|
pub fn QuotaEngine::stats(self : QuotaEngine) -> QuotaStats {
  let subjects : Array[String] = []
  for bucket in self.buckets {
    push_unique(subjects, bucket.key.subject)
  }
  for window in self.windows {
    push_unique(subjects, window.key.subject)
  }
  for flow in self.gcra_flows {
    push_unique(subjects, flow.key.subject)
  }
  for subject in self.subjects {
    push_unique(subjects, subject.subject)
  }
  {
    limits: self.limits.length(),
    token_buckets: self.buckets.length(),
    windows: self.windows.length(),
    gcra_flows: self.gcra_flows.length(),
    subjects: subjects.length(),
  }
}

///|
pub fn QuotaEngine::validate(self : QuotaEngine) -> Array[ValidationIssue] {
  let issues : Array[ValidationIssue] = []
  for limit in self.limits {
    if limit.name == "" {
      issues.push({
        code: "empty-limit-name",
        message: "limit name must not be empty",
      })
    }
    if limit.capacity <= 0 {
      issues.push({
        code: "invalid-capacity",
        message: "\{limit.name} capacity must be positive",
      })
    }
    if limit.refill <= 0 {
      issues.push({
        code: "invalid-refill",
        message: "\{limit.name} refill must be positive",
      })
    }
    if limit.window_ms <= 0L {
      issues.push({
        code: "invalid-window",
        message: "\{limit.name} window_ms must be positive",
      })
    }
  }
  for link in self.links {
    if find_limit(self.limits, link.limit_name) is None {
      issues.push({
        code: "missing-linked-limit",
        message: "\{link.limit_name} used by link \{link.child.key()} is missing",
      })
    }
    if link.child == link.parent {
      issues.push({
        code: "self-link",
        message: "\{link.child.key()} cannot link to itself",
      })
    }
  }
  for subject in self.subjects {
    if subject.subject == "" {
      issues.push({
        code: "empty-subject",
        message: "fair-share subject must not be empty",
      })
    }
    if subject.weight <= 0 {
      issues.push({
        code: "invalid-weight",
        message: "\{subject.subject} weight must be positive",
      })
    }
  }
  issues
}

///|
pub fn QuotaEngine::to_json(self : QuotaEngine) -> String {
  let buf = StringBuilder()
  buf.write_string("{\"limits\":[")
  for i = 0; i < self.limits.length(); i = i + 1 {
    if i > 0 {
      buf.write_char(',')
    }
    let limit = self.limits[i]
    buf.write_string(
      "{\"name\":\"\{limit.name}\",\"capacity\":\{limit.capacity},\"refill\":\{limit.refill},\"window_ms\":\{limit.window_ms},\"burst\":\{limit.burst}}",
    )
  }
  buf.write_string("],\"stats\":")
  buf.write_string(self.stats().to_json())
  buf.write_string("}")
  buf.to_string()
}

///|
pub fn FairShareItem::to_json(self : FairShareItem) -> String {
  "{ \"subject\": \"\{self.subject}\", \"weight\": \{self.weight}, \"used\": \{self.used}, \"score\": \{self.score} }"
}