///|
/// A preflight observation about the supplied bindings. Missing variables
/// are informational: RFC 6570 deliberately permits undefined variables.
pub(all) enum BindingIssue {
  Missing(String)
  Unused(String)
  Duplicate(String)
  CompositePrefix(String)
}

///|
pub struct BindingReport {
  issues : Array[BindingIssue]
}

///|
pub fn BindingReport::issues(self : BindingReport) -> Array[BindingIssue] {
  self.issues.copy()
}

///|
pub fn BindingReport::is_clean(self : BindingReport) -> Bool {
  self.issues.is_empty()
}

///|
pub fn BindingReport::missing_count(self : BindingReport) -> Int {
  let mut count = 0
  for issue in self.issues {
    if issue is Missing(_) {
      count += 1
    }
  }
  count
}

///|
pub fn BindingIssue::code(self : BindingIssue) -> String {
  match self {
    Missing(_) => "missing"
    Unused(_) => "unused"
    Duplicate(_) => "duplicate"
    CompositePrefix(_) => "composite_prefix"
  }
}

///|
pub fn BindingIssue::name(self : BindingIssue) -> String {
  match self {
    Missing(name) | Unused(name) | Duplicate(name) | CompositePrefix(name) =>
      name
  }
}

///|
/// Inspect variable presence, duplicate bindings, and prefix/type conflicts
/// without expanding or exposing any bound value in the report.
pub fn Template::inspect_bindings(
  self : Template,
  bindings : Array[(String, Value)],
) -> BindingReport {
  let issues : Array[BindingIssue] = []
  let required : Map[String, Bool] = Map([])
  for name in self.variables {
    required[name] = true
  }
  let supplied : Map[String, Value] = Map([])
  for binding in bindings {
    let (name, value) = binding
    if supplied.contains(name) {
      issues.push(Duplicate(name))
    } else {
      supplied[name] = value
      if !required.contains(name) {
        issues.push(Unused(name))
      }
    }
  }
  for name in self.variables {
    if !supplied.contains(name) {
      issues.push(Missing(name))
    }
  }
  let reported : Map[String, Bool] = Map([])
  for segment in self.segments {
    match segment {
      Literal(_) => ()
      Expression(_, specs, _, _) =>
        for spec in specs {
          if spec.prefix is Some(_) && !reported.contains(spec.name) {
            match supplied.get(spec.name) {
              Some(Scalar(_)) | None => ()
              Some(_) => {
                issues.push(CompositePrefix(spec.name))
                reported[spec.name] = true
              }
            }
          }
        }
    }
  }
  { issues, }
}