///|
pub(all) struct ValidationProblem {
  path : String
  kind : String
  expected : String
  actual : String
} derive(Debug, Eq, ToJson)

///|
pub suberror ValidationError {
  ValidationFailed(Array[ValidationProblem])
} derive(Debug)

///|
fn type_name(value : Value) -> String {
  match value {
    Text(_) | Bare(_) => "string"
    Number(_) => "number"
    Boolean(_) => "boolean"
    Object(_) | SealedObject(_) => "object"
    List(_) => "list"
    Null => "null"
    _ => "unresolved"
  }
}

///|
fn validation_compatible(reference : Value, value : Value) -> Bool {
  match (reference, value) {
    (Null | Text("null") | Bare("null"), _)
    | (_, Null | Text("null") | Bare("null")) => true
    (Object(_) | SealedObject(_), Object(_) | SealedObject(_)) => true
    (Object(_) | SealedObject(_), _) => false
    (List(_), List(_) | Object(_) | SealedObject(_)) => true
    (List(_), _) => false
    (Text(_) | Bare(_), _) | (_, Text(_) | Bare(_)) => true
    _ => type_name(reference) == type_name(value)
  }
}

///|
fn validation_problem(
  parts : Array[String],
  kind : String,
  reference : Value,
  actual : String,
) -> ValidationProblem raise ParseError {
  { path: join_path(parts), kind, expected: type_name(reference), actual, }
}

///|
fn validate_pair(
  reference : Value,
  value : Value,
  parts : Array[String],
  problems : Array[ValidationProblem],
  work : Work,
) -> Unit raise ParseError {
  work.spend(1)
  if !validation_compatible(reference, value) {
    problems.push(
      validation_problem(parts, "wrong-type", reference, type_name(value)),
    )
    return
  }
  match (reference, value) {
    (
      Object(expected)
      | SealedObject(expected),
      Object(actual)
      | SealedObject(actual),
    ) =>
      for key, child in expected {
        let next = parts.copy()
        next.push(key)
        match actual.get(key) {
          None =>
            problems.push(validation_problem(next, "missing", child, "missing"))
          Some(v) => validate_pair(child, v, next, problems, work)
        }
      }
    (List(expected), List(_) | Object(_) | SealedObject(_)) => {
      let actual = get_as_list(Object({ "value": value }), "value") catch {
        _ => {
          problems.push(
            validation_problem(parts, "wrong-type", reference, type_name(value)),
          )
          return
        }
      }
      if !expected.is_empty() {
        for item in actual {
          work.spend(1)
          if !validation_compatible(expected[0], item) {
            problems.push(
              validation_problem(
                parts,
                "list-element",
                expected[0],
                type_name(item),
              ),
            )
            break
          }
        }
      }
    }
    _ => ()
  }
}

///|
/// Collect all missing/type problems, retaining duplicate restricted paths.
/// Like checkValid, this is a structural check, not application-level validation.
pub fn validation_problems(
  config : Value,
  reference : Value,
  paths? : Array[String] = [],
) -> Array[ValidationProblem] raise ParseError {
  ignore(object_fields(config))
  ignore(object_fields(reference))
  if !is_resolved(reference) {
    raise Invalid("validation reference is unresolved")
  }
  if !is_resolved(config) {
    raise Invalid("configuration to validate is unresolved")
  }
  let result = []
  let work = tree_work()
  if paths.is_empty() {
    validate_pair(reference, config, [], result, work)
  } else {
    for path in paths {
      work.spend(path.length() + 1)
      let parts = split_path(path)
      match get_path(reference, parts) {
        None => ()
        Some(expected) =>
          match get_path(config, parts) {
            None =>
              result.push(
                validation_problem(parts, "missing", expected, "missing"),
              )
            Some(actual) => validate_pair(expected, actual, parts, result, work)
          }
      }
    }
  }
  result
}

///|
pub fn check_valid(
  config : Value,
  reference : Value,
  paths? : Array[String] = [],
) -> Unit raise {
  let problems = validation_problems(config, reference, paths~)
  if !problems.is_empty() {
    raise ValidationFailed(problems)
  }
}