///|
/// Fact validation against an engine schema.
///
/// The validate operation checks real fact records against the same
/// dotted-path schema the check operation uses for rules: each typed
/// path present in a record must carry the declared type, and paths
/// listed under `required` must exist. Absent unrequired paths are
/// fine — check defers them to runtime, and validate treats them as
/// optional. Errors carry the path, the expected type and the found
/// type (or `missing`), per record, with the batch never aborting.

///|
fn validate_record(
  fact : Value,
  schema : Map[String, String],
  required : Map[String, Bool],
) -> Array[Value] {
  let errors : Array[Value] = []
  let paths : Array[String] = []
  for path, _ in schema {
    paths.push(path)
  }
  for path, _ in required {
    if !schema.contains(path) {
      paths.push(path)
    }
  }
  paths.sort_by(fn(a, b) { stable_string_compare(a, b) })
  for path in paths {
    let found = pick_at(fact, split_path(path))
    match found {
      None =>
        if required.contains(path) {
          errors.push(
            record([
              ("path", String(path)),
              ("expected", String(schema.get(path).unwrap_or("value"))),
              ("found", String("missing")),
              ("error", String("missing required path")),
            ]),
          )
        }
      Some(value) =>
        match schema.get(path) {
          Some(expected) =>
            if type_name(value) != expected {
              errors.push(
                record([
                  ("path", String(path)),
                  ("expected", String(expected)),
                  ("found", String(type_name(value))),
                  ("error", String("type mismatch")),
                ]),
              )
            }
          None => ()
        }
    }
  }
  errors
}

///|
fn validate_request(request : Value) -> Value raise {
  let schema : Map[String, String] = Map([])
  match get(request, "schema") {
    Null => ()
    Object(fields) =>
      for key, value in fields {
        let kind = str(value)
        if !known_types.contains(kind) {
          raise input_error(
            "E3008",
            "unknown schema type for " + key + ": " + kind,
          )
        }
        schema[key] = kind
      }
    _ => raise input_error("E3009", "schema must be an object")
  }
  let required : Map[String, Bool] = Map([])
  match get(request, "required") {
    Null => ()
    listed =>
      for item in arr(listed) {
        let path = str(item)
        required[path] = true
      }
  }
  let facts = match get(request, "facts") {
    Array(items) => items
    other => [other]
  }
  let results : Array[Value] = []
  let mut error_records = 0
  let mut error_count = 0
  for i = 0; i < facts.length(); i = i + 1 {
    let errors = validate_record(facts[i], schema, required)
    let ok = errors.is_empty()
    if !ok {
      error_records += 1
      error_count += errors.length()
    }
    results.push(
      record([
        ("index", Number(i.to_double())),
        ("ok", Bool(ok)),
        ("errors", Array(errors)),
      ]),
    )
  }
  record([
    ("results", Array(results)),
    ("records", Number(facts.length().to_double())),
    ("error_records", Number(error_records.to_double())),
    ("error_count", Number(error_count.to_double())),
  ])
}