///|
/// Compile tiers and detach them from caller-owned arrays.
/// The default mode preserves graduated pricing for existing callers.
/// Bounds must increase strictly, and only the last bound must be None.
pub fn compile_plan(
  id~ : String,
  version~ : String,
  metric~ : String,
  unit~ : String,
  currency~ : String,
  tiers~ : ArrayView[Tier],
  mode~ : PricingMode = Graduated,
) -> Plan raise PricingError {
  label(id, "plan.id")
  label(version, "plan.version")
  label(metric, "plan.metric")
  label(unit, "plan.unit")
  check_currency(currency)
  if tiers.length() == 0 || tiers.length() > 1024 {
    invalid(
      "tiers", "plan.tiers", "provide 1..1024 tiers, ending with an unbounded tier",
    )
  }
  let compiled = []
  let mut previous = 0N
  for i, tier in tiers {
    let path = "plan.tiers[\{i}]"
    let bound = match tier.up_to {
      Some(text) => {
        if i == tiers.length() - 1 {
          invalid(
            "unbounded",
            path + ".up_to",
            "the last tier must have an unbounded upper limit",
          )
        }
        let n = natural(text, path + ".up_to")
        if n <= previous {
          invalid(
            "order",
            path + ".up_to",
            "upper bounds must be strictly increasing and positive",
          )
        }
        previous = n
        Some(n)
      }
      None => {
        if i != tiers.length() - 1 {
          invalid(
            "unbounded",
            path + ".up_to",
            "only the last tier may be unbounded",
          )
        }
        None
      }
    }
    compiled.push({
      up_to: bound,
      price: decimal(tier.unit_price, path + ".unit_price"),
    })
  }
  { id, version, metric, unit, currency, mode, tiers: compiled, }
}

///|
fn check_currency(currency : String) -> Unit raise PricingError {
  if currency.length() != 3 {
    invalid(
      "currency", "plan.currency", "use an explicit three-letter uppercase currency label",
    )
  }
  for c in currency {
    if c < 'A' || c > 'Z' {
      invalid(
        "currency", "plan.currency", "use ASCII uppercase letters; no currency conversion is performed",
      )
    }
  }
}

///|
/// Return the plan's explicit pricing semantics.
pub fn Plan::mode(self : Plan) -> PricingMode {
  self.mode
}

///|
fn validate_usage(plan : Plan, usage : Usage) -> BigInt raise PricingError {
  label(usage.customer, "usage.customer")
  label(usage.metric, "usage.metric")
  label(usage.period, "usage.period")
  if usage.metric != plan.metric {
    invalid("metric", "usage.metric", "expected metric '\{plan.metric}'")
  }
  natural(usage.quantity, "usage.quantity")
}