///|
fn field(request : Json, name : String) -> Json raise WellError {
  match request {
    Object(map) =>
      match map.get(name) {
        Some(value) => value
        None => {
          fail("missing field: " + name)
          Json::null()
        }
      }
    _ => {
      fail("request must be an object")
      Json::null()
    }
  }
}

///|
fn string_field(request : Json, name : String) -> String raise WellError {
  match field(request, name) {
    String(value) => value
    _ => {
      fail("expected string: " + name)
      ""
    }
  }
}

///|
fn number_field(request : Json, name : String) -> Double raise WellError {
  match field(request, name) {
    Number(value, ..) => {
      if !finite(value) {
        fail("nonfinite field: " + name)
      }
      value
    }
    _ => {
      fail("expected number: " + name)
      0.0
    }
  }
}

///|
fn array_field(request : Json, name : String) -> Array[Json] raise WellError {
  match field(request, name) {
    Array(values) => values
    _ => {
      fail("expected array: " + name)
      []
    }
  }
}

///|
fn request_grid(request : Json) -> Array[Double] raise WellError {
  depth_grid(
    number_field(request, "start"),
    number_field(request, "stop"),
    number_field(request, "step"),
  )
}

///|
fn check_fields(
  request : Json,
  allowed : Array[String],
) -> Unit raise WellError {
  match request {
    Object(map) =>
      for name, _ in map {
        if !allowed.contains(name) {
          fail("unknown request field: " + name)
        }
      }
    _ => fail("request must be object")
  }
}

///|
/// Stateless JSON protocol. All domain work is implemented here, not in the IO adapter.
pub fn dispatch(request : Json) -> Json raise WellError {
  let command = string_field(request, "command")
  match command {
    "validate-las2" => {
      check_fields(request, ["command", "las"])
      las2_report(read_las_document(string_field(request, "las")))
    }
    "strict-normalize" => {
      check_fields(request, ["command", "las"])
      Json::object({
        "las": Json::string(
          write_las_strict(parse_strict(string_field(request, "las"))),
        ),
      })
    }
    "archive-las2" => {
      check_fields(request, ["command", "las"])
      let document = read_las_document(string_field(request, "las"))
      Json::object({
        "las": Json::string(document_text(document)),
        "validation": las2_report(document),
      })
    }
    "qa" => {
      check_fields(request, ["command", "las", "max_gap"])
      quality_report(
        parse(string_field(request, "las")),
        [],
        number_field(request, "max_gap"),
      )
    }
    "normalize" => {
      check_fields(request, ["command", "las"])
      Json::object({
        "las": Json::string(
          write_las(ascending(parse(string_field(request, "las")))),
        ),
      })
    }
    "csv" => {
      check_fields(request, ["command", "las"])
      Json::object({
        "csv": Json::string(write_csv(parse(string_field(request, "las")))),
      })
    }
    "resample" => {
      check_fields(request, [
        "command", "las", "start", "stop", "step", "max_gap",
      ])
      let result = resample(
        parse(string_field(request, "las")),
        request_grid(request),
        number_field(request, "max_gap"),
      )
      Json::object({
        "las": Json::string(write_las(result)),
        "csv": Json::string(write_csv(result)),
      })
    }
    "interval" => {
      check_fields(request, [
        "command", "las", "curve", "start", "stop", "max_gap",
      ])
      interval_report(
        summarize_interval(
          parse(string_field(request, "las")),
          string_field(request, "curve"),
          number_field(request, "start"),
          number_field(request, "stop"),
          number_field(request, "max_gap"),
        ),
      )
    }
    "assemble" => {
      check_fields(request, [
        "command", "runs", "start", "stop", "step", "max_gap", "policy", "tolerance",
      ])
      let run_requests = array_field(request, "runs")
      if run_requests.length() == 0 || run_requests.length() > 64 {
        fail("invalid run count")
      }
      let runs : Array[Log] = []
      let names : Array[String] = []
      for run in run_requests {
        check_fields(run, ["name", "las"])
        names.push(string_field(run, "name"))
        runs.push(parse(string_field(run, "las")))
      }
      let policy = match string_field(request, "policy") {
        "reject" => Reject
        "first" => PreferFirst
        "last" => PreferLast
        _ => {
          fail("policy must be reject, first or last")
          Reject
        }
      }
      let result = assemble(
        runs,
        names,
        request_grid(request),
        number_field(request, "max_gap"),
        policy,
        tolerance=number_field(request, "tolerance"),
      )
      Json::object({
        "las": Json::string(write_las(result.log)),
        "csv": Json::string(write_csv(result.log)),
        "provenance_csv": Json::string(provenance_csv(result)),
        "assembly": assembly_report(result),
      })
    }
    _ => {
      fail("unknown command: " + command)
      Json::null()
    }
  }
}

///|
/// A request yields exactly one JSON envelope, including recoverable input errors.
pub fn execute(text : String) -> String {
  let response = try {
    let result = dispatch(read_report(text))
    Json::object({ "ok": Json::boolean(true), "result": result })
  } catch {
    WellError(message) =>
      Json::object({
        "ok": Json::boolean(false),
        "error": Json::string(message),
      })
  }
  response.stringify()
}