///|
/// Aggregate validation counters for dashboards and CI gates.
pub struct ValidationSummary {
  diagnostics : Int
  errors : Int
  warnings : Int
  unknown_changes : Int
  duplicate_ids : Int
  width_mismatches : Int
  monotonic_timestamps : Bool
  score : Int
} derive(Debug, Eq)

///|
/// Count diagnostics and structural risks in one trace.
pub fn validation_summary(trace : Trace) -> ValidationSummary {
  let diagnostics = deep_validate(trace)
  let (errors, warnings) = diagnostic_counts(diagnostics)
  let mut unknown_changes = 0
  let mut width_mismatches = 0
  for change in trace.changes {
    match find_signal(trace, change.id) {
      None => unknown_changes += 1
      Some(signal) =>
        if change.value.width() > 0 && change.value.width() > signal.width {
          width_mismatches += 1
        }
    }
  }
  let duplicate_ids = duplicate_signal_ids(trace).length()
  let monotonic = timestamps_monotonic(trace)
  let penalty = errors * 20 +
    warnings * 5 +
    unknown_changes * 10 +
    width_mismatches * 10
  let monotonic_penalty = if monotonic { 0 } else { 20 }
  let score = Int::max(0, Int::min(100, 100 - penalty - monotonic_penalty))
  {
    diagnostics: diagnostics.length(),
    errors,
    warnings,
    unknown_changes,
    duplicate_ids,
    width_mismatches,
    monotonic_timestamps: monotonic,
    score,
  }
}

///|
/// Return whether timestamps are nondecreasing in source order.
pub fn timestamps_monotonic(trace : Trace) -> Bool {
  let mut previous : Int64? = None
  for change in trace.changes {
    match previous {
      Some(value) if change.time < value => return false
      _ => ()
    }
    previous = Some(change.time)
  }
  true
}

///|
/// Return a compact validation line suitable for CI output.
pub fn validation_summary_line(summary : ValidationSummary) -> String {
  "score=" +
  summary.score.to_string() +
  " diagnostics=" +
  summary.diagnostics.to_string() +
  " errors=" +
  summary.errors.to_string() +
  " warnings=" +
  summary.warnings.to_string() +
  " unknown_changes=" +
  summary.unknown_changes.to_string() +
  " width_mismatches=" +
  summary.width_mismatches.to_string() +
  " monotonic=" +
  summary.monotonic_timestamps.to_string()
}

///|
/// Render validation counters as JSON.
pub fn validation_summary_json(summary : ValidationSummary) -> String {
  "{\"diagnostics\":" +
  summary.diagnostics.to_string() +
  ",\"errors\":" +
  summary.errors.to_string() +
  ",\"warnings\":" +
  summary.warnings.to_string() +
  ",\"unknown_changes\":" +
  summary.unknown_changes.to_string() +
  ",\"duplicate_ids\":" +
  summary.duplicate_ids.to_string() +
  ",\"width_mismatches\":" +
  summary.width_mismatches.to_string() +
  ",\"monotonic_timestamps\":" +
  summary.monotonic_timestamps.to_string() +
  ",\"score\":" +
  summary.score.to_string() +
  "}"
}

///|
/// Return declaration identifiers that are repeated in a malformed trace.
pub fn duplicate_signal_ids(trace : Trace) -> Array[String] {
  let result : Array[String] = []
  for index in 0.. Array[String] {
  let result : Array[String] = []
  for change in trace.changes {
    if !trace.has_signal(change.id) && !result.contains(change.id) {
      result.push(change.id)
    }
  }
  result
}

///|
/// Return a compact structural gate result.
pub fn structural_gate(trace : Trace) -> Bool {
  let summary = validation_summary(trace)
  summary.errors == 0 &&
  summary.unknown_changes == 0 &&
  summary.width_mismatches == 0 &&
  summary.monotonic_timestamps
}

///|
/// Return whether a trace is safe for bounded analysis.
pub fn analysis_safe(trace : Trace) -> Bool {
  structural_gate(trace) &&
  trace.end_time >= 0L &&
  trace.signals.length() <= 100000
}

///|
/// Render a one-line gate explanation.
pub fn structural_gate_report(trace : Trace) -> String {
  let summary = validation_summary(trace)
  if structural_gate(trace) {
    "pass " + validation_summary_line(summary)
  } else {
    "fail " + validation_summary_line(summary)
  }
}

///|
/// Return all declarations with values wider than their declared width.
pub fn width_mismatch_ids(trace : Trace) -> Array[String] {
  let result : Array[String] = []
  for change in trace.changes {
    match find_signal(trace, change.id) {
      Some(signal) if change.value.width() > signal.width &&
        !result.contains(change.id) => result.push(change.id)
      _ => ()
    }
  }
  result
}