///|
fn field(fields : Map[String, Json], key : String) -> Json raise QueueError {
  match fields.get(key) {
    Some(value) => value
    None => raise Invalid("missing required field")
  }
}

///|
fn[T : @json.FromJson] decode(value : Json) -> T raise QueueError {
  @json.from_json(value) catch {
    _ => raise Invalid("wrong field type")
  }
}

///|
fn integer(value : Json) -> Int raise QueueError {
  let x : Double = decode(value)
  if x < -2147483648.0 || x > 2147483647.0 || x != x.to_int().to_double() {
    raise Invalid("expected exact 32-bit integer")
  }
  x.to_int()
}

///|
fn number(fields : Map[String, Json], key : String) -> Double raise QueueError {
  decode(field(fields, key))
}

///|
fn count(fields : Map[String, Json], key : String) -> Int raise QueueError {
  integer(field(fields, key))
}

///|
fn known(
  fields : Map[String, Json],
  names : Array[String],
) -> Unit raise QueueError {
  for key, _ in fields {
    if !names.contains(key) {
      raise Invalid("unknown operation field")
    }
  }
}

///|
fn mmc_fields(fields : Map[String, Json]) -> Mmc raise QueueError {
  {
    arrival: number(fields, "arrival"),
    service: number(fields, "service"),
    servers: count(fields, "servers"),
  }
}

///|
fn request(request : Json) -> Json raise QueueError {
  guard request is Object(fields) else {
    raise Invalid("request must be object")
  }
  let op : String = decode(field(fields, "op"))
  let result = Map([])
  result["ok"] = @json.to_json(true)
  result["operation"] = @json.to_json(op)
  match op {
    "mmc" => {
      known(fields, [
        "op", "arrival", "service", "servers", "time", "probability", "state",
      ])
      let q = mmc_fields(fields)
      result["metrics"] = @json.to_json(q.analyze())
      let time = number(fields, "time")
      result["wait_survival"] = @json.to_json(q.wait_survival(time))
      result["response_survival"] = @json.to_json(q.response_survival(time))
      let p = number(fields, "probability")
      result["wait_quantile"] = @json.to_json(q.wait_quantile(p))
      result["response_quantile"] = @json.to_json(q.response_quantile(p))
      let state = count(fields, "state")
      result["state_probability"] = @json.to_json(q.state_probability(state))
      result["state_tail"] = @json.to_json(q.state_tail(state))
    }
    "finite" => {
      known(fields, [
        "op", "arrival", "service", "servers", "capacity", "abandonment",
      ])
      let q : FiniteQueue = {
        arrival: number(fields, "arrival"),
        service: number(fields, "service"),
        servers: count(fields, "servers"),
        capacity: count(fields, "capacity"),
        abandonment: number(fields, "abandonment"),
      }
      result["metrics"] = @json.to_json(q.analyze())
      result["outcomes"] = @json.to_json(q.outcomes())
    }
    "engset" => {
      known(fields, ["op", "sources", "channels", "per_source", "service"])
      let q : Engset = {
        sources: count(fields, "sources"),
        channels: count(fields, "channels"),
        per_source: number(fields, "per_source"),
        service: number(fields, "service"),
      }
      result["metrics"] = @json.to_json(q.analyze())
    }
    "mg1" => {
      known(fields, ["op", "arrival", "mean_service", "second_moment"])
      result["metrics"] = @json.to_json(
        mg1(
          number(fields, "arrival"),
          number(fields, "mean_service"),
          number(fields, "second_moment"),
        ),
      )
    }
    "staffing" => {
      known(fields, [
        "op", "arrival", "service", "target", "time", "maximum_utilization", "maximum_servers",
      ])
      result["staffing"] = @json.to_json(
        sla_staffing(
          number(fields, "arrival"),
          number(fields, "service"),
          number(fields, "target"),
          number(fields, "time"),
          maximum_utilization=number(fields, "maximum_utilization"),
          maximum_servers=count(fields, "maximum_servers"),
        ),
      )
    }
    "loss" => {
      known(fields, ["op", "offered", "channels"])
      result["blocking"] = @json.to_json(
        erlang_b(number(fields, "offered"), count(fields, "channels")),
      )
    }
    _ => network_request(op, fields, result)
  }
  @json.to_json(result)
}

///|
fn failure(code : String) -> Json {
  @json.to_json({ "ok": @json.to_json(false), "code": @json.to_json(code) })
}

///|
fn json_budget(text : String) -> Unit raise QueueError {
  if text.length() > 1000000 {
    raise Limit("request exceeds one million code units")
  }
  let mut depth = 0
  let mut quoted = false
  let mut escaped = false
  for c in text.iter() {
    if quoted {
      if escaped {
        escaped = false
      } else if c == '\\' {
        escaped = true
      } else if c == '"' {
        quoted = false
      }
    } else if c == '"' {
      quoted = true
    } else if c == '[' || c == '{' {
      depth += 1
      if depth > 16 {
        raise Limit("request nesting exceeds sixteen")
      }
    } else if c == ']' || c == '}' {
      depth -= 1
    }
  }
}

///|
/// Bounded pure JSON transport. Errors do not echo supplied data. Duplicate
/// keys use core JSON's last-value behavior. CLI wraps this function verbatim.
pub fn process_json(text : String) -> String {
  let result = try {
    json_budget(text)
    let parsed = @json.parse(text) catch {
      _ => raise Invalid("malformed JSON")
    }
    request(parsed)
  } catch {
    Invalid(_) => failure("invalid")
    Numerical(_) => failure("numerical")
    Limit(_) => failure("limit")
    Unstable(_) => failure("unstable")
    Infeasible(_) => failure("infeasible")
  }
  result.stringify()
}