///|
/// A bounded, versioned JSON transport; snapshots contain calibration scores only.
fn snapshot_parse(text : String) -> Json raise ConformalError {
  if text.length() > 4000000 {
    raise ResourceLimit("snapshot exceeds four million code units")
  }
  @json.parse(text, max_nesting_depth=12, reject_duplicate_keys=true) catch {
    _ => raise Invalid("invalid snapshot JSON")
  }
}

///|
fn snapshot_object(
  value : Json,
  keys : Array[String],
) -> Map[String, Json] raise ConformalError {
  match value {
    Object(fields) => {
      if fields.length() != keys.length() {
        raise Invalid("snapshot fields do not match schema")
      }
      for key in keys {
        if !fields.contains(key) {
          raise Invalid("missing snapshot field: " + key)
        }
      }
      fields
    }
    _ => raise Invalid("snapshot must be an object")
  }
}

///|
fn snapshot_field(
  fields : Map[String, Json],
  key : String,
) -> Json raise ConformalError {
  match fields.get(key) {
    Some(value) => value
    None => raise Invalid("missing snapshot field: " + key)
  }
}

///|
fn snapshot_number(value : Json) -> Double raise ConformalError {
  match value {
    Number(number, ..) =>
      if finite(number) {
        number
      } else {
        raise Invalid("nonfinite snapshot number")
      }
    _ => raise Invalid("expected snapshot number")
  }
}

///|
fn snapshot_scores(value : Json) -> Scores raise ConformalError {
  match value {
    Array(values) => {
      if values.length() > 100000 {
        raise ResourceLimit("snapshot score limit exceeded")
      }
      Scores::new(values.map(snapshot_number))
    }
    _ => raise Invalid("snapshot scores must be an array")
  }
}

///|
fn snapshot_version(
  fields : Map[String, Json],
  kind : String,
) -> Unit raise ConformalError {
  if snapshot_number(snapshot_field(fields, "schema")) != 1.0 {
    raise Invalid("unsupported snapshot schema")
  }
  match snapshot_field(fields, "kind") {
    String(value) => if value != kind { raise Invalid("wrong snapshot kind") }
    _ => raise Invalid("invalid snapshot kind")
  }
}

///|
/// Scores are finite, signed, and sorted; alpha is chosen at prediction time.
pub fn Scores::to_snapshot(self : Scores) -> String {
  Json::object(
    Map([
      ("schema", Json::number(1.0)),
      ("kind", Json::string("scores")),
      ("scores", Json::array(self.sorted.map(x => Json::number(x)))),
    ]),
  ).stringify()
}

///|
/// Rejects unknown fields, duplicate keys, nonfinite numbers and resource excess.
pub fn Scores::from_snapshot(text : String) -> Scores raise ConformalError {
  let fields = snapshot_object(snapshot_parse(text), [
    "schema", "kind", "scores",
  ])
  snapshot_version(fields, "scores")
  snapshot_scores(snapshot_field(fields, "scores"))
}