///|
/// Severity assigned to an automated audit finding.
pub enum AuditSeverity {
  AuditInfo
  AuditWarning
  AuditError
  AuditCritical
}

///|
/// One actionable waveform audit finding.
pub struct AuditIssue {
  code : String
  severity : AuditSeverity
  message : String
  station : String
  channel : String
  score_penalty : Double
}

///|
/// Audit result for one trace.
pub struct TraceAudit {
  station : String
  channel : String
  issues : Array[AuditIssue]
  score : Double
  status : String
  report : StationQualityReport
}

///|
/// Aggregate audit result for a network.
pub struct NetworkAudit {
  traces : Int
  passed : Int
  failed : Int
  critical_findings : Int
  mean_score : Double
  audits : Array[TraceAudit]
}

///|
/// Return all audit severity constructors.
pub fn supported_audit_severities() -> Array[AuditSeverity] {
  [AuditInfo, AuditWarning, AuditError, AuditCritical]
}

///|
/// Return a stable severity label.
pub fn audit_severity_name(severity : AuditSeverity) -> String {
  match severity {
    AuditInfo => "info"
    AuditWarning => "warning"
    AuditError => "error"
    AuditCritical => "critical"
  }
}

///|
/// Create an audit issue.
pub fn make_audit_issue(
  code : String,
  severity : AuditSeverity,
  message : String,
  trace : Trace,
  score_penalty : Double,
) -> AuditIssue {
  {
    code,
    severity,
    message,
    station: trace.station,
    channel: trace.channel,
    score_penalty: score_penalty.max(0.0),
  }
}

///|
/// Audit metadata, sample integrity, and strong-motion plausibility.
pub fn audit_trace(
  trace : Trace,
  expected_samples : Int,
  full_scale : Double,
  baseline : Array[Double],
) -> TraceAudit {
  let quality = station_quality_report(
    trace, expected_samples, full_scale, 1.0e-12, baseline,
  )
  let issues : Array[AuditIssue] = Array::new()
  if trace.sampling_rate <= 0.0 {
    issues.push(
      make_audit_issue(
        "META_RATE",
        AuditCritical,
        "sample rate is not positive",
        trace,
        0.5,
      ),
    )
  }
  if trace.npts() == 0 {
    issues.push(
      make_audit_issue(
        "DATA_EMPTY",
        AuditCritical,
        "trace has no samples",
        trace,
        0.6,
      ),
    )
  }
  if trace.station == "" || trace.channel == "" {
    issues.push(
      make_audit_issue(
        "META_ID",
        AuditError,
        "station or channel identifier is empty",
        trace,
        0.2,
      ),
    )
  }
  if quality.completeness < 0.95 {
    issues.push(
      make_audit_issue(
        "DATA_GAP",
        AuditWarning,
        "observed sample count is below expectation",
        trace,
        0.15,
      ),
    )
  }
  if quality.clipping_fraction > 0.001 {
    issues.push(
      make_audit_issue(
        "DATA_CLIP",
        AuditError,
        "full-scale clipping detected",
        trace,
        0.25,
      ),
    )
  }
  if quality.flatline_fraction > 0.9 {
    issues.push(
      make_audit_issue(
        "DATA_FLAT",
        AuditError,
        "channel is substantially flatlined",
        trace,
        0.3,
      ),
    )
  }
  if quality.signal_to_noise_db < 3.0 {
    issues.push(
      make_audit_issue(
        "NOISE_SNR",
        AuditWarning,
        "signal-to-noise ratio is low",
        trace,
        0.1,
      ),
    )
  }
  let mut penalty = 0.0
  for issue in issues {
    penalty += issue.score_penalty
  }
  let score = (quality.score - penalty).max(0.0).min(1.0)
  {
    station: trace.station,
    channel: trace.channel,
    issues,
    score,
    status: quality_status(score),
    report: quality,
  }
}

///|
/// Audit a network of traces using a common expectation.
pub fn audit_network(
  traces : Array[Trace],
  expected_samples : Int,
  full_scale : Double,
) -> NetworkAudit {
  let audits : Array[TraceAudit] = Array::new()
  for trace in traces {
    audits.push(audit_trace(trace, expected_samples, full_scale, trace.data))
  }
  let mut passed = 0
  let mut critical = 0
  let mut total = 0.0
  for audit in audits {
    total += audit.score
    if audit.score >= 0.65 {
      passed += 1
    }
    for issue in audit.issues {
      if issue.severity is AuditCritical {
        critical += 1
      }
    }
  }
  {
    traces: audits.length(),
    passed,
    failed: audits.length() - passed,
    critical_findings: critical,
    mean_score: if audits.is_empty() {
      0.0
    } else {
      total / audits.length().to_double()
    },
    audits,
  }
}

///|
/// Return a risk score that emphasizes critical findings.
pub fn network_audit_risk(audit : NetworkAudit) -> Double {
  let failure_fraction = if audit.traces == 0 {
    1.0
  } else {
    audit.failed.to_double() / audit.traces.to_double()
  }
  (failure_fraction * 0.6 +
  audit.critical_findings.to_double() / audit.traces.max(1).to_double() * 0.4).min(
    1.0,
  )
}

///|
/// Export all audit issues for a review queue.
pub fn audit_issues_to_csv(audit : NetworkAudit) -> String {
  let mut output = "station,channel,code,severity,message,penalty\n"
  for trace_audit in audit.audits {
    for issue in trace_audit.issues {
      output = output +
        issue.station +
        "," +
        issue.channel +
        "," +
        issue.code +
        "," +
        audit_severity_name(issue.severity) +
        "," +
        issue.message +
        "," +
        issue.score_penalty.to_string() +
        "\n"
    }
  }
  output
}

///|
/// Format an audit headline for the CLI.
pub fn network_audit_text(audit : NetworkAudit) -> String {
  "traces=" +
  audit.traces.to_string() +
  " passed=" +
  audit.passed.to_string() +
  " failed=" +
  audit.failed.to_string() +
  " critical=" +
  audit.critical_findings.to_string() +
  " mean_score=" +
  audit.mean_score.to_string() +
  " risk=" +
  network_audit_risk(audit).to_string()
}