///|
/// Strict positional JSON array. No object keys can be silently overwritten.
/// Decimal strings avoid JS's floating-point integer precision limit.
pub fn Session::checkpoint(self : Session) -> String raise LedgerError {
  if self.recovering {
    raise RecoveryRequired
  }
  let records : Array[Json] = []
  for i, block in self.blocks {
    if block is Confirmed(_, hash) {
      records.push(
        Json::array([Json::string(i.to_string()), Json::string(hash)]),
      )
    }
  }
  Json::array([
    Json::string("MFL1"),
    Json::string(self.spec.resource),
    Json::string(self.spec.length.to_string()),
    Json::string(self.spec.etag),
    Json::string(self.spec.sha256),
    Json::string(self.chunk_size.to_string()),
    Json::array(records),
  ]).stringify()
}

///|
fn decimal(text : String) -> Int64 raise LedgerError {
  if text.is_empty() ||
    text.length() > 19 ||
    (text.length() > 1 && text[0] == '0') {
    raise InvalidCheckpoint("noncanonical decimal")
  }
  for c in text {
    if c < '0' || c > '9' {
      raise InvalidCheckpoint("expected decimal digits")
    }
  }
  @strconv.parse_int64(text) catch {
    _ => raise InvalidCheckpoint("decimal exceeds Int64")
  }
}

///|
/// Exact schema and object identity required. All recorded coverage starts
/// untrusted, reservations are discarded, and old capabilities cannot be reused.
pub fn restore(
  checkpoint : String,
  expected : ObjectSpec,
) -> Session raise LedgerError {
  if checkpoint.length() > 16000000 {
    raise InvalidCheckpoint("checkpoint exceeds 16M code units")
  }
  let value = @json.parse(checkpoint) catch {
    _ => raise InvalidCheckpoint("malformed JSON")
  }
  let (resource, length, etag, sha256, size, records) = match value {
    [
      String("MFL1"),
      String(resource),
      String(length),
      String(etag),
      String(sha256),
      String(size),
      Array(records),
    ] => (resource, length, etag, sha256, size, records)
    _ =>
      raise InvalidCheckpoint("unsupported version, field type or extra field")
  }
  let spec = ObjectSpec::new(resource, decimal(length), etag, sha256) catch {
    _ => raise InvalidCheckpoint("invalid object specification")
  }
  if spec != expected {
    raise InvalidCheckpoint(
      "object identity mismatch; retain input and restart explicitly",
    )
  }
  let chunk_size = decimal(size)
  if chunk_size > 4194304L {
    raise InvalidCheckpoint("unsupported chunk size")
  }
  let session = Session::new(spec, chunk_size=chunk_size.to_int()) catch {
    _ => raise InvalidCheckpoint("invalid block limits")
  }
  let mut previous = -1L
  for record in records {
    let (index, hash) = match record {
      [String(index), String(hash)] => (decimal(index), hash)
      _ => raise InvalidCheckpoint("invalid block record or extra field")
    }
    if index <= previous ||
      index >= session.blocks.length().to_int64() ||
      !valid_digest(hash) {
      raise InvalidCheckpoint(
        "duplicate, unsorted, out-of-bounds block or invalid digest",
      )
    }
    session.blocks[index.to_int()] = Unverified(hash)
    previous = index
  }
  session.recovering = true
  session
}