///|
fn tariff_json(tariff : Tariff) -> Json {
  match tariff {
    MeteredTariff(p, minimum, zero) =>
      {
        "kind": "metered",
        "plan": plan_json(p),
        "minimum": minimum,
        "zero_usage": zero_usage_name(zero),
      }
    PackageTariff(p) => { "kind": "package", "plan": package_plan_json(p) }
  }
}

///|
fn tariff_from_json(value : Json, path : String) -> Tariff raise PricingError {
  match item_kind(value, path) {
    "metered" => {
      let r = fields(value, ["kind", "plan", "minimum", "zero_usage"], path)
      MeteredTariff(
        plan_from_json(r["plan"]),
        text_value(r["minimum"], path + ".minimum"),
        zero_usage_from_name(text_value(r["zero_usage"], path + ".zero_usage")),
      )
    }
    "package" => {
      let r = fields(value, ["kind", "plan"], path)
      PackageTariff(package_plan_from_json(r["plan"]))
    }
    _ => {
      invalid("kind", path, "only metered and package tariffs are supported")
      abort("unreachable")
    }
  }
}

///|
pub fn encode_catalog(catalog : Catalog) -> String raise PricingError {
  let entries : Array[Json] = catalog.entries.map(d => {
    "starts_on": d.entry.starts_on,
    "ends_before": match d.entry.ends_before {
      Some(s) => Json::string(s)
      None => Json::null()
    },
    "tariff": tariff_json(d.entry.tariff),
  })
  let json : Json = { "schema": "jieheng.catalog.v1", "entries": entries }
  bounded_json(json)
}

///|
pub fn decode_catalog(text : String) -> Catalog raise PricingError {
  let r = fields(parse_document(text), ["schema", "entries"], "catalog")
  expect_value(r, "schema", "jieheng.catalog.v1", "catalog")
  let entries : Array[CatalogEntry] = []
  for i, value in bounded_items(r["entries"], 512, "catalog.entries") {
    let path = "catalog.entries[\{i}]"
    let e = fields(value, ["starts_on", "ends_before", "tariff"], path)
    entries.push({
      starts_on: text_value(e["starts_on"], path + ".starts_on"),
      ends_before: match e["ends_before"] {
        Null => None
        v => Some(text_value(v, path + ".ends_before"))
      },
      tariff: tariff_from_json(e["tariff"], path + ".tariff"),
    })
  }
  compile_catalog(entries[:])
}

///|
pub fn encode_usage_bundle(
  usages : ArrayView[Usage],
) -> String raise PricingError {
  check_usage_bundle(usages)
  let json : Json = {
    "schema": "jieheng.usage-bundle.v1",
    "usages": usages.map(usage_json),
  }
  bounded_json(json)
}

///|
pub fn decode_usage_bundle(text : String) -> Array[Usage] raise PricingError {
  let r = fields(parse_document(text), ["schema", "usages"], "usages")
  expect_value(r, "schema", "jieheng.usage-bundle.v1", "usages")
  let usages = bounded_items(r["usages"], 64, "usages").map(usage_from_json)
  check_usage_bundle(usages[:])
  usages
}

///|
pub fn encode_catalog_bill(record : CatalogBill) -> String raise PricingError {
  ignore(date_key(record.on, "catalog.on"))
  let json : Json = {
    "schema": "jieheng.catalog-bill.v1",
    "on": record.on,
    "bill": bill_json(record.bill),
  }
  bounded_json(json)
}

///|
pub fn decode_catalog_bill(text : String) -> CatalogBill raise PricingError {
  let r = fields(parse_document(text), ["schema", "on", "bill"], "catalog")
  expect_value(r, "schema", "jieheng.catalog-bill.v1", "catalog")
  let on = text_value(r["on"], "catalog.on")
  ignore(date_key(on, "catalog.on"))
  { on, bill: bill_from_json(r["bill"]), }
}