///|
/// Trusted charge input: one already aggregated metric, one plan and one floor.
/// Plans are validated snapshots; arrays are consumed without being retained.
pub(all) struct ChargeInput {
  plan : Plan
  usage : Usage
  minimum : String
  zero_usage : ZeroUsageMinimum
}

///|
/// Pricing statement, not a legal invoice or payment instruction.
/// Each charge is settled separately; amount sums those settlements exactly.
/// delta = amount - total can exceed one minor unit over multiple charges.
pub(all) struct Invoice {
  customer : String
  period : String
  currency : String
  charges : Array[Charge]
  usage_total : String
  top_up : String
  total : String
  scale : Int32
  rounding : Rounding
  minor_units : String
  amount : String
  delta : String
} derive(Debug, Eq)

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

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

///|
fn check_invoice_inputs(
  items : ArrayView[ChargeInput],
) -> Unit raise PricingError {
  if items.length() == 0 || items.length() > 64 {
    invalid("size", "invoice.items", "provide 1 to 64 metric charges")
  }
  let first = items[0]
  let seen : Map[String, Unit] = Map([])
  for i, item in items {
    let path = "invoice.items[\{i}]"
    ignore(validate_usage(item.plan, item.usage)) catch {
      Invalid(code~, path=inner, message~) =>
        raise Invalid(code~, path=path + "." + inner, message~)
    }
    ignore(decimal(item.minimum, path + ".minimum"))
    if item.usage.customer != first.usage.customer ||
      item.usage.period != first.usage.period ||
      item.plan.currency != first.plan.currency {
      invalid(
        "identity", path, "all charges must share one customer, period and currency; no FX conversion is supported",
      )
    }
    if seen.contains(item.usage.metric) {
      invalid(
        "duplicate",
        path + ".usage.metric",
        "one charge per metric is supported; aggregate duplicate metrics before rating",
      )
    }
    seen[item.usage.metric] = ()
  }
}

///|
/// Rate multiple metrics for one customer-period, apply each explicit minimum,
/// settle each charge, then sum minor units without a second rounding operation.
///
/// ```mbt check
/// test {
///   let p = @jieheng.compile_plan(
///     id="api",
///     version="v1",
///     metric="calls",
///     unit="call",
///     currency="USD",
///     tiers=[{ up_to: None, unit_price: "0.006", }],
///   )
///   let bill = @jieheng.rate_invoice(
///     [
///       {
///         plan: p,
///         usage: {
///           customer: "acme",
///           metric: "calls",
///           period: "2026-10",
///           quantity: "1",
///         },
///         minimum: "0",
///         zero_usage: Waive,
///       },
///     ],
///     scale=2,
///     rounding=HalfEven,
///   )
///   inspect(bill.total, content="0.006")
///   inspect(bill.amount, content="0.01")
/// }
/// ```
pub fn rate_invoice(
  items : ArrayView[ChargeInput],
  scale~ : Int32,
  rounding~ : Rounding,
) -> Invoice raise PricingError {
  check_invoice_inputs(items)
  let charges : Array[Charge] = []
  for i, item in items {
    charges.push(
      rate_charge(
        item.plan,
        item.usage,
        minimum=item.minimum,
        zero_usage=item.zero_usage,
        scale~,
        rounding~,
      ) catch {
        Invalid(code~, path~, message~) =>
          raise Invalid(code~, path="invoice.items[\{i}]." + path, message~)
      },
    )
  }
  let report = invoice_totals(charges[:], scale~, rounding~)
  verify_invoice(items, report, scale~, rounding~)
  report
}

///|
fn invoice_totals(
  charges : ArrayView[Charge],
  scale~ : Int32,
  rounding~ : Rounding,
) -> Invoice raise PricingError {
  let mut raw = 0N
  let mut top_up = 0N
  let mut total = 0N
  let mut units = 0N
  for charge in charges {
    raw += amount_atoms(charge.calculation.total, "invoice.usage_total")
    top_up += amount_atoms(charge.top_up, "invoice.top_up")
    total += amount_atoms(charge.total, "invoice.total")
    units += wide_natural(
      charge.settlement.minor_units,
      78,
      "invoice.minor_units",
    )
  }
  let amount = fixed_amount(units, scale)
  let paid = amount_atoms(amount, "invoice.amount")
  for n in [raw, top_up, total] {
    ignore(amount_atoms(format_atoms(n), "invoice.totals"))
  }
  let first = charges[0].calculation
  {
    customer: first.usage.customer,
    period: first.usage.period,
    currency: first.currency,
    charges: charges.to_owned(),
    usage_total: format_atoms(raw),
    top_up: format_atoms(top_up),
    total: format_atoms(total),
    scale,
    rounding,
    minor_units: units.to_string(),
    amount,
    delta: format_atoms(paid - total),
  }
}

///|
/// Verify against the complete trusted item list and settlement policy.
/// Missing/reordered/duplicated charges and all forged aggregates are rejected.
pub fn verify_invoice(
  items : ArrayView[ChargeInput],
  invoice : Invoice,
  scale~ : Int32,
  rounding~ : Rounding,
) -> Unit raise PricingError {
  check_invoice_inputs(items)
  if invoice.charges.length() != items.length() {
    invalid(
      "coverage", "invoice.charges", "exactly one charge is required per trusted input, in the original order",
    )
  }
  for i, item in items {
    verify_charge(
      item.plan,
      item.usage,
      invoice.charges[i],
      minimum=item.minimum,
      zero_usage=item.zero_usage,
      scale~,
      rounding~,
    ) catch {
      Invalid(code~, path~, message~) =>
        raise Invalid(code~, path="invoice.charges[\{i}]." + path, message~)
    }
  }
  let expected = invoice_totals(invoice.charges[:], scale~, rounding~)
  if invoice != expected {
    invalid(
      "total", "invoice.totals", "identity, policy and all totals must match the independently verified per-charge settlements; no invoice-wide re-rounding",
    )
  }
}