///|
fn text_field(
  fields : Map[String, Json],
  key : String,
) -> String raise PolicyError {
  match fields.get(key) {
    Some(String(s)) => s
    _ => raise Invalid("missing or non-string field: " + key)
  }
}

///|
fn execute(request : Json) -> (Int, Json) raise PolicyError {
  guard request is Object(fields) else {
    raise Invalid("request must be an object")
  }
  let command = text_field(fields, "command")
  let allowed = match command {
    "help" => ["command"]
    "diff" => ["command", "before", "after", "scope", "max_nodes"]
    "audit" | "format" => ["command", "policy", "max_nodes"]
    "check" => ["command", "policy", "assertions", "max_nodes"]
    _ => raise Invalid("unknown command")
  }
  for key, _ in fields {
    if !allowed.contains(key) {
      raise Invalid("unknown field: " + key)
    }
  }
  let max_nodes = match fields.get("max_nodes") {
    None => 100000
    Some(Number(n, ..)) => {
      if n < 2.0 || n > 200000.0 || n != n.to_int().to_double() {
        raise Invalid("max_nodes must be an integer in 2..200000")
      }
      n.to_int()
    }
    _ => raise Invalid("max_nodes must be numeric")
  }
  match command {
    "help" =>
      (
        0,
        {
          "schema": "moonpolicyproof.help.v1",
          "usage": "diff BEFORE AFTER [SCOPE] | audit POLICY | check POLICY ASSERTIONS | format POLICY; diff/check exit 1 on changes/failures, 2 invalid input, 3 resource exhaustion, 4 internal verification failure",
        },
      )
    "format" =>
      (
        0,
        {
          "schema": "moonpolicyproof.format.v1",
          "policy": Policy::parse(text_field(fields, "policy")).render(),
        },
      )
    "audit" =>
      (
        0,
        audit_json(
          Policy::parse(text_field(fields, "policy")).audit(max_nodes~),
        ),
      )
    "check" => {
      let results = Policy::parse(text_field(fields, "policy")).check_assertions(
        parse_assertions(text_field(fields, "assertions")),
        max_nodes~,
      )
      (
        if results.iter().all(r => r.passed) {
          0
        } else {
          1
        },
        assertions_json(results),
      )
    }
    "diff" => {
      let before = Policy::parse(text_field(fields, "before"))
      let after = Policy::parse(text_field(fields, "after"))
      let scope = match fields.get("scope") {
        None => None
        Some(String(s)) => Some(Scope::parse(s))
        _ => raise Invalid("scope must be a string")
      }
      let diff = match scope {
        None => compare(before, after, max_nodes~)
        Some(s) => compare_scoped(before, after, s, max_nodes~)
      }
      // Scope is included in the envelope, so scoped equality is never presented as global equality.
      let body : Json = {
        "schema": "moonpolicyproof.comparison.v1",
        "scope": match scope {
          None => Json::null()
          Some(s) => Json::string(s.render())
        },
        "result": diff.to_json(),
      }
      (if diff.equivalent() { 0 } else { 1 }, body)
    }
    _ => raise Invalid("unknown command")
  }
}

///|
/// Host-neutral bounded JSON entry point. No partial success survives an error.
pub fn handle_request(text : String) -> (Int, String) {
  if text.length() > 400000 {
    return (
      2,
      (
        {
          "schema": "moonpolicyproof.error.v1",
          "category": "invalid",
          "message": "request too large",
        } : Json).stringify(),
    )
  }
  let request = parse_request_json(text) catch {
    _ =>
      return (
        2,
        (
          {
            "schema": "moonpolicyproof.error.v1",
            "category": "invalid",
            "message": "invalid JSON or nesting depth",
          } : Json).stringify(),
      )
  }
  try {
    let (code, body) = execute(request)
    (code, body.stringify())
  } catch {
    Invalid(message) =>
      (
        2,
        (
          {
            "schema": "moonpolicyproof.error.v1",
            "category": "invalid",
            "message": message,
          } : Json).stringify(),
      )
    Internal(message) =>
      (
        4,
        (
          {
            "schema": "moonpolicyproof.error.v1",
            "category": "internal",
            "message": message,
          } : Json).stringify(),
      )
    Resource(message) =>
      (
        3,
        (
          {
            "schema": "moonpolicyproof.error.v1",
            "category": "resource",
            "message": message,
          } : Json).stringify(),
      )
  }
}