///|
pub(all) struct ActionBudget {
  action : String
  max_new_grants : Int
  max_revocations : Int
} derive(Debug)

///|
fn budget_for_action(
  budgets : Array[ActionBudget],
  action : String,
) -> ActionBudget {
  for budget in budgets {
    if budget.action == action {
      return budget
    }
  }
  { action, max_new_grants: 0, max_revocations: 0, }
}

///|
fn count_action_changes(
  report : AuditReport,
  action : String,
  kind : ChangeKind,
) -> Int {
  let mut count = 0
  for change in report.changes {
    if change.request.action == action && change.kind == kind {
      count += 1
    }
  }
  count
}

///|
/// Independent per-action budgets. Unlisted actions have a zero budget.
/// This gate complements the tenant-aware strict gate.
pub fn check_action_budgets(
  report : AuditReport,
  budgets : Array[ActionBudget],
) -> GateResult {
  let findings : Array[GateFinding] = []
  if !report.complete {
    return { verdict: Indeterminate, findings, }
  }
  let seen : Array[String] = []
  for budget in budgets {
    if !is_valid_identifier(budget.action) ||
      budget.max_new_grants < 0 ||
      budget.max_revocations < 0 ||
      seen.contains(budget.action) {
      return { verdict: Indeterminate, findings, }
    }
    seen.push(budget.action)
  }
  let actions : Array[String] = []
  for change in report.changes {
    if !actions.contains(change.request.action) {
      actions.push(change.request.action)
    }
  }
  for action in actions {
    let budget = budget_for_action(budgets, action)
    let grants = count_action_changes(report, action, NewGrant)
    let revocations = count_action_changes(report, action, RevokedGrant)
    if grants > budget.max_new_grants {
      for change in report.changes {
        if change.request.action == action && change.kind is NewGrant {
          findings.push({
            code: "ACTION_GRANT_BUDGET",
            request: change.request,
            message: "new grants for \{action} exceed its budget",
          })
        }
      }
    }
    if revocations > budget.max_revocations {
      for change in report.changes {
        if change.request.action == action && change.kind is RevokedGrant {
          findings.push({
            code: "ACTION_REVOCATION_BUDGET",
            request: change.request,
            message: "revocations for \{action} exceed its budget",
          })
        }
      }
    }
  }
  { verdict: if findings.length() == 0 { Pass } else { Fail }, findings, }
}