///|
/// Strict versioned JSON plan decoder. Unknown fields and duplicate keys fail.
pub fn decode_plan(text : String) -> Plan raise PricingError {
  plan_from_json(parse_document(text))
}

///|
fn plan_from_json(value : Json) -> Plan raise PricingError {
  let object = fields(
    value,
    ["schema", "id", "version", "metric", "unit", "currency", "mode", "tiers"],
    "plan",
  )
  expect_value(object, "schema", "jieheng.plan.v1", "plan")
  let mode = match text_value(object["mode"], "plan.mode") {
    "graduated" => Graduated
    "volume" => Volume
    _ =>
      raise Invalid(
        code="unsupported",
        path="plan.mode",
        message="choose graduated or volume",
      )
  }
  guard object["tiers"] is Array(items) else {
    raise Invalid(code="type", path="plan.tiers", message="expected an array")
  }
  if items.length() > 1024 {
    invalid("tiers", "plan.tiers", "maximum 1024 tiers")
  }
  let tiers = []
  for i, item in items {
    let path = "plan.tiers[\{i}]"
    let tier = fields(item, ["up_to", "unit_price"], path)
    let up_to = match tier["up_to"] {
      Null => None
      value => Some(text_value(value, path + ".up_to"))
    }
    tiers.push({
      up_to,
      unit_price: text_value(tier["unit_price"], path + ".unit_price"),
    })
  }
  compile_plan(
    id=text_value(object["id"], "plan.id"),
    version=text_value(object["version"], "plan.version"),
    metric=text_value(object["metric"], "plan.metric"),
    unit=text_value(object["unit"], "plan.unit"),
    currency=text_value(object["currency"], "plan.currency"),
    tiers~,
    mode~,
  )
}

///|
/// Deterministic schema-order JSON; decimal trailing zeroes are normalized.
pub fn encode_plan(plan : Plan) -> String {
  plan_json(plan).stringify()
}

///|
fn plan_json(plan : Plan) -> Json {
  let tiers : Array[Json] = []
  for tier in plan.tiers {
    let bound : Json = match tier.up_to {
      Some(n) => Json::string(n.to_string())
      None => Json::null()
    }
    tiers.push({ "up_to": bound, "unit_price": format_atoms(tier.price) })
  }
  {
    "schema": "jieheng.plan.v1",
    "id": plan.id,
    "version": plan.version,
    "metric": plan.metric,
    "unit": plan.unit,
    "currency": plan.currency,
    "mode": pricing_mode_name(plan.mode),
    "tiers": tiers,
  }
}

///|
fn pricing_mode_name(mode : PricingMode) -> String {
  match mode {
    Graduated => "graduated"
    Volume => "volume"
  }
}