///|
/// Application value shapes; formatting numbers/booleans remains the caller's job.
pub(all) enum BindingKind {
  ScalarKind
  ListKind
  AssocKind
} derive(Eq, Debug)

///|
pub(all) struct BindingRule {
  name : String
  kind : BindingKind
} derive(Debug)

///|
/// Captures rules independently of caller-owned arrays. No bound values retained.
struct ContractTemplate {
  template : Template
  rules : Array[BindingRule]
  required : Array[String]
  required_sites : Array[BindingIssue]
  reject_unused : Bool
  spans : Map[String, Span]
}

///|
/// Compile shape constraints once, separately from RFC expansion semantics.
/// Prefix variables always require scalars, even for empty composite bindings.
pub fn Template::with_contract(
  self : Template,
  rules : Array[BindingRule],
  required~ : Array[String] = [],
  reject_unused~ : Bool = true,
  max_rules~ : Int32 = 4096,
  max_rule_codepoints~ : Int32 = 65536,
) -> Result[ContractTemplate, TemplateError] {
  if max_rules < 0 || max_rule_codepoints < 0 {
    return Err(problem("InvalidLimit", "negative contract budget", 0, 0))
  }
  if rules.length() > max_rules || required.length() > max_rules {
    return Err(problem("ContractLimit", "too many contract declarations", 0, 0))
  }
  let mut remaining = max_rule_codepoints
  for name in rules.iter().map(r => r.name).concat(required.iter()) {
    for _ in name.iter() {
      if remaining == 0 {
        return Err(
          problem("ContractLimit", "contract names exceed budget", 0, 0),
        )
      }
      remaining = remaining - 1
    }
  }
  let spans : Map[String, Span] = Map([])
  let prefixes : Map[String, Span] = Map([])
  for segment in self.segments {
    if segment is Expression(_, specs) {
      for spec in specs {
        if !spans.contains(spec.name) {
          spans[spec.name] = spec.span
        }
        if spec.modifier is Prefix(_) && !prefixes.contains(spec.name) {
          prefixes[spec.name] = spec.span
        }
      }
    }
  }
  let kinds : Map[String, BindingKind] = Map([])
  for rule in rules {
    if kinds.contains(rule.name) {
      return Err(problem("DuplicateRule", "duplicate type declaration", 0, 0))
    }
    let span = match spans.get(rule.name) {
      Some(span) => span
      None =>
        return Err(problem("UnknownRule", "type name is not referenced", 0, 0))
    }
    if prefixes.contains(rule.name) && rule.kind != ScalarKind {
      return Err(
        problem(
          "IncompatibleRule",
          "prefix requires a scalar type",
          span.start,
          span.end,
        ),
      )
    }
    kinds[rule.name] = rule.kind
  }
  let required_sites : Array[BindingIssue] = []
  for name in required {
    match spans.get(name) {
      Some(span) => required_sites.push({ name, span, })
      None =>
        return Err(
          problem("UnknownRequired", "required name is not referenced", 0, 0),
        )
    }
  }
  let captured = rules.copy()
  // Deterministic template order, including implicit prefix constraints.
  for name in self.variables() {
    if prefixes.contains(name) && !kinds.contains(name) {
      captured.push({ name, kind: ScalarKind, })
      spans[name] = prefixes[name]
    }
  }
  Ok({
    template: self,
    rules: captured,
    required: required.copy(),
    required_sites,
    reject_unused,
    spans,
  })
}

///|
fn Value::kind(self : Value) -> BindingKind {
  match self {
    Scalar(_) => ScalarKind
    List(_) => ListKind
    Assoc(_) => AssocKind
  }
}

///|
fn BindingKind::description(self : BindingKind) -> String {
  match self {
    ScalarKind => "expected a scalar string"
    ListKind => "expected a list of strings"
    AssocKind => "expected an associative value"
  }
}

///|
/// Input/shape/presence validation only; output limits are enforced by measure.
pub fn ContractTemplate::validate(
  self : ContractTemplate,
  variables : Variables,
  limits~ : ExpansionLimits = ExpansionLimits::new(),
) -> Result[Unit, TemplateError] {
  match check_contract_input(variables, self.required, limits) {
    Err(e) => return Err(e)
    Ok(_) => ()
  }
  for rule in self.rules {
    if variables.values.get(rule.name) is Some(value) &&
      value.kind() != rule.kind {
      let span = self.spans[rule.name]
      return Err(
        problem("BindingType", rule.kind.description(), span.start, span.end),
      )
    }
  }
  // Required names and every prefix shape were resolved at compilation. Avoid
  // allocating a full diagnostic report (and scanning input a second time) on
  // the success path. Preserve shape-before-presence-before-unused precedence.
  for site in self.required_sites {
    match variables.values.get(site.name) {
      None =>
        return Err(
          problem(
            "MissingBinding",
            "required variable is not bound",
            site.span.start,
            site.span.end,
          ),
        )
      Some(value) =>
        if !defined(value) {
          return Err(
            problem(
              "OmittedBinding",
              "required collection is empty and would be omitted",
              site.span.start,
              site.span.end,
            ),
          )
        }
    }
  }
  if self.reject_unused {
    for name in variables.values.keys() {
      if !self.spans.contains(name) {
        return Err(
          problem(
            "UnusedBinding", "bindings include names not referenced by the template",
            0, 0,
          ),
        )
      }
    }
  }
  Ok(())
}

///|
/// Collect presence, unused and shape findings without exposing bound values.
/// The cap applies to the complete report; overflow never returns a clean subset.
pub fn ContractTemplate::inspect(
  self : ContractTemplate,
  variables : Variables,
  limits~ : ExpansionLimits = ExpansionLimits::new(),
  max_findings~ : Int32 = 256,
) -> Result[Array[BindingFinding], TemplateError] {
  let findings = match
    self.template.check_contract(
      variables,
      required=self.required,
      reject_unused=self.reject_unused,
      limits~,
      max_findings~,
    ) {
    Err(e) => return Err(e)
    Ok(findings) => findings
  }
  for rule in self.rules {
    if variables.values.get(rule.name) is Some(value) &&
      value.kind() != rule.kind {
      if findings.length() == max_findings {
        return Err(problem("FindingLimit", "findings exceed budget", 0, 0))
      }
      findings.push({
        code: "BindingType",
        variable: rule.name,
        span: Some(self.spans[rule.name]),
      })
    }
  }
  Ok(findings)
}

///|
pub fn ContractTemplate::expand(
  self : ContractTemplate,
  variables : Variables,
  limits~ : ExpansionLimits = ExpansionLimits::new(),
) -> Result[String, TemplateError] {
  match self.validate(variables, limits~) {
    Err(e) => Err(e)
    Ok(_) => self.template.expand_impl(variables, Some(limits))
  }
}

///|
pub fn ContractTemplate::measure(
  self : ContractTemplate,
  variables : Variables,
  limits~ : ExpansionLimits = ExpansionLimits::new(),
) -> Result[Int32, TemplateError] {
  match self.validate(variables, limits~) {
    Err(e) => Err(e)
    Ok(_) =>
      self.template
      .render_impl(variables, Some(limits), false, false)
      .map(r => r.bytes)
  }
}

///|
pub fn ContractTemplate::expand_mapped(
  self : ContractTemplate,
  variables : Variables,
  limits~ : ExpansionLimits = ExpansionLimits::new(),
) -> Result[Expansion, TemplateError] {
  match self.validate(variables, limits~) {
    Err(e) => Err(e)
    Ok(_) => self.template.render_impl(variables, Some(limits), true, true)
  }
}

///|
pub extend BindingKind with Eq::{equal, not_equal}

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

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