///|
pub fn diagnostic_event(
  code : String,
  stage : String,
  detail : String,
  value : Double,
) -> DiagnosticEvent {
  { code, stage, detail, value }
}

///|
pub fn diagnostic_ok(stage : String, detail : String) -> PipelineDiagnostic {
  { stage, message: detail, severity: Pass }
}

///|
pub fn diagnostic_warning(
  stage : String,
  detail : String,
) -> PipelineDiagnostic {
  { stage, message: detail, severity: Warn }
}

///|
pub fn diagnostic_error(stage : String, detail : String) -> PipelineDiagnostic {
  { stage, message: detail, severity: Fail }
}

///|
pub fn diagnostics_for_image(image : GrayImage) -> Array[DiagnosticEvent] {
  [
    diagnostic_event(
      "DIMENSIONS",
      "image",
      "width",
      Double::from_int(image.width),
    ),
    diagnostic_event(
      "HEIGHT",
      "image",
      "height",
      Double::from_int(image.height),
    ),
    diagnostic_event("MEAN", "image", "mean intensity", image_mean(image)),
    diagnostic_event(
      "VARIANCE",
      "image",
      "intensity variance",
      image_variance(image),
    ),
    diagnostic_event("FOCUS", "image", "focus score", focus_score(image)),
    diagnostic_event("NOISE", "image", "texture noise", texture_noise(image)),
  ]
}

///|
pub fn diagnostics_for_calibration(
  fit : CalibrationFit,
) -> Array[DiagnosticEvent] {
  [
    diagnostic_event(
      "RMS",
      "calibration",
      "root mean square residual",
      fit.rms_error,
    ),
    diagnostic_event("MAX", "calibration", "maximum residual", fit.max_error),
    diagnostic_event(
      "QUALITY",
      "calibration",
      "quality score",
      calibration_quality_score(fit, fit.residuals.length()),
    ),
  ]
}

///|
pub fn diagnostics_for_batch(batch : BatchRun) -> Array[DiagnosticEvent] {
  let summary = batch.summary()
  [
    diagnostic_event(
      "TOTAL",
      "batch",
      "measurement count",
      Double::from_int(summary.total),
    ),
    diagnostic_event(
      "PASS",
      "batch",
      "pass count",
      Double::from_int(summary.pass),
    ),
    diagnostic_event(
      "WARN",
      "batch",
      "warning count",
      Double::from_int(summary.warn),
    ),
    diagnostic_event(
      "FAIL",
      "batch",
      "failure count",
      Double::from_int(summary.fail),
    ),
    diagnostic_event(
      "RATE",
      "batch",
      "acceptance rate",
      summary.acceptance_rate,
    ),
  ]
}

///|
pub fn diagnostics_merge(
  groups : Array[Array[DiagnosticEvent]],
) -> Array[DiagnosticEvent] {
  let result : Array[DiagnosticEvent] = []
  for group in groups {
    result.append(group)
  }
  result
}

///|
pub fn diagnostics_filter(
  events : Array[DiagnosticEvent],
  code : String,
) -> Array[DiagnosticEvent] {
  events.filter(fn(event) { event.code == code })
}

///|
pub fn diagnostics_max(events : Array[DiagnosticEvent]) -> DiagnosticEvent? {
  if events.is_empty() {
    None
  } else {
    let mut result = events[0]
    for event in events[1:] {
      if event.value > result.value {
        result = event
      }
    }
    Some(result)
  }
}

///|
pub fn diagnostics_summary(events : Array[DiagnosticEvent]) -> StatsSummary {
  describe_samples(events.map(fn(event) { event.value }))
}

///|
pub fn diagnostics_text(events : Array[DiagnosticEvent]) -> String {
  let out = StringBuilder()
  for event in events {
    out.write_string("[")
    out.write_string(event.code)
    out.write_string("] ")
    out.write_string(event.stage)
    out.write_string(" - ")
    out.write_string(event.detail)
    out.write_string(": ")
    out.write_string(event.value.to_string())
    out.write_string("\n")
  }
  out.to_string()
}

///|
pub fn validate_image(image : GrayImage) -> Array[PipelineDiagnostic] {
  let result : Array[PipelineDiagnostic] = []
  if image.width <= 0 || image.height <= 0 {
    result.push(diagnostic_error("image", "image has no pixels"))
  }
  if image.pixels.length() != image.width * image.height {
    result.push(
      diagnostic_error("image", "pixel buffer length does not match dimensions"),
    )
  }
  for value in image.pixels {
    if !is_finite(value) {
      result.push(
        diagnostic_error("image", "image contains a non-finite value"),
      )
      break
    }
  }
  if result.is_empty() {
    result.push(diagnostic_ok("image", "image validation passed"))
  }
  result
}

///|
pub fn validate_caliper_config(
  config : CaliperConfig,
) -> Array[PipelineDiagnostic] {
  let result : Array[PipelineDiagnostic] = []
  if distance(config.start, config.finish) <= 0.0 {
    result.push(
      diagnostic_error("config", "measurement segment has zero length"),
    )
  }
  if config.threshold < 0.0 {
    result.push(diagnostic_error("config", "threshold must be non-negative"))
  }
  if config.expected_edges <= 0 {
    result.push(
      diagnostic_error("config", "expected edge count must be positive"),
    )
  }
  if result.is_empty() {
    result.push(diagnostic_ok("config", "caliper configuration passed"))
  }
  result
}

///|
pub fn validate_tolerance(tolerance : Tolerance) -> Array[PipelineDiagnostic] {
  let result : Array[PipelineDiagnostic] = []
  if tolerance.lower > tolerance.upper {
    result.push(
      diagnostic_error("tolerance", "lower bound exceeds upper bound"),
    )
  }
  if tolerance.nominal < tolerance.lower || tolerance.nominal > tolerance.upper {
    result.push(
      diagnostic_warning("tolerance", "nominal is outside the declared bounds"),
    )
  }
  if result.is_empty() {
    result.push(diagnostic_ok("tolerance", "tolerance validation passed"))
  }
  result
}

///|
pub fn diagnostic_severity(
  events : Array[PipelineDiagnostic],
) -> MeasurementDecision {
  let mut result = Pass
  for event in events {
    match event.severity {
      Fail => result = Fail
      Warn => if result == Pass { result = Warn }
      Pass => ()
    }
  }
  result
}

///|
pub fn diagnostics_ok(events : Array[PipelineDiagnostic]) -> Bool {
  diagnostic_severity(events) == Pass
}

///|
pub fn diagnostics_to_markdown(events : Array[PipelineDiagnostic]) -> String {
  let out = StringBuilder()
  for event in events {
    out.write_string("- ")
    out.write_string(decision_name(event.severity))
    out.write_string(" ")
    out.write_string(event.stage)
    out.write_string(": ")
    out.write_string(event.message)
    out.write_string("\n")
  }
  out.to_string()
}