///|
/// Validated version-one request. Parsing never constructs private models unchecked.
pub struct Request {
  dna : Dna
  enzymes : Array[Enzyme]
  policy : AmbiguityPolicy
} derive(Debug)

///|
fn json_object(
  value : Json,
  keys : Array[String],
) -> Map[String, Json] raise RestrictError {
  match value {
    Object(fields) => {
      for key in fields.keys() {
        if !keys.contains(key) {
          raise InvalidInput("JSON_UNKNOWN_FIELD")
        }
      }
      fields
    }
    _ => raise InvalidInput("JSON_OBJECT")
  }
}

///|
fn required(
  fields : Map[String, Json],
  key : String,
) -> Json raise RestrictError {
  match fields.get(key) {
    Some(value) => value
    None => raise InvalidInput("JSON_REQUIRED_" + key)
  }
}

///|
fn json_string(value : Json) -> String raise RestrictError {
  match value {
    String(s) => s
    _ => raise InvalidInput("JSON_STRING")
  }
}

///|
fn json_int(value : Json) -> Int raise RestrictError {
  match value {
    Number(n, ..) => {
      if n < -2147483648.0 ||
        n > 2147483647.0 ||
        n != n ||
        n.to_int().to_double() != n {
        raise InvalidInput("JSON_INTEGER")
      }
      n.to_int()
    }
    _ => raise InvalidInput("JSON_INTEGER")
  }
}

///|
fn parse_json(text : String) -> Json raise RestrictError {
  if text.length() > 2000000 {
    raise LimitExceeded("JSON_BYTES")
  }
  let mut depth = 0
  let mut quoted = false
  let mut escaped = false
  for ch in text {
    if quoted {
      if escaped {
        escaped = false
      } else if ch == '\\' {
        escaped = true
      } else if ch == '"' {
        quoted = false
      }
    } else if ch == '"' {
      quoted = true
    } else if ch == '[' || ch == '{' {
      depth += 1
      if depth > 32 {
        raise LimitExceeded("JSON_DEPTH_32")
      }
    } else if ch == ']' || ch == '}' {
      depth -= 1
    }
  }
  @json.parse(text[:]) catch {
    _ => raise InvalidInput("JSON_SYNTAX")
  }
}

///|
/// Strict request schema; omitted policy defaults to certain-only recognition.
pub fn read_request(text : String) -> Request raise RestrictError {
  let fields = json_object(parse_json(text), [
    "version", "sequence", "topology", "enzymes", "policy",
  ])
  if json_int(required(fields, "version")) != 1 {
    raise InvalidInput("JSON_VERSION")
  }
  let topology = match json_string(required(fields, "topology")) {
    "linear" => Topology::Linear
    "circular" => Topology::Circular
    _ => raise InvalidInput("JSON_TOPOLOGY")
  }
  let policy = match fields.get("policy") {
    None => CertainOnly
    Some(String("certain")) => CertainOnly
    Some(String("possible")) => IncludePossible
    Some(String("reject")) => RejectPossible
    _ => raise InvalidInput("JSON_POLICY")
  }
  let dna = Dna::new(json_string(required(fields, "sequence")), topology~)
  let models = match required(fields, "enzymes") {
    Array(values) => values
    _ => raise InvalidInput("JSON_ENZYMES")
  }
  if models.length() > 32 {
    raise LimitExceeded("ENZYME_MAX_32")
  }
  let enzymes : Array[Enzyme] = []
  for model in models {
    let f = json_object(model, ["name", "motif", "top", "bottom"])
    enzymes.push(
      Enzyme::new(
        json_string(required(f, "name")),
        json_string(required(f, "motif")),
        json_int(required(f, "top")),
        json_int(required(f, "bottom")),
      ),
    )
  }
  { dna, enzymes, policy }
}

///|
pub fn Request::run(self : Request) -> Digest raise RestrictError {
  digest_many(self.dna, self.enzymes, policy=self.policy)
}

///|
pub fn Request::verify(
  self : Request,
  report : Digest,
) -> Unit raise RestrictError {
  if self.policy != report.policy {
    raise InvalidInput("VERIFY_POLICY")
  }
  verify_digest(self.dna, self.enzymes, report)
}

///|
/// Versioned report wrapper around explicit typed coordinate evidence.
pub fn write_report(report : Digest) -> String {
  let value : Json = {
    "version": 1,
    "kind": "moonrestrict.digest",
    "digest": report.to_json(),
  }
  value.stringify()
}

///|
/// Decode does not establish authenticity; Request::verify is mandatory for untrusted reports.
pub fn read_report(text : String) -> Digest raise RestrictError {
  let fields = json_object(parse_json(text), ["version", "kind", "digest"])
  if json_int(required(fields, "version")) != 1 ||
    json_string(required(fields, "kind")) != "moonrestrict.digest" {
    raise InvalidInput("REPORT_VERSION")
  }
  let encoded = required(fields, "digest")
  let report : Digest = @json.from_json(encoded) catch {
    _ => raise InvalidInput("REPORT_SCHEMA")
  }
  // Derived integer decoders truncate fractions; exact schema roundtrip rejects those and unknown fields.
  if report.to_json() != encoded {
    raise InvalidInput("REPORT_NONCANONICAL_SCHEMA")
  }
  report
}

///|
/// End-to-end strict JSON request -> computed and independently verified report.
pub fn run_json(text : String) -> String raise RestrictError {
  let request = read_request(text)
  let report = request.run()
  request.verify(report)
  let output = write_report(report)
  if output.length() > 2000000 {
    raise LimitExceeded("REPORT_BYTES")
  }
  output
}