///|
pub(all) struct BindingIssue {
  name : String
  span : Span
} derive(Debug)

///|
/// Missing/omitted entries use first occurrence order; unused names are sorted.
pub(all) struct BindingReport {
  referenced : Array[String]
  missing : Array[BindingIssue]
  omitted : Array[BindingIssue]
  unused : Array[String]
  invalid : Array[TemplateError]
} derive(Debug)

///|
/// Construct a copied binding set, rejecting ambiguous duplicate names.
pub fn Variables::from_entries(
  entries : Array[(String, Value)],
) -> Result[Variables, TemplateError] {
  let vars = Variables::new()
  for (name, value) in entries {
    if vars.values.contains(name) {
      return Err(problem("DuplicateBinding", "duplicate binding name", 0, 0))
    }
    vars.set(name, value)
  }
  Ok(vars)
}

///|
/// Read-only preflight. Empty scalar is defined; empty collections are omitted.
/// Reports all incompatible prefix occurrences without expanding any values.
pub fn Template::check_bindings(
  self : Template,
  variables : Variables,
  limits? : ExpansionLimits,
) -> Result[BindingReport, TemplateError] {
  if limits is Some(budget) {
    match check_input_budget(variables, budget) {
      Err(e) => return Err(e)
      Ok(_) => ()
    }
  }
  Ok(self.binding_report(variables))
}

///|
fn Template::binding_report(
  self : Template,
  variables : Variables,
) -> BindingReport {
  let seen : Map[String, Bool] = Map([])
  let missing : Array[BindingIssue] = []
  let omitted : Array[BindingIssue] = []
  let invalid : Array[TemplateError] = []
  for segment in self.segments {
    if segment is Expression(_, specs) {
      for spec in specs {
        let first = !seen.contains(spec.name)
        seen[spec.name] = true
        match variables.values.get(spec.name) {
          None =>
            if first {
              missing.push({ name: spec.name, span: spec.span, })
            }
          Some(value) => {
            if !defined(value) && first {
              omitted.push({ name: spec.name, span: spec.span, })
            }
            if defined(value) &&
              spec.modifier is Prefix(_) &&
              !(value is Scalar(_)) {
              invalid.push(
                problem(
                  "PrefixOnComposite",
                  "prefix is only valid on scalar values",
                  spec.span.start,
                  spec.span.end,
                ),
              )
            }
          }
        }
      }
    }
  }
  let unused = variables.values
    .keys()
    .filter(name => !seen.contains(name))
    .to_array()
  unused.sort()
  { referenced: self.variables(), missing, omitted, unused, invalid, }
}

///|
pub extend BindingIssue with Debug::{to_repr}

///|
pub extend BindingReport with Debug::{to_repr}

///|
fn Template::validate_bindings(
  self : Template,
  variables : Variables,
  required : Array[String],
  reject_unused : Bool,
  limits : ExpansionLimits,
) -> Result[Int32, TemplateError] {
  let remaining_input = match check_input_budget(variables, limits) {
    Err(e) => return Err(e)
    Ok(remaining) => remaining
  }
  if required.is_empty() && !reject_unused {
    // No presence/unused report is requested. Keep the original first-prefix
    // error and input-before-prefix precedence without building report arrays.
    for segment in self.segments {
      if segment is Expression(_, specs) {
        for spec in specs {
          if spec.modifier is Prefix(_) &&
            variables.values.get(spec.name) is Some(value) &&
            defined(value) &&
            !(value is Scalar(_)) {
            return Err(
              problem(
                "PrefixOnComposite",
                "prefix is only valid on scalar values",
                spec.span.start,
                spec.span.end,
              ),
            )
          }
        }
      }
    }
    return Ok(remaining_input)
  }
  let report = self.binding_report(variables)
  let referenced : Map[String, Bool] = Map([])
  let absent : Map[String, BindingIssue] = Map([])
  let empty : Map[String, BindingIssue] = Map([])
  if !required.is_empty() {
    for name in report.referenced {
      referenced[name] = true
    }
    for issue in report.missing {
      absent[issue.name] = issue
    }
    for issue in report.omitted {
      empty[issue.name] = issue
    }
  }
  for name in required {
    if !referenced.contains(name) {
      return Err(
        problem(
          "UnknownRequired", "required name is not referenced by the template", 0,
          0,
        ),
      )
    }
    if absent.get(name) is Some(issue) {
      return Err(
        problem(
          "MissingBinding",
          "required variable is not bound",
          issue.span.start,
          issue.span.end,
        ),
      )
    }
    if empty.get(name) is Some(issue) {
      return Err(
        problem(
          "OmittedBinding",
          "required collection is empty and would be omitted",
          issue.span.start,
          issue.span.end,
        ),
      )
    }
  }
  if reject_unused && !report.unused.is_empty() {
    return Err(
      problem(
        "UnusedBinding", "bindings include names not referenced by the template",
        0, 0,
      ),
    )
  }
  if !report.invalid.is_empty() {
    return Err(report.invalid[0])
  }
  Ok(remaining_input)
}