///|
/// Encode a complete trusted input manifest with embedded validated plans.
pub fn encode_invoice_input(
  items : ArrayView[ChargeInput],
) -> String raise PricingError {
  check_invoice_inputs(items)
  let entries : Array[Json] = items.map(item => {
    "plan": plan_json(item.plan),
    "usage": usage_json(item.usage),
    "minimum": item.minimum,
    "zero_usage": zero_usage_name(item.zero_usage),
  })
  let json : Json = { "schema": "jieheng.invoice-input.v1", "items": entries }
  bounded_json(json)
}

///|
pub fn decode_invoice_input(
  text : String,
) -> Array[ChargeInput] raise PricingError {
  let root = fields(parse_document(text), ["schema", "items"], "invoice")
  expect_value(root, "schema", "jieheng.invoice-input.v1", "invoice")
  let entries = bounded_items(root["items"], 64, "invoice.items")
  let items : Array[ChargeInput] = []
  for i, entry in entries {
    let path = "invoice.items[\{i}]"
    let item = fields(entry, ["plan", "usage", "minimum", "zero_usage"], path)
    items.push({
      plan: plan_from_json(item["plan"]),
      usage: usage_from_json(item["usage"]),
      minimum: text_value(item["minimum"], path + ".minimum"),
      zero_usage: zero_usage_from_name(
        text_value(item["zero_usage"], path + ".zero_usage"),
      ),
    })
  }
  check_invoice_inputs(items[:])
  items
}

///|
/// Structural encoding; verify_invoice is required for semantic acceptance.
pub fn encode_invoice(invoice : Invoice) -> String raise PricingError {
  if invoice.charges.length() == 0 || invoice.charges.length() > 64 {
    invalid("size", "invoice.charges", "expected 1 to 64 charges")
  }
  let json : Json = {
    "schema": "jieheng.invoice.v1",
    "customer": invoice.customer,
    "period": invoice.period,
    "currency": invoice.currency,
    "charges": invoice.charges.map(charge_json),
    "usage_total": invoice.usage_total,
    "top_up": invoice.top_up,
    "total": invoice.total,
    "scale": invoice.scale,
    "rounding": rounding_name(invoice.rounding),
    "minor_units": invoice.minor_units,
    "amount": invoice.amount,
    "delta": invoice.delta,
  }
  bounded_json(json)
}

///|
pub fn decode_invoice(text : String) -> Invoice raise PricingError {
  let root = fields(
    parse_document(text),
    [
      "schema", "customer", "period", "currency", "charges", "usage_total", "top_up",
      "total", "scale", "rounding", "minor_units", "amount", "delta",
    ],
    "invoice",
  )
  expect_value(root, "schema", "jieheng.invoice.v1", "invoice")
  {
    customer: text_value(root["customer"], "invoice.customer"),
    period: text_value(root["period"], "invoice.period"),
    currency: text_value(root["currency"], "invoice.currency"),
    charges: bounded_items(root["charges"], 64, "invoice.charges").map(
      charge_from_json,
    ),
    usage_total: text_value(root["usage_total"], "invoice.usage_total"),
    top_up: text_value(root["top_up"], "invoice.top_up"),
    total: text_value(root["total"], "invoice.total"),
    scale: small_integer(root["scale"], 18, "invoice.scale"),
    rounding: rounding_from_name(
      text_value(root["rounding"], "invoice.rounding"),
    ),
    minor_units: text_value(root["minor_units"], "invoice.minor_units"),
    amount: text_value(root["amount"], "invoice.amount"),
    delta: text_value(root["delta"], "invoice.delta"),
  }
}