///|
/// A semantic/resource issue in a caller-constructed configuration. Empty path is root.
pub(all) struct ValidationIssue {
  path : String
  code : String
  message : String
} derive(Debug, Eq)

///|
priv struct Validator {
  limits : Limits
  issues : Array[ValidationIssue]
  mut nodes : Int32
  mut stopped : Bool
}

///| Check structure and limits without serialization. max_input is not an output budget here.

///|
/// Returns independent issues; traversal stops on the global node budget.
pub fn validate(
  root : Value,
  limits~ : Limits = Limits::default(),
) -> Array[ValidationIssue] raise ConfigError {
  ignore(Reader::new(b"", limits))
  let v : Validator = { limits, issues: [], nodes: 0, stopped: false, }
  match root {
    Group(items) => v.group(items, "", 0)
    _ =>
      v.issue("", "type_mismatch", "expected root group; actual " + root.kind())
  }
  v.issues
}

///|
fn member_path(parent : String, name : String) -> String {
  if parent == "" {
    name
  } else {
    parent + "." + name
  }
}

///|
fn Validator::issue(
  self : Validator,
  path : String,
  code : String,
  message : String,
) -> Unit {
  self.issues.push({ path, code, message, })
}

///|
fn Validator::group(
  self : Validator,
  items : Array[(String, Value)],
  path : String,
  depth : Int32,
) -> Unit {
  let names : Map[String, Bool] = Map([])
  for (name, value) in items {
    if self.stopped {
      break
    }
    let child = member_path(path, name)
    check_name(name) catch {
      ConfigError(d) => self.issue(child, d.code, d.message)
    }
    if names.contains(name) {
      self.issue(child, "duplicate_name", "duplicate setting: " + name)
    }
    names[name] = true
    self.visit(value, child, depth)
  }
}

///|
fn Validator::visit(
  self : Validator,
  value : Value,
  path : String,
  depth : Int32,
) -> Unit {
  if self.nodes >= self.limits.max_nodes {
    self.issue(path, "node_limit", "value count exceeds limit")
    self.stopped = true
    return
  }
  self.nodes += 1
  match value {
    Float(v) =>
      if v.is_inf() || v.is_nan() {
        self.issue(path, "non_finite_float", "expected finite float")
      }
    Text(bytes) =>
      if bytes.length() > self.limits.max_string {
        self.issue(path, "string_limit", "decoded string exceeds byte limit")
      } else {
        for b in bytes {
          if b == b'\x00' {
            self.issue(
              path, "unsupported_nul", "NUL is not representable by libconfig string APIs",
            )
            break
          }
        }
      }
    Group(items) =>
      if depth >= self.limits.max_depth {
        self.issue(path, "depth_limit", "container nesting exceeds limit")
      } else {
        self.group(items, path, depth + 1)
      }
    ScalarArray(items) | List(items) => {
      if depth >= self.limits.max_depth {
        self.issue(path, "depth_limit", "container nesting exceeds limit")
        return
      }
      for i = 0; i < items.length(); i = i + 1 {
        if self.stopped {
          break
        }
        let child = path + "[" + i.to_string() + "]"
        if value is ScalarArray(_) {
          match items[i] {
            Group(_) | ScalarArray(_) | List(_) =>
              self.issue(
                child,
                "array_type",
                "array element must be scalar; actual " + items[i].kind(),
              )
            _ => ()
          }
          if i > 0 && items[0].kind() != items[i].kind() {
            self.issue(
              child,
              "array_type",
              "expected " + items[0].kind() + "; actual " + items[i].kind(),
            )
          }
        }
        self.visit(items[i], child, depth + 1)
      }
    }
    _ => ()
  }
}