///|
/// A supported consumer version bound to one declared contract and historical
/// snapshot. Labels are supplied by the caller, not authenticated provenance.
pub struct ConfigConsumerBinding {
  consumer : String
  version : String
  baseline_id : String
  contract : ConfigConsumerContract
  baseline : ConfigValue
}

///|
pub fn ConfigConsumerBinding::new(
  consumer : String,
  version : String,
  baseline_id : String,
  contract : ConfigConsumerContract,
  baseline : ConfigValue,
) -> Result[ConfigConsumerBinding, String] {
  if consumer.trim().length() == 0 ||
    version.trim().length() == 0 ||
    baseline_id.trim().length() == 0 {
    return Err("consumer, version and baseline_id must not be blank")
  }
  Ok({ consumer, version, baseline_id, contract, baseline, })
}

///|
/// A comparison under the SAME contract on both snapshots. Issue identity is
/// (path, kind), not just path. Persistent issues do not imply unchanged values.
pub struct ConfigConsumerRegression {
  consumer : String
  version : String
  baseline_id : String
  introduced : Array[ConfigConsumerIssue]
  persistent : Array[ConfigConsumerIssue]
  resolved : Array[ConfigConsumerIssue]
  effective_comparable : Bool
  effective_changes : Array[ConfigEffectiveChange]
}

///|
pub fn ConfigConsumerRegression::consumer(
  self : ConfigConsumerRegression,
) -> String {
  self.consumer
}

///|
pub fn ConfigConsumerRegression::version(
  self : ConfigConsumerRegression,
) -> String {
  self.version
}

///|
pub fn ConfigConsumerRegression::baseline_id(
  self : ConfigConsumerRegression,
) -> String {
  self.baseline_id
}

///|
pub fn ConfigConsumerRegression::introduced(
  self : ConfigConsumerRegression,
) -> Array[ConfigConsumerIssue] {
  self.introduced.copy()
}

///|
pub fn ConfigConsumerRegression::persistent(
  self : ConfigConsumerRegression,
) -> Array[ConfigConsumerIssue] {
  self.persistent.copy()
}

///|
pub fn ConfigConsumerRegression::resolved(
  self : ConfigConsumerRegression,
) -> Array[ConfigConsumerIssue] {
  self.resolved.copy()
}

///|
/// False if either snapshot violates the contract: an empty change array then
/// means unknown, NOT unchanged. Callers must check this before interpreting it.
pub fn ConfigConsumerRegression::effective_comparable(
  self : ConfigConsumerRegression,
) -> Bool {
  self.effective_comparable
}

///|
pub fn ConfigConsumerRegression::effective_changes(
  self : ConfigConsumerRegression,
) -> Array[ConfigEffectiveChange] {
  self.effective_changes.copy()
}

///|
/// A separate, opt-in behavior-preservation gate. Candidate acceptance via
/// passes() retains its original meaning. Unknown comparisons fail closed.
pub fn ConfigConsumerRegression::preserves_effective_values(
  self : ConfigConsumerRegression,
) -> Bool {
  self.effective_comparable &&
  self.effective_changes.all(change => !change.value_changed)
}

///|
pub fn ConfigConsumerReleaseReport::preserves_effective_values(
  self : ConfigConsumerReleaseReport,
) -> Bool {
  self.entries.all(entry => entry.preserves_effective_values())
}

///|
/// Strict candidate acceptance: historical violations are NOT grandfathered.
pub fn ConfigConsumerRegression::passes(
  self : ConfigConsumerRegression,
) -> Bool {
  self.introduced.is_empty() && self.persistent.is_empty()
}

///|
pub struct ConfigConsumerReleaseReport {
  entries : Array[ConfigConsumerRegression]
}

///|
pub fn ConfigConsumerReleaseReport::entries(
  self : ConfigConsumerReleaseReport,
) -> Array[ConfigConsumerRegression] {
  self.entries.copy()
}

///|
/// All supplied versions are supported and must accept the candidate. This
/// does not include structural gates or prove unchanged effective values.
pub fn ConfigConsumerReleaseReport::passes(
  self : ConfigConsumerReleaseReport,
) -> Bool {
  self.entries.all(entry => entry.passes())
}

///|
/// Reject empty support sets and duplicate (consumer, version) identities.
/// Results are ordered lexically by consumer then version (not semver).
/// Per-category issues retain the checker's stable path order.
pub fn ConfigConsumerReleaseReport::check(
  bindings : Array[ConfigConsumerBinding],
  candidate : ConfigValue,
) -> Result[ConfigConsumerReleaseReport, String] {
  if bindings.is_empty() {
    return Err("at least one supported consumer version is required")
  }
  let ordered = bindings.copy()
  ordered.sort_by((a, b) => {
    let order = a.consumer.lexical_compare(b.consumer)
    if order == 0 {
      a.version.lexical_compare(b.version)
    } else {
      order
    }
  })
  for index in 1.. {
    let historical = binding.contract.check(binding.baseline)
    let proposed = binding.contract.check(candidate)
    let effective_comparable = historical.is_empty() && proposed.is_empty()
    let effective_changes = if effective_comparable {
      consumer_effective_changes(binding.contract, binding.baseline, candidate)
    } else {
      []
    }
    let introduced = proposed.filter(issue => !historical.contains(issue))
    let persistent = proposed.filter(issue => historical.contains(issue))
    let resolved = historical.filter(issue => !proposed.contains(issue))
    ConfigConsumerRegression::{
      consumer: binding.consumer,
      version: binding.version,
      baseline_id: binding.baseline_id,
      introduced,
      persistent,
      resolved,
      effective_comparable,
      effective_changes,
    }
  })
  Ok({ entries, })
}