///|
/// A structured validation finding for a waveform or model.
pub struct ValidationFinding {
  code : String
  severity : Int
  message : String
  sample_index : Int?
}

///|
/// Validate a signal's amplitude and timing invariants.
pub fn validate_samples(
  data : Array[Double],
  sample_rate : Double,
  maximum_amplitude : Double,
) -> Array[ValidationFinding] {
  let findings = Array::new()
  if data.is_empty() {
    findings.push({
      code: "EMPTY",
      severity: 2,
      message: "Signal has no samples",
      sample_index: None,
    })
  }
  if sample_rate <= 0.0 {
    findings.push({
      code: "RATE",
      severity: 2,
      message: "Sample rate must be positive",
      sample_index: None,
    })
  }
  let limit = maximum_amplitude.abs()
  for index = 0; index < data.length(); index = index + 1 {
    if !is_valid_double(data[index]) {
      findings.push({
        code: "NONFINITE",
        severity: 2,
        message: "Invalid floating-point sample",
        sample_index: Some(index),
      })
    } else if limit > 0.0 && data[index].abs() > limit {
      findings.push({
        code: "RANGE",
        severity: 1,
        message: "Sample exceeds configured amplitude range",
        sample_index: Some(index),
      })
    }
  }
  findings
}

///|
/// Validate a trace and return all findings instead of stopping at the first.
pub fn validate_trace_findings(
  trace : Trace,
  maximum_amplitude : Double,
) -> Array[ValidationFinding] {
  validate_samples(trace.data, trace.sampling_rate, maximum_amplitude)
}

///|
/// Return the number of error-severity findings.
pub fn validation_error_count(findings : Array[ValidationFinding]) -> Int {
  findings.count_if(finding => finding.severity >= 2)
}

///|
/// Return the number of warning-severity findings.
pub fn validation_warning_count(findings : Array[ValidationFinding]) -> Int {
  findings.count_if(finding => finding.severity == 1)
}

///|
/// Return true when no error-severity finding exists.
pub fn validation_passes(findings : Array[ValidationFinding]) -> Bool {
  validation_error_count(findings) == 0
}

///|
/// Validate that a velocity model is compatible with a source depth.
pub fn validate_model_for_depth(
  model : VelocityModel,
  source_depth : Double,
) -> Array[ValidationFinding] {
  let findings = Array::new()
  match validate_velocity_model(model) {
    Ok(_) => ()
    Err(message) =>
      findings.push({ code: "MODEL", severity: 2, message, sample_index: None })
  }
  if source_depth < 0.0 {
    findings.push({
      code: "DEPTH",
      severity: 2,
      message: "Source depth cannot be negative",
      sample_index: None,
    })
  }
  findings
}

///|
/// Return a stable text representation of validation findings.
pub fn validation_findings_text(findings : Array[ValidationFinding]) -> String {
  let mut output = "severity,code,message,sample\n"
  for finding in findings {
    let sample = match finding.sample_index {
      Some(index) => index.to_string()
      None => ""
    }
    output = output +
      finding.severity.to_string() +
      "," +
      finding.code +
      "," +
      finding.message +
      "," +
      sample +
      "\n"
  }
  output
}

///|
/// Require a clean trace, returning the first human-readable issue.
pub fn require_valid_trace(
  trace : Trace,
  maximum_amplitude : Double,
) -> Result[Unit, String] {
  let findings = validate_trace_findings(trace, maximum_amplitude)
  if validation_passes(findings) {
    Ok(())
  } else {
    Err(validation_findings_text(findings))
  }
}

///|
/// Validate the round-trip size invariant for a trace's SAC encoding.
pub fn validate_sac_roundtrip_size(trace : Trace) -> Bool {
  let bytes = to_sac_bytes(trace, false)
  bytes.length() == sac_record_size(trace.npts())
}

///|
/// Validate the fixed-width invariant for a miniSEED encoding.
pub fn validate_miniseed_roundtrip_size(trace : Trace) -> Bool {
  let bytes = to_miniseed_bytes(trace, 1, true)
  bytes.length() == miniseed_record_size(trace.npts(), 64)
}