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

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

///|
fn request_fields(
  fields : Map[String, Json],
  allowed : Array[String],
) -> Unit raise CodaError {
  for key, _ in fields {
    let mut found = false
    for name in allowed {
      if key == name {
        found = true
      }
    }
    if !found {
      raise Invalid("unknown field for selected operation")
    }
  }
}

///|
fn execute_request(request : Json) -> Json raise CodaError {
  guard request is Object(fields) else {
    raise Invalid("request must be an object")
  }
  let op : String = decode(required(fields, "op"))
  let allowed = ["op", "features", "samples", "rows"]
  match op {
    "analyze" => ()
    "pca" => allowed.push("components")
    "cluster" => {
      allowed.push("clusters")
      allowed.push("max_iterations")
    }
    "score" => allowed.push("ridge")
    _ => raise Invalid("unknown operation")
  }
  request_fields(fields, allowed)
  let features : Array[String] = decode(required(fields, "features"))
  let samples : Array[String] = decode(required(fields, "samples"))
  let rows : Array[Array[Double]] = decode(required(fields, "rows"))
  let data = dataset(features, samples, rows)
  let result = Map([])
  result["ok"] = @json.to_json(true)
  result["operation"] = @json.to_json(op)
  result["features"] = @json.to_json(data.feature_names())
  result["samples"] = @json.to_json(data.sample_names())
  match op {
    "analyze" => {
      result["center"] = @json.to_json(center(data))
      result["clr"] = @json.to_json(data.clr_rows())
      result["ilr"] = @json.to_json(
        data.ilr_rows(helmert_basis(data.part_count())),
      )
      result["variation"] = @json.to_json(variation_matrix(data))
      result["total_variance"] = @json.to_json(total_variance(data))
    }
    "pca" => {
      let components : Int = request_int(required(fields, "components"))
      let model = pca(data, components)
      let scores = model.transform(data)
      result["scores"] = @json.to_json(scores)
      result["reconstructed"] = @json.to_json(model.reconstruct(scores))
      result["eigenvalues"] = @json.to_json(model.eigenvalues())
      result["explained_ratio"] = @json.to_json(model.explained_ratio())
      result["loadings_ilr"] = @json.to_json(model.loadings())
      result["center"] = @json.to_json(model.center())
    }
    "cluster" => {
      let count : Int = request_int(required(fields, "clusters"))
      let limit : Int = match fields.get("max_iterations") {
        Some(v) => request_int(v)
        None => 100
      }
      let model = kmeans(data, count, max_iterations=limit)
      result["assignments"] = @json.to_json(model.assignments())
      result["centers"] = @json.to_json(model.centers())
      result["counts"] = @json.to_json(model.counts())
      result["inertia"] = @json.to_json(model.inertia())
      result["iterations"] = @json.to_json(model.iterations())
      result["converged"] = @json.to_json(model.converged())
    }
    "score" => {
      let ridge : Double = decode(required(fields, "ridge"))
      let model = gaussian(data, ridge)
      result["squared_mahalanobis"] = @json.to_json(
        model.squared_mahalanobis(data),
      )
      result["negative_log_density_ilr"] = @json.to_json(
        model.negative_log_density(data),
      )
      result["rank"] = @json.to_json(model.rank(data))
      result["ridge"] = @json.to_json(model.ridge())
      result["center"] = @json.to_json(model.center())
    }
    _ => raise Invalid("unknown operation")
  }
  @json.to_json(result)
}

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

///|
fn nesting_budget(text : String) -> Unit raise CodaError {
  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 Budget("JSON nesting exceeds 16")
      }
    } else if c == ']' || c == '}' {
      depth -= 1
    }
  }
}

///|
/// Pure bounded JSON interface. Errors never echo user-provided input or labels.
/// JSON duplicate keys follow the core parser's last-value semantics.
pub fn process_json(text : String) -> String {
  let response = if text.length() > 1000000 {
    protocol_error("budget", "request exceeds one million code units")
  } else {
    try {
      nesting_budget(text)
      let parsed = @json.parse(text) catch {
        _ => raise Invalid("invalid JSON or nesting exceeds 16")
      }
      execute_request(parsed)
    } catch {
      Invalid(message) => protocol_error("invalid", message)
      Shape(message) => protocol_error("shape", message)
      Budget(message) => protocol_error("budget", message)
      Numerical(message) => protocol_error("numerical", message)
    }
  }
  response.stringify()
}

///|
fn request_int(value : Json) -> Int raise CodaError {
  let n : Double = decode(value)
  if !(n >= -1000000.0 && n <= 1000000.0) {
    raise Invalid("integer outside request range")
  }
  let result = n.to_int()
  if result.to_double() != n {
    raise Invalid("fractional integer request field")
  }
  result
}