///|
pub(all) struct ReplayRequest {
  study : AliquotStudy
  parameters : Array[ParameterValue]
} derive(Debug, ToJson, FromJson)

///|
pub extend ReplayRequest with @debug.Debug::{to_repr}

///|
pub extend ReplayRequest with @json.ToJson::{to_json}

///|
pub extend ReplayRequest with @json.FromJson::{from_json}

///|
// Deriving FromJson checks required types. Also reject unknown fields so that
// e.g. sulfate is never silently dropped from this deliberately narrow model.
fn known_fields(original : Json, decoded : Json) -> Bool {
  match (original, decoded) {
    (Object(a), Object(b)) => {
      for key, value in a {
        match b.get(key) {
          Some(other) => if !known_fields(value, other) { return false }
          None => return false
        }
      }
      true
    }
    (Array(a), Array(b)) => {
      if a.length() != b.length() {
        return false
      }
      for i = 0; i < a.length(); i = i + 1 {
        if !known_fields(a[i], b[i]) {
          return false
        }
      }
      true
    }
    _ => original == decoded
  }
}

///|
fn rejected_json(message : String) -> String {
  let payload : Json = { "ok": false, "error": message }
  payload.stringify()
}

///|
pub fn reconcile_json(input : String) -> String {
  try {
    let value = @json.parse(input)
    let study : AliquotStudy = @json.from_json(value)
    if !known_fields(value, @json.to_json(study)) {
      return rejected_json(
        "unknown field or noninteger value; untracked ions/processes are not supported",
      )
    }
    match reconcile_aliquots(study) {
      Ok(result) => {
        let payload : Json = { "ok": true, "result": @json.to_json(result) }
        payload.stringify()
      }
      Err(error) => rejected_json(error)
    }
  } catch {
    _ =>
      rejected_json(
        "invalid AliquotStudy JSON; integer units and every required field are mandatory",
      )
  }
}

///|
pub fn challenge_json(input : String) -> String {
  try {
    let value = @json.parse(input)
    let claim : ZeroTransferClaim = @json.from_json(value)
    if !known_fields(value, @json.to_json(claim)) {
      return rejected_json("unknown field or noninteger value in claim/study")
    }
    match challenge_zero_transfer(claim) {
      Ok(result) => {
        let payload : Json = { "ok": true, "result": @json.to_json(result) }
        payload.stringify()
      }
      Err(error) => rejected_json(error)
    }
  } catch {
    _ => rejected_json("invalid ZeroTransferClaim JSON")
  }
}

///|
pub fn replay_json(input : String) -> String {
  try {
    let value = @json.parse(input)
    let request : ReplayRequest = @json.from_json(value)
    if !known_fields(value, @json.to_json(request)) {
      return rejected_json("unknown field or noninteger value in replay/study")
    }
    match replay_handling(request.study, request.parameters) {
      Ok(result) => {
        let payload : Json = { "ok": true, "result": @json.to_json(result) }
        payload.stringify()
      }
      Err(error) => rejected_json(error)
    }
  } catch {
    _ => rejected_json("invalid ReplayRequest JSON")
  }
}