///|
fn catalog_change_name(change : CatalogChange) -> String {
  match change {
    MetricAdded => "metric-added"
    MetricRemoved => "metric-removed"
    BaselineGap => "baseline-gap"
    CandidateGap => "candidate-gap"
    CurrencyChanged => "currency-changed"
    UnitChanged => "unit-changed"
    IdentityChanged => "identity-changed"
    VersionChanged => "version-changed"
    ModelChanged => "model-changed"
    PricingChanged => "pricing-changed"
    MinimumChanged => "minimum-changed"
    ZeroPolicyChanged => "zero-policy-changed"
  }
}

///|
fn catalog_change_from_name(name : String) -> CatalogChange raise PricingError {
  match name {
    "metric-added" => MetricAdded
    "metric-removed" => MetricRemoved
    "baseline-gap" => BaselineGap
    "candidate-gap" => CandidateGap
    "currency-changed" => CurrencyChanged
    "unit-changed" => UnitChanged
    "identity-changed" => IdentityChanged
    "version-changed" => VersionChanged
    "model-changed" => ModelChanged
    "pricing-changed" => PricingChanged
    "minimum-changed" => MinimumChanged
    "zero-policy-changed" => ZeroPolicyChanged
    _ => {
      invalid(
        "unsupported", "migration.changes", "unknown migration change code",
      )
      abort("unreachable")
    }
  }
}

///|
fn migration_index_json(index : Int32?) -> Json {
  match index {
    Some(i) => Json(i)
    None => Json::null()
  }
}

///|
pub fn encode_catalog_migration(
  report : CatalogMigration,
) -> String raise PricingError {
  migration_window(report.starts_on, report.ends_before)
  if report.spans.length() == 0 || report.spans.length() > 4096 {
    invalid("size", "migration.spans", "expected 1 to 4096 spans")
  }
  let spans : Array[Json] = []
  for s in report.spans {
    for index in [s.baseline_entry, s.candidate_entry] {
      if index is Some(i) && (i < 0 || i > 511) {
        invalid(
          "index", "migration.spans", "entry indices must be 0..511 or null",
        )
      }
    }
    let changes : Array[Json] = s.changes.map(c => {
      Json::string(catalog_change_name(c))
    })
    if changes.length() > 12 {
      invalid("size", "migration.changes", "maximum 12 change codes")
    }
    spans.push({
      "metric": s.metric,
      "starts_on": s.starts_on,
      "ends_before": s.ends_before,
      "baseline_entry": migration_index_json(s.baseline_entry),
      "candidate_entry": migration_index_json(s.candidate_entry),
      "changes": changes,
    })
  }
  bounded_json({
    "schema": "jieheng.catalog-migration.v1",
    "starts_on": report.starts_on,
    "ends_before": report.ends_before,
    "spans": spans,
  })
}

///|
/// Structural decoding only; trusted catalogs are required for verification.
pub fn decode_catalog_migration(
  text : String,
) -> CatalogMigration raise PricingError {
  let r = fields(
    parse_document(text),
    ["schema", "starts_on", "ends_before", "spans"],
    "migration",
  )
  expect_value(r, "schema", "jieheng.catalog-migration.v1", "migration")
  let start = text_value(r["starts_on"], "migration.starts_on")
  let end = text_value(r["ends_before"], "migration.ends_before")
  migration_window(start, end)
  let spans : Array[MigrationSpan] = []
  for i, v in bounded_items(r["spans"], 4096, "migration.spans") {
    let path = "migration.spans[\{i}]"
    let s = fields(
      v,
      [
        "metric", "starts_on", "ends_before", "baseline_entry", "candidate_entry",
        "changes",
      ],
      path,
    )
    let index = (v : Json) => {
      match v {
        Null => None
        _ => Some(small_integer(v, 511, path + ".entry"))
      }
    }
    guard s["changes"] is Array(changes) else {
      invalid("type", path + ".changes", "expected an array")
      abort("unreachable")
    }
    if changes.length() > 12 {
      invalid("size", path + ".changes", "maximum 12 change codes")
    }
    spans.push({
      metric: text_value(s["metric"], path + ".metric"),
      starts_on: text_value(s["starts_on"], path + ".starts_on"),
      ends_before: text_value(s["ends_before"], path + ".ends_before"),
      baseline_entry: index(s["baseline_entry"]),
      candidate_entry: index(s["candidate_entry"]),
      changes: changes.map(c => {
        catalog_change_from_name(text_value(c, path + ".changes"))
      }),
    })
  }
  { starts_on: start, ends_before: end, spans, }
}