///|
/// A transport record, not a signature or an authenticated invoice.
pub(all) struct Evidence {
  calculation : Calculation
  settlement : Settlement
} derive(Debug, Eq)

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

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

///|
/// Verify against trusted inputs and policy, never against embedded claims alone.
pub fn verify_evidence(
  plan : Plan,
  usage : Usage,
  evidence : Evidence,
  scale~ : Int32,
  rounding~ : Rounding,
) -> Unit raise PricingError {
  verify_calculation(plan, usage, evidence.calculation)
  verify_settlement(
    evidence.calculation,
    evidence.settlement,
    scale~,
    rounding~,
  )
}

///|
fn rounding_name(rounding : Rounding) -> String {
  match rounding {
    HalfEven => "half-even"
    HalfUp => "half-up"
    Down => "down"
    Up => "up"
  }
}

///|
fn rounding_from_name(text : String) -> Rounding raise PricingError {
  match text {
    "half-even" => HalfEven
    "half-up" => HalfUp
    "down" => Down
    "up" => Up
    _ =>
      raise Invalid(
        code="rounding",
        path="settlement.rounding",
        message="choose half-even, half-up, down, or up",
      )
  }
}

///|
/// Encode every typed field in deterministic schema order.
/// Does not validate authenticity: use verify_evidence before accepting a record.
pub fn encode_evidence(evidence : Evidence) -> String {
  evidence_json(evidence).stringify()
}

///|
fn evidence_json(evidence : Evidence) -> Json {
  {
    "schema": "jieheng.evidence.v1",
    "calculation": calculation_json(evidence.calculation),
    "settlement": settlement_json(evidence.settlement),
  }
}

///|
fn calculation_json(c : Calculation) -> Json {
  let lines : Array[Json] = []
  for line in c.lines {
    lines.push({
      "tier": line.tier,
      "from_exclusive": line.from_exclusive,
      "to_inclusive": line.to_inclusive,
      "quantity": line.quantity,
      "unit_price": line.unit_price,
      "subtotal": line.subtotal,
    })
  }
  {
    "plan_id": c.plan_id,
    "plan_version": c.plan_version,
    "currency": c.currency,
    "unit": c.unit,
    "usage": usage_json(c.usage),
    "lines": lines,
    "total": c.total,
  }
}

///|
fn settlement_json(s : Settlement) -> Json {
  {
    "currency": s.currency,
    "scale": s.scale,
    "rounding": rounding_name(s.rounding),
    "minor_units": s.minor_units,
    "amount": s.amount,
    "delta": s.delta,
  }
}

///|
/// Strict structural decoder; semantic acceptance requires verify_evidence.
pub fn decode_evidence(text : String) -> Evidence raise PricingError {
  evidence_from_json(parse_document(text))
}

///|
fn evidence_from_json(value : Json) -> Evidence raise PricingError {
  let root = fields(value, ["schema", "calculation", "settlement"], "evidence")
  expect_value(root, "schema", "jieheng.evidence.v1", "evidence")
  {
    calculation: calculation_from_json(root["calculation"]),
    settlement: settlement_from_json(root["settlement"]),
  }
}

///|
fn calculation_from_json(value : Json) -> Calculation raise PricingError {
  let c = fields(
    value,
    ["plan_id", "plan_version", "currency", "unit", "usage", "lines", "total"],
    "calculation",
  )
  guard c["lines"] is Array(items) else {
    raise Invalid(
      code="type",
      path="calculation.lines",
      message="expected an array",
    )
  }
  if items.length() > 1024 {
    invalid("size", "calculation.lines", "maximum 1024 lines")
  }
  let lines : Array[Line] = []
  for i, item in items {
    let path = "calculation.lines[\{i}]"
    let line = fields(
      item,
      [
        "tier", "from_exclusive", "to_inclusive", "quantity", "unit_price", "subtotal",
      ],
      path,
    )
    lines.push({
      tier: small_integer(line["tier"], 1023, path + ".tier"),
      from_exclusive: text_value(
        line["from_exclusive"],
        path + ".from_exclusive",
      ),
      to_inclusive: text_value(line["to_inclusive"], path + ".to_inclusive"),
      quantity: text_value(line["quantity"], path + ".quantity"),
      unit_price: text_value(line["unit_price"], path + ".unit_price"),
      subtotal: text_value(line["subtotal"], path + ".subtotal"),
    })
  }
  {
    plan_id: text_value(c["plan_id"], "calculation.plan_id"),
    plan_version: text_value(c["plan_version"], "calculation.plan_version"),
    currency: text_value(c["currency"], "calculation.currency"),
    unit: text_value(c["unit"], "calculation.unit"),
    usage: usage_from_json(c["usage"]),
    lines,
    total: text_value(c["total"], "calculation.total"),
  }
}

///|
fn settlement_from_json(value : Json) -> Settlement raise PricingError {
  let s = fields(
    value,
    ["currency", "scale", "rounding", "minor_units", "amount", "delta"],
    "settlement",
  )
  {
    currency: text_value(s["currency"], "settlement.currency"),
    scale: small_integer(s["scale"], 18, "settlement.scale"),
    rounding: rounding_from_name(
      text_value(s["rounding"], "settlement.rounding"),
    ),
    minor_units: text_value(s["minor_units"], "settlement.minor_units"),
    amount: text_value(s["amount"], "settlement.amount"),
    delta: text_value(s["delta"], "settlement.delta"),
  }
}

///|
fn small_integer(
  value : Json,
  max : Int32,
  path : String,
) -> Int32 raise PricingError {
  guard value is Number(number, ..) &&
    number >= 0.0 &&
    number <= max.to_double() &&
    number == number.to_int().to_double() else {
    raise Invalid(
      code="integer",
      path~,
      message="expected an integer JSON index or scale within range",
    )
  }
  number.to_int()
}