///|
/// Infinite interval endpoints use explicit strings; JSON numbers remain finite.
fn endpoint_json(value : Double) -> Json {
  if value.is_pos_inf() {
    Json::string("+inf")
  } else if value == @double.neg_infinity {
    Json::string("-inf")
  } else {
    Json::number(value)
  }
}

///|
fn endpoint_from_json(value : Json) -> Double raise ConformalError {
  match value {
    String("+inf") => @double.infinity
    String("-inf") => @double.neg_infinity
    Number(number, ..) =>
      if finite(number) {
        number
      } else {
        raise Invalid("nonfinite JSON number")
      }
    _ =>
      raise Invalid(
        "interval endpoint must be finite or an explicit infinity string",
      )
  }
}

///|
/// Valid empty sets retain their endpoints; unbounded sets do not become null.
pub fn intervals_to_json(
  intervals : Array[Interval],
) -> String raise ConformalError {
  if intervals.is_empty() || intervals.length() > 100000 {
    raise ResourceLimit("interval export size outside 1..100000")
  }
  let rows = []
  for interval in intervals {
    validate_interval(interval)
    rows.push(
      Json::object(
        Map([
          ("lower", endpoint_json(interval.lower)),
          ("upper", endpoint_json(interval.upper)),
        ]),
      ),
    )
  }
  Json::object(
    Map([
      ("schema", Json::number(1.0)),
      ("kind", Json::string("intervals")),
      ("rows", Json::array(rows)),
    ]),
  ).stringify()
}

///|
/// Same bounded strict parser as calibration snapshots.
pub fn intervals_from_json(
  text : String,
) -> Array[Interval] raise ConformalError {
  let fields = snapshot_object(snapshot_parse(text), ["schema", "kind", "rows"])
  snapshot_version(fields, "intervals")
  match snapshot_field(fields, "rows") {
    Array(rows) => {
      if rows.is_empty() || rows.length() > 100000 {
        raise ResourceLimit("interval import size outside 1..100000")
      }
      let result = []
      for row in rows {
        let fields = snapshot_object(row, ["lower", "upper"])
        let interval = {
          lower: endpoint_from_json(snapshot_field(fields, "lower")),
          upper: endpoint_from_json(snapshot_field(fields, "upper")),
        }
        validate_interval(interval)
        result.push(interval)
      }
      result
    }
    _ => raise Invalid("interval rows must be an array")
  }
}

///|
/// Label sets have a schema and a fixed class vocabulary size.
pub fn sets_to_json(
  sets : Array[Array[Int32]],
  classes : Int32,
) -> String raise ConformalError {
  if classes < 2 || classes > 1024 {
    raise Invalid("invalid export class count")
  }
  if sets.is_empty() ||
    sets.length() > 100000 ||
    sets.length() > 2000000 / classes {
    raise ResourceLimit("set export exceeds supported budget")
  }
  let rows = []
  for labels in sets {
    validate_label_set(labels, classes)
    rows.push(Json::array(labels.map(label => Json::number(label.to_double()))))
  }
  Json::object(
    Map([
      ("schema", Json::number(1.0)),
      ("kind", Json::string("sets")),
      ("classes", Json::number(classes.to_double())),
      ("rows", Json::array(rows)),
    ]),
  ).stringify()
}

///|
/// Returns class vocabulary size with the validated prediction sets.
pub fn sets_from_json(
  text : String,
) -> (Int32, Array[Array[Int32]]) raise ConformalError {
  let fields = snapshot_object(snapshot_parse(text), [
    "schema", "kind", "classes", "rows",
  ])
  snapshot_version(fields, "sets")
  let classes = snapshot_integer(snapshot_field(fields, "classes"), 2, 1024)
  match snapshot_field(fields, "rows") {
    Array(rows) => {
      if rows.is_empty() ||
        rows.length() > 100000 ||
        rows.length() > 2000000 / classes {
        raise ResourceLimit("set import exceeds supported budget")
      }
      let result = []
      for row in rows {
        match row {
          Array(labels) => {
            if labels.length() > classes {
              raise Invalid("too many labels in imported set")
            }
            let labels = labels.map(label => {
              snapshot_integer(label, 0, classes - 1)
            })
            validate_label_set(labels, classes)
            result.push(labels)
          }
          _ => raise Invalid("each prediction set must be an array")
        }
      }
      (classes, result)
    }
    _ => raise Invalid("prediction sets must be an array")
  }
}