///|
/// One route whose origin-validation state changes between two VRP snapshots.
pub(all) struct RouteImpact {
  previous : ValidationDecision
  current : ValidationDecision
} derive(Eq, Debug)

///|
fn validity_label(validity : RouteValidity) -> String {
  match validity {
    Valid => "valid"
    Invalid => "invalid"
    NotFound => "not found"
  }
}

///|
pub fn RouteImpact::summary(self : RouteImpact) -> String {
  let route = self.current.route
  "AS\{route.origin_asn()} \{route.prefix().to_string()}: \{validity_label(self.previous.validity)} -> \{validity_label(self.current.validity)}"
}

///|
fn human_relation_label(relation : VrpRelation) -> String {
  match relation {
    Authorizes => "authorizes"
    AsnMismatch => "origin ASN mismatch"
    LengthExceeded => "route prefix length exceeds maxLength"
    AsnAndLengthMismatch => "origin ASN and prefix length mismatch"
  }
}

///|
fn write_decision_evidence(
  buffer : StringBuilder,
  label : String,
  decision : ValidationDecision,
) -> Unit {
  buffer.write_string("  \{label} (\{validity_label(decision.validity)}):")
  if decision.matches.is_empty() {
    buffer.write_string("\n    no covering VRP")
  } else {
    for evidence in decision.matches {
      let vrp = evidence.vrp
      buffer.write_string(
        "\n    AS\{vrp.asn()} \{vrp.prefix().to_string()} maxLength \{vrp.max_length()}: \{human_relation_label(evidence.relation)}",
      )
    }
  }
}

///|
/// Explain a route-state transition using all old and new covering VRPs.
///
/// Evidence remains in validator order. An empty side is stated explicitly so
/// a NotFound decision cannot be mistaken for a missing report section.
pub fn RouteImpact::explanation(self : RouteImpact) -> String {
  let buffer = StringBuilder()
  buffer.write_string(self.summary())
  buffer.write_char('\n')
  write_decision_evidence(buffer, "before", self.previous)
  buffer.write_char('\n')
  write_decision_evidence(buffer, "after", self.current)
  buffer.to_string()
}

///|
/// Route-validation changes caused by replacing one complete VRP snapshot.
pub(all) struct SnapshotImpact {
  changed : Array[RouteImpact]
  unchanged : Int
} derive(Eq, Debug)

///|
pub fn SnapshotImpact::is_empty(self : SnapshotImpact) -> Bool {
  self.changed.is_empty()
}

///|
pub fn SnapshotImpact::summary(self : SnapshotImpact) -> String {
  "\{self.changed.length()} changed (\{self.unchanged} unchanged)"
}

///|
/// Evaluate how replacing a VRP snapshot changes a set of route decisions.
///
/// Only validity transitions are reported. Changes to covering evidence that
/// leave a route in the same state are counted as unchanged.
pub fn assess_snapshot_impact(
  routes : Array[RouteAnnouncement],
  previous_vrps : Array[Vrp],
  current_vrps : Array[Vrp],
) -> SnapshotImpact {
  let previous_index = VrpIndex::new(previous_vrps)
  let current_index = VrpIndex::new(current_vrps)
  let changed : Array[RouteImpact] = []
  let mut unchanged = 0
  for route in routes {
    let previous = previous_index.validate(route)
    let current = current_index.validate(route)
    if previous.validity == current.validity {
      unchanged = unchanged + 1
    } else {
      changed.push({ previous, current, })
    }
  }
  { changed, unchanged, }
}