///|
/// Human- and machine-readable audit summaries for process studies.
pub(all) enum Severity {
  Pass
  Notice
  Fail
} derive(Debug, Eq, ToJson)

///|
pub(all) struct Finding {
  code : String
  severity : Severity
  title : String
  detail : String
  measured : Double
  limit : Double
} derive(Debug, Eq, ToJson)

///|
pub fn Finding::pass(
  code~ : String,
  title~ : String,
  measured~ : Double,
  limit~ : Double,
  detail~ : String,
) -> Finding {
  { code, severity: Pass, title, detail, measured, limit }
}

///|
pub fn Finding::notice(
  code~ : String,
  title~ : String,
  measured~ : Double,
  limit~ : Double,
  detail~ : String,
) -> Finding {
  { code, severity: Notice, title, detail, measured, limit }
}

///|
pub fn Finding::fail(
  code~ : String,
  title~ : String,
  measured~ : Double,
  limit~ : Double,
  detail~ : String,
) -> Finding {
  { code, severity: Fail, title, detail, measured, limit }
}

///|
pub(all) struct AuditReport {
  name : String
  findings : Array[Finding]
  passed : Bool
  score : Double
} derive(Debug, Eq, ToJson)

///|
pub fn AuditReport::new(name~ : String) -> AuditReport {
  { name, findings: [], passed: true, score: 1.0 }
}

///|
pub fn AuditReport::add(self : AuditReport, finding : Finding) -> AuditReport {
  let findings = self.findings.copy()
  findings.push(finding)
  let mut failures = 0
  let mut notices = 0
  for item in findings {
    match item.severity {
      Pass => ()
      Notice => notices += 1
      Fail => failures += 1
    }
  }
  let score = (findings.length() - failures * 2 - notices).to_double() /
    findings.length().max(1).to_double()
  {
    name: self.name,
    findings,
    passed: failures == 0,
    score: score.max(0.0).min(1.0),
  }
}

///|
pub fn AuditReport::finding_count(
  self : AuditReport,
  severity : Severity,
) -> Int {
  let mut count = 0
  for item in self.findings {
    if item.severity == severity {
      count += 1
    }
  }
  count
}

///|
pub fn AuditReport::to_csv(self : AuditReport) -> String {
  let lines = ["code,severity,title,measured,limit,detail"]
  for item in self.findings {
    let severity = match item.severity {
      Pass => "pass"
      Notice => "notice"
      Fail => "fail"
    }
    lines.push(
      "\{item.code},\{severity},\{escape_csv(item.title)},\{item.measured},\{item.limit},\{escape_csv(item.detail)}",
    )
  }
  lines.join("\n")
}

///|
pub fn AuditReport::to_markdown(self : AuditReport) -> String {
  let lines = [
    "# \{self.name}",
    "",
    "| Code | Severity | Finding | Measured | Limit |",
    "|---|---|---|---:|---:|",
  ]
  for item in self.findings {
    let severity = match item.severity {
      Pass => "PASS"
      Notice => "NOTICE"
      Fail => "FAIL"
    }
    lines.push(
      "| \{item.code} | \{severity} | \{item.title} | \{item.measured} | \{item.limit} |",
    )
  }
  lines.push("")
  let result = if self.passed { "PASS" } else { "FAIL" }
  lines.push("Result: \{result} (score=\{self.score})")
  lines.join("\n")
}

///|
fn escape_csv(value : String) -> String {
  if value.contains(",") || value.contains("\"") || value.contains("\n") {
    "\"" + value.replace(old="\"", new="\"\"") + "\""
  } else {
    value
  }
}

///|
pub fn balance_report(
  name~ : String,
  residuals : ArrayView[Double],
  tolerance~ : Double,
) -> AuditReport raise @domain.ExtractionError {
  @domain.require_positive("balance tolerance", tolerance)
  let report = AuditReport::new(name~)
  let mut result = report
  for index, residual in residuals {
    @domain.require_finite("balance residual", residual)
    let absolute = residual.abs()
    result = result.add(
      if absolute <= tolerance {
        Finding::pass(
          code="BAL-\{index}",
          title="material balance",
          measured=absolute,
          limit=tolerance,
          detail="residual within tolerance",
        )
      } else {
        Finding::fail(
          code="BAL-\{index}",
          title="material balance",
          measured=absolute,
          limit=tolerance,
          detail="residual exceeds tolerance",
        )
      },
    )
  }
  result
}

///|
/// Build a report from the analysis package's aggregate balance audit.
pub fn analysis_balance_report(
  summary : @analysis.BalanceAudit,
  tolerance~ : Double,
) -> AuditReport raise @domain.ExtractionError {
  let report = balance_report(
    name="scenario balance audit",
    [summary.maximum_absolute_residual],
    tolerance~,
  )
  if summary.passed {
    report
  } else {
    report.add(
      Finding::fail(
        code="BAL-SUMMARY",
        title="scenario aggregate",
        measured=summary.maximum_absolute_residual,
        limit=tolerance,
        detail="one or more scenarios failed",
      ),
    )
  }
}