///|
/// Explicitly tagged inputs preserve each pricing model's complete contract.
pub(all) enum BillItem {
  Metered(ChargeInput)
  Packaged(PackagePlan, Usage)
}

///|
pub(all) enum BillEvidence {
  MeteredCharge(Charge)
  PackageCharge(PackageEvidence)
} derive(Debug, Eq)

///|
pub extend BillEvidence with Debug::{to_repr}

///|
pub extend BillEvidence with Eq::{equal, not_equal}

///|
/// Item settlements are summed without a second rounding operation.
pub(all) struct BillTotals {
  usage_total : String
  top_up : String
  total : String
  scale : Int32
  rounding : Rounding
  minor_units : String
  amount : String
  delta : String
} derive(Debug, Eq)

///|
pub extend BillTotals with Debug::{to_repr}

///|
pub extend BillTotals with Eq::{equal, not_equal}

///|
pub(all) struct Bill {
  customer : String
  period : String
  currency : String
  items : Array[BillEvidence]
  totals : BillTotals
} derive(Debug, Eq)

///|
pub extend Bill with Debug::{to_repr}

///|
pub extend Bill with Eq::{equal, not_equal}

///|
fn bill_identity(item : BillItem) -> (Usage, String) raise PricingError {
  match item {
    Metered(input) => {
      ignore(validate_usage(input.plan, input.usage))
      ignore(decimal(input.minimum, "minimum"))
      (input.usage, input.plan.currency)
    }
    Packaged(plan, usage) => {
      ignore(package_quantity(plan, usage))
      (usage, plan.currency)
    }
  }
}

///|
fn check_bill(
  items : ArrayView[BillItem],
) -> (Usage, String) raise PricingError {
  if items.length() == 0 || items.length() > 64 {
    invalid(
      "size", "bill.items", "provide 1 to 64 explicitly tagged metric items",
    )
  }
  let (first, currency) = bill_identity(items[0])
  let seen : Map[String, Unit] = Map([])
  for i, item in items {
    let (usage, c) = bill_identity(item) catch {
      Invalid(code~, path~, message~) =>
        raise Invalid(code~, path="bill.items[\{i}]." + path, message~)
    }
    if usage.customer != first.customer ||
      usage.period != first.period ||
      c != currency {
      invalid(
        "identity",
        "bill.items[\{i}]",
        "all items must share customer, period and currency",
      )
    }
    if seen.contains(usage.metric) {
      invalid(
        "duplicate",
        "bill.items[\{i}].metric",
        "a metric can be billed only once across all pricing models",
      )
    }
    seen[usage.metric] = ()
  }
  (first, currency)
}

///|
/// Combine metered and package items with their original evidence intact.
///
/// ```mbt check
/// test {
///   let p = @jieheng.compile_package(
///     id="sms",
///     version="v1",
///     metric="messages",
///     unit="message",
///     currency="USD",
///     package_size="1000",
///     package_price="0.85",
///   )
///   let b = @jieheng.rate_bill(
///     [
///       Packaged(p, {
///         customer: "acme",
///         metric: "messages",
///         period: "2026-10",
///         quantity: "2501",
///       }),
///     ],
///     scale=2,
///     rounding=HalfEven,
///   )
///   inspect(b.totals.amount, content="2.55")
/// }
/// ```
pub fn rate_bill(
  items : ArrayView[BillItem],
  scale~ : Int32,
  rounding~ : Rounding,
) -> Bill raise PricingError {
  let (usage, currency) = check_bill(items)
  let records : Array[BillEvidence] = []
  for i, item in items {
    let record = try {
      match item {
        Metered(c) =>
          MeteredCharge(
            rate_charge(
              c.plan,
              c.usage,
              minimum=c.minimum,
              zero_usage=c.zero_usage,
              scale~,
              rounding~,
            ),
          )
        Packaged(p, u) => PackageCharge(rate_packages(p, u, scale~, rounding~))
      }
    } catch {
      Invalid(code~, path~, message~) =>
        raise Invalid(code~, path="bill.items[\{i}]." + path, message~)
    }
    records.push(record)
  }
  let bill = {
    customer: usage.customer,
    period: usage.period,
    currency,
    items: records,
    totals: bill_totals(records[:], scale~, rounding~),
  }
  verify_bill(items, bill, scale~, rounding~)
  bill
}

///|
fn bill_totals(
  items : ArrayView[BillEvidence],
  scale~ : Int32,
  rounding~ : Rounding,
) -> BillTotals raise PricingError {
  let mut raw = 0N
  let mut top_up = 0N
  let mut exact = 0N
  let mut units = 0N
  for item in items {
    let (r, t, total, s) = match item {
      MeteredCharge(c) => (c.calculation.total, c.top_up, c.total, c.settlement)
      PackageCharge(c) => (c.total, "0", c.total, c.settlement)
    }
    raw += amount_atoms(r, "bill.usage_total")
    top_up += amount_atoms(t, "bill.top_up")
    exact += amount_atoms(total, "bill.total")
    units += wide_natural(s.minor_units, 78, "bill.minor_units")
  }
  for n in [raw, top_up, exact] {
    ignore(amount_atoms(format_atoms(n), "bill.totals"))
  }
  let amount = fixed_amount(units, scale)
  let paid = amount_atoms(amount, "bill.amount")
  {
    usage_total: format_atoms(raw),
    top_up: format_atoms(top_up),
    total: format_atoms(exact),
    scale,
    rounding,
    minor_units: units.to_string(),
    amount,
    delta: format_atoms(paid - exact),
  }
}

///|
/// Reject model substitution, omission, reordering and forged totals. Each item
/// is checked by its independent pricing verifier, against the trusted manifest.
pub fn verify_bill(
  items : ArrayView[BillItem],
  bill : Bill,
  scale~ : Int32,
  rounding~ : Rounding,
) -> Unit raise PricingError {
  let (usage, currency) = check_bill(items)
  if bill.customer != usage.customer ||
    bill.period != usage.period ||
    bill.currency != currency ||
    bill.items.length() != items.length() {
    invalid(
      "identity", "bill", "bill identity and ordered item coverage must match trusted inputs",
    )
  }
  for i, item in items {
    try {
      match (item, bill.items[i]) {
        (Metered(c), MeteredCharge(e)) =>
          verify_charge(
            c.plan,
            c.usage,
            e,
            minimum=c.minimum,
            zero_usage=c.zero_usage,
            scale~,
            rounding~,
          )
        (Packaged(p, u), PackageCharge(e)) =>
          verify_packages(p, u, e, scale~, rounding~)
        _ =>
          invalid(
            "model", "kind", "evidence kind must match the trusted pricing model",
          )
      }
    } catch {
      Invalid(code~, path~, message~) =>
        raise Invalid(code~, path="bill.items[\{i}]." + path, message~)
    }
  }
  if bill.totals != bill_totals(bill.items[:], scale~, rounding~) {
    invalid(
      "total", "bill.totals", "expected exact item totals and the sum of independently verified item minor units",
    )
  }
}