///|
priv struct RuleHitCount {
  rule : @assay.Rule
  count : Int
}

///|
priv struct EpochRunCount {
  epoch : @epoch.QCEpoch
  count : Int
  in_control : Int
  warning : Int
  requires_review : Int
  incomplete : Int
}

///|
priv struct ControlRuleHitCount {
  control_level_id : String
  rule : @assay.Rule
  count : Int
}

///|
priv struct EpochRuleHitCount {
  epoch : @epoch.QCEpoch
  rule : @assay.Rule
  count : Int
}

///|
priv struct EpochControlRuleHitCount {
  epoch : @epoch.QCEpoch
  control_level_id : String
  rule : @assay.Rule
  count : Int
}

///|
priv struct ControlCoverageCount {
  epoch : @epoch.QCEpoch
  control_level_id : String
  required : Bool
  expected_runs : Int
  observed_runs : Int
  missing_runs : Int
  precision_mismatch_runs : Int
}

///|
priv struct AuditSummary {
  input_runs : Int
  assessed_runs : Int
  in_control : Int
  warning : Int
  requires_review : Int
  incomplete : Int
  total_rule_hits : Int
  rule_hits : Array[RuleHitCount]
  epoch_runs : Array[EpochRunCount]
  control_rule_hits : Array[ControlRuleHitCount]
  epoch_rule_hits : Array[EpochRuleHitCount]
  epoch_control_rule_hits : Array[EpochControlRuleHitCount]
  control_coverage : Array[ControlCoverageCount]
  expected_control_observations : Int
  observed_control_observations : Int
  missing_control_observations : Int
  precision_mismatch_control_observations : Int
  required_missing_control_observations : Int
  track_issues : Int
}

///|
fn audit_rule_order() -> Array[@assay.Rule] {
  [
    @assay.Rule12s,
    @assay.Rule13s,
    @assay.Rule22s,
    @assay.RuleR4s,
    @assay.Rule41s,
    @assay.Rule10x,
  ]
}

///|
fn audit_rule_disposition_name(disposition : @assay.RuleDisposition) -> String {
  match disposition {
    @assay.Disabled => "disabled"
    @assay.Warning => "warning"
    @assay.RequiresReview => "requires_review"
  }
}

///|
fn audit_increment_epoch_rule_hit(
  counts : Array[EpochRuleHitCount],
  epoch : @epoch.QCEpoch,
  rule : @assay.Rule,
) -> Unit {
  let mut match_index : Int? = None
  for index, entry in counts {
    if entry.epoch.has_same_metadata(epoch) && entry.rule == rule {
      match_index = Some(index)
      break
    }
  }
  match match_index {
    Some(index) =>
      counts[index] = {
        epoch: counts[index].epoch,
        rule: counts[index].rule,
        count: counts[index].count + 1,
      }
    None => counts.push({ epoch, rule, count: 1 })
  }
}

///|
fn audit_increment_epoch_control_rule_hit(
  counts : Array[EpochControlRuleHitCount],
  epoch : @epoch.QCEpoch,
  control_level_id : String,
  rule : @assay.Rule,
) -> Unit {
  let mut match_index : Int? = None
  for index, entry in counts {
    if entry.epoch.has_same_metadata(epoch) &&
      entry.control_level_id == control_level_id &&
      entry.rule == rule {
      match_index = Some(index)
      break
    }
  }
  match match_index {
    Some(index) =>
      counts[index] = {
        epoch: counts[index].epoch,
        control_level_id: counts[index].control_level_id,
        rule: counts[index].rule,
        count: counts[index].count + 1,
      }
    None => counts.push({ epoch, control_level_id, rule, count: 1 })
  }
}

///|
fn audit_epoch_total_rule_hits(
  counts : Array[EpochRuleHitCount],
  epoch : @epoch.QCEpoch,
) -> Int {
  let mut total = 0
  for entry in counts {
    if entry.epoch.has_same_metadata(epoch) {
      total = total + entry.count
    }
  }
  total
}

///|
fn audit_summary(
  program : @assay.AssayProgram,
  runs : Array[@run.QCRun],
  trajectory : @evidence.Trajectory,
) -> AuditSummary {
  let mut in_control = 0
  let mut warning = 0
  let mut requires_review = 0
  let mut incomplete = 0
  let mut total_rule_hits = 0
  let rule_hits : Array[RuleHitCount] = audit_rule_order().map(fn(rule) {
    let mut count = 0
    for assessment in trajectory.assessments {
      for hit in assessment.hits {
        if hit.rule == rule {
          count = count + 1
        }
      }
    }
    total_rule_hits = total_rule_hits + count
    { rule, count }
  })
  for assessment in trajectory.assessments {
    match assessment.status {
      @evidence.InControl => in_control = in_control + 1
      @evidence.Warning => warning = warning + 1
      @evidence.RequiresReview => requires_review = requires_review + 1
      @evidence.Incomplete => incomplete = incomplete + 1
    }
  }
  let epochs : Array[EpochRunCount] = []
  for run_index, run in runs {
    let (run_in_control, run_warning, run_requires_review, run_incomplete) = match
      trajectory.assessments.get(run_index) {
      Some(assessment) =>
        match assessment.status {
          @evidence.InControl => (1, 0, 0, 0)
          @evidence.Warning => (0, 1, 0, 0)
          @evidence.RequiresReview => (0, 0, 1, 0)
          @evidence.Incomplete => (0, 0, 0, 1)
        }
      None => (0, 0, 0, 0)
    }
    let mut match_index : Int? = None
    for index, entry in epochs {
      if entry.epoch.has_same_metadata(run.epoch) {
        match_index = Some(index)
        break
      }
    }
    match match_index {
      Some(index) =>
        epochs[index] = {
          epoch: epochs[index].epoch,
          count: epochs[index].count + 1,
          in_control: epochs[index].in_control + run_in_control,
          warning: epochs[index].warning + run_warning,
          requires_review: epochs[index].requires_review + run_requires_review,
          incomplete: epochs[index].incomplete + run_incomplete,
        }
      None =>
        epochs.push({
          epoch: run.epoch,
          count: 1,
          in_control: run_in_control,
          warning: run_warning,
          requires_review: run_requires_review,
          incomplete: run_incomplete,
        })
    }
  }
  let control_rule_hits : Array[ControlRuleHitCount] = []
  for assessment in trajectory.assessments {
    for hit in assessment.hits {
      let levels : Array[String] = []
      for point in hit.evidence {
        if !contains_level(levels, point.control_level_id) {
          levels.push(point.control_level_id)
        }
      }
      for level_id in levels {
        let mut match_index : Int? = None
        for index, entry in control_rule_hits {
          if entry.control_level_id == level_id && entry.rule == hit.rule {
            match_index = Some(index)
            break
          }
        }
        match match_index {
          Some(index) =>
            control_rule_hits[index] = {
              control_level_id: control_rule_hits[index].control_level_id,
              rule: control_rule_hits[index].rule,
              count: control_rule_hits[index].count + 1,
            }
          None =>
            control_rule_hits.push({
              control_level_id: level_id,
              rule: hit.rule,
              count: 1,
            })
        }
      }
    }
  }
  let epoch_rule_hits : Array[EpochRuleHitCount] = []
  let epoch_control_rule_hits : Array[EpochControlRuleHitCount] = []
  for assessment_index, assessment in trajectory.assessments {
    match runs.get(assessment_index) {
      Some(run) =>
        for hit in assessment.hits {
          audit_increment_epoch_rule_hit(epoch_rule_hits, run.epoch, hit.rule)
          let levels : Array[String] = []
          for point in hit.evidence {
            if !contains_level(levels, point.control_level_id) {
              levels.push(point.control_level_id)
            }
          }
          for level_id in levels {
            audit_increment_epoch_control_rule_hit(
              epoch_control_rule_hits,
              run.epoch,
              level_id,
              hit.rule,
            )
          }
        }
      None => ()
    }
  }
  let control_coverage : Array[ControlCoverageCount] = []
  for epoch_entry in epochs {
    for level in program.levels {
      let mut observed_runs = 0
      let mut missing_runs = 0
      let mut precision_mismatch_runs = 0
      for run in runs {
        if run.epoch.has_same_metadata(epoch_entry.epoch) {
          let mut point : @run.ControlPoint? = None
          for observation in run.observations {
            if observation.control_level_id == level.id && point is None {
              point = Some(observation)
            }
          }
          match point {
            Some(observation) if observation.precision != program.precision =>
              precision_mismatch_runs = precision_mismatch_runs + 1
            Some(_) => observed_runs = observed_runs + 1
            None => missing_runs = missing_runs + 1
          }
        }
      }
      control_coverage.push({
        epoch: epoch_entry.epoch,
        control_level_id: level.id,
        required: level.required,
        expected_runs: epoch_entry.count,
        observed_runs,
        missing_runs,
        precision_mismatch_runs,
      })
    }
  }
  let mut expected_control_observations = 0
  let mut observed_control_observations = 0
  let mut missing_control_observations = 0
  let mut precision_mismatch_control_observations = 0
  let mut required_missing_control_observations = 0
  for coverage in control_coverage {
    expected_control_observations = expected_control_observations +
      coverage.expected_runs
    observed_control_observations = observed_control_observations +
      coverage.observed_runs
    missing_control_observations = missing_control_observations +
      coverage.missing_runs
    precision_mismatch_control_observations = precision_mismatch_control_observations +
      coverage.precision_mismatch_runs
    if coverage.required {
      required_missing_control_observations = required_missing_control_observations +
        coverage.missing_runs
    }
  }
  {
    input_runs: runs.length(),
    assessed_runs: trajectory.assessments.length(),
    in_control,
    warning,
    requires_review,
    incomplete,
    total_rule_hits,
    rule_hits,
    epoch_runs: epochs,
    control_rule_hits,
    epoch_rule_hits,
    epoch_control_rule_hits,
    control_coverage,
    expected_control_observations,
    observed_control_observations,
    missing_control_observations,
    precision_mismatch_control_observations,
    required_missing_control_observations,
    track_issues: trajectory.input_issues.length(),
  }
}

///|
fn audit_json_epoch(epoch : @epoch.QCEpoch) -> Json {
  let fields : Map[String, Json] = Map([])
  fields["id"] = Json::string(epoch.id)
  fields["reagent_lot"] = Json::string(epoch.reagent_lot)
  fields["control_lot"] = Json::string(epoch.control_lot)
  fields["calibration_id"] = Json::string(epoch.calibration_id)
  fields["program_version"] = Json::string(epoch.program_version)
  Json::object(fields)
}

///|
fn audit_json_run(run : @run.QCRun) -> Json {
  let fields : Map[String, Json] = Map([])
  fields["run_id"] = Json::string(run.run_id)
  fields["sequence"] = json_number(run.sequence)
  fields["timestamp"] = Json::string(run.timestamp)
  fields["epoch"] = audit_json_epoch(run.epoch)
  fields["observations"] = Json::array(
    run.observations.map(fn(point) {
      let point_fields : Map[String, Json] = Map([])
      point_fields["control_level_id"] = Json::string(point.control_level_id)
      point_fields["value_scaled"] = json_number(point.value)
      point_fields["precision"] = Json::number(point.precision.to_double())
      Json::object(point_fields)
    }),
  )
  Json::object(fields)
}

///|
fn audit_json_program(program : @assay.AssayProgram) -> Json {
  let fields : Map[String, Json] = Map([])
  fields["assay_id"] = Json::string(program.assay_id)
  fields["unit"] = Json::string(program.unit)
  fields["precision"] = Json::number(program.precision.to_double())
  fields["control_levels"] = Json::array(
    program.levels.map(fn(level) {
      let level_fields : Map[String, Json] = Map([])
      level_fields["id"] = Json::string(level.id)
      level_fields["mean_scaled"] = json_number(level.mean)
      level_fields["standard_deviation_scaled"] = json_number(
        level.standard_deviation,
      )
      level_fields["required"] = Json::boolean(level.required)
      Json::object(level_fields)
    }),
  )
  fields["rule_dispositions"] = Json::array(
    audit_rule_order().map(fn(rule) {
      let rule_fields : Map[String, Json] = Map([])
      rule_fields["rule"] = Json::string(rule.name())
      rule_fields["disposition"] = Json::string(
        audit_rule_disposition_name(program.disposition_for(rule)),
      )
      Json::object(rule_fields)
    }),
  )
  Json::object(fields)
}

///|
fn audit_json_epoch_rule_hits(
  summary : AuditSummary,
  epoch : @epoch.QCEpoch,
) -> Json {
  let hits : Array[Json] = []
  for entry in summary.epoch_rule_hits {
    if entry.epoch.has_same_metadata(epoch) {
      hits.push(
        Json::object({
          "rule": Json::string(entry.rule.name()),
          "count": Json::number(entry.count.to_double()),
        }),
      )
    }
  }
  Json::array(hits)
}

///|
fn audit_json_epoch_control_rule_hits(
  summary : AuditSummary,
  epoch : @epoch.QCEpoch,
) -> Json {
  let hits : Array[Json] = []
  for entry in summary.epoch_control_rule_hits {
    if entry.epoch.has_same_metadata(epoch) {
      hits.push(
        Json::object({
          "control_level_id": Json::string(entry.control_level_id),
          "rule": Json::string(entry.rule.name()),
          "count": Json::number(entry.count.to_double()),
        }),
      )
    }
  }
  Json::array(hits)
}

///|
fn audit_json_summary(summary : AuditSummary) -> Json {
  let fields : Map[String, Json] = Map([])
  fields["input_runs"] = Json::number(summary.input_runs.to_double())
  fields["assessed_runs"] = Json::number(summary.assessed_runs.to_double())
  fields["status_counts"] = Json::object({
    "InControl": Json::number(summary.in_control.to_double()),
    "Warning": Json::number(summary.warning.to_double()),
    "RequiresReview": Json::number(summary.requires_review.to_double()),
    "Incomplete": Json::number(summary.incomplete.to_double()),
  })
  fields["total_rule_hits"] = Json::number(summary.total_rule_hits.to_double())
  fields["rule_hit_counts"] = Json::array(
    summary.rule_hits.map(fn(entry) {
      let rule_fields : Map[String, Json] = Map([])
      rule_fields["rule"] = Json::string(entry.rule.name())
      rule_fields["count"] = Json::number(entry.count.to_double())
      Json::object(rule_fields)
    }),
  )
  fields["epoch_run_counts"] = Json::array(
    summary.epoch_runs.map(fn(entry) {
      let epoch_fields = match audit_json_epoch(entry.epoch) {
        Object(value) => value
        _ => Map([])
      }
      epoch_fields["run_count"] = Json::number(entry.count.to_double())
      epoch_fields["status_counts"] = Json::object({
        "InControl": Json::number(entry.in_control.to_double()),
        "Warning": Json::number(entry.warning.to_double()),
        "RequiresReview": Json::number(entry.requires_review.to_double()),
        "Incomplete": Json::number(entry.incomplete.to_double()),
      })
      epoch_fields["rule_hit_counts"] = audit_json_epoch_rule_hits(
        summary,
        entry.epoch,
      )
      epoch_fields["control_rule_hit_counts"] = audit_json_epoch_control_rule_hits(
        summary,
        entry.epoch,
      )
      Json::object(epoch_fields)
    }),
  )
  fields["control_rule_hit_counts"] = Json::array(
    summary.control_rule_hits.map(fn(entry) {
      let control_fields : Map[String, Json] = Map([])
      control_fields["control_level_id"] = Json::string(entry.control_level_id)
      control_fields["rule"] = Json::string(entry.rule.name())
      control_fields["count"] = Json::number(entry.count.to_double())
      Json::object(control_fields)
    }),
  )
  fields["control_observation_coverage"] = Json::array(
    summary.control_coverage.map(fn(entry) {
      let coverage_fields = match audit_json_epoch(entry.epoch) {
        Object(value) => value
        _ => Map([])
      }
      coverage_fields["control_level_id"] = Json::string(entry.control_level_id)
      coverage_fields["required"] = Json::boolean(entry.required)
      coverage_fields["expected_runs"] = Json::number(
        entry.expected_runs.to_double(),
      )
      coverage_fields["observed_runs"] = Json::number(
        entry.observed_runs.to_double(),
      )
      coverage_fields["missing_runs"] = Json::number(
        entry.missing_runs.to_double(),
      )
      coverage_fields["precision_mismatch_runs"] = Json::number(
        entry.precision_mismatch_runs.to_double(),
      )
      Json::object(coverage_fields)
    }),
  )
  fields["control_observation_counts"] = Json::object({
    "expected": Json::number(summary.expected_control_observations.to_double()),
    "observed": Json::number(summary.observed_control_observations.to_double()),
    "missing": Json::number(summary.missing_control_observations.to_double()),
    "precision_mismatch": Json::number(
      summary.precision_mismatch_control_observations.to_double(),
    ),
    "required_missing": Json::number(
      summary.required_missing_control_observations.to_double(),
    ),
  })
  fields["track_issue_count"] = Json::number(summary.track_issues.to_double())
  Json::object(fields)
}

///|
/// Render a versioned JSON audit report containing the assay configuration,
/// epoch snapshots, assessments, issues, and reconciliable track summaries.
pub fn render_audit_json(
  program : @assay.AssayProgram,
  runs : Array[@run.QCRun],
  trajectory : @evidence.Trajectory,
) -> String {
  let fields : Map[String, Json] = Map([])
  fields["schema_version"] = Json::number(1.0)
  fields["program"] = audit_json_program(program)
  let summary = audit_summary(program, runs, trajectory)
  fields["runs"] = Json::array(runs.map(audit_json_run))
  fields["epochs"] = Json::array(
    summary.epoch_runs.map(fn(entry) { audit_json_epoch(entry.epoch) }),
  )
  fields["summary"] = audit_json_summary(summary)
  fields["assessments"] = Json::array(
    trajectory.assessments.map(json_assessment),
  )
  fields["input_issues"] = Json::array(trajectory.input_issues.map(json_issue))
  Json::object(fields).stringify(indent=2)
}

///|
/// Render a deterministic manifest for the files produced by the offline
/// review bundle. The manifest contains no source path or wall-clock time.
pub fn render_audit_manifest(
  program : @assay.AssayProgram,
  runs : Array[@run.QCRun],
  trajectory : @evidence.Trajectory,
) -> String {
  let summary = audit_summary(program, runs, trajectory)
  let fields : Map[String, Json] = Map([])
  fields["schema_version"] = Json::number(1.0)
  fields["report_schema_version"] = Json::number(1.0)
  fields["assay_id"] = Json::string(program.assay_id)
  fields["counts"] = Json::object({
    "input_runs": Json::number(summary.input_runs.to_double()),
    "assessed_runs": Json::number(summary.assessed_runs.to_double()),
    "epoch_count": Json::number(summary.epoch_runs.length().to_double()),
    "track_issue_count": Json::number(summary.track_issues.to_double()),
    "rule_hit_count": Json::number(summary.total_rule_hits.to_double()),
    "expected_control_observations": Json::number(
      summary.expected_control_observations.to_double(),
    ),
    "observed_control_observations": Json::number(
      summary.observed_control_observations.to_double(),
    ),
    "missing_control_observations": Json::number(
      summary.missing_control_observations.to_double(),
    ),
    "precision_mismatch_control_observations": Json::number(
      summary.precision_mismatch_control_observations.to_double(),
    ),
    "required_missing_control_observations": Json::number(
      summary.required_missing_control_observations.to_double(),
    ),
    "status_counts": Json::object({
      "InControl": Json::number(summary.in_control.to_double()),
      "Warning": Json::number(summary.warning.to_double()),
      "RequiresReview": Json::number(summary.requires_review.to_double()),
      "Incomplete": Json::number(summary.incomplete.to_double()),
    }),
  })
  fields["epoch_rule_hit_counts"] = Json::array(
    summary.epoch_runs.map(fn(entry) {
      Json::object({
        "epoch_id": Json::string(entry.epoch.id),
        "run_count": Json::number(entry.count.to_double()),
        "rule_hit_counts": audit_json_epoch_rule_hits(summary, entry.epoch),
        "control_rule_hit_counts": audit_json_epoch_control_rule_hits(
          summary,
          entry.epoch,
        ),
      })
    }),
  )
  fields["artifacts"] = Json::array(
    [
      {
        "path": Json::string("report.html"),
        "media_type": Json::string("text/html"),
      },
      {
        "path": Json::string("audit.json"),
        "media_type": Json::string("application/json"),
      },
      {
        "path": Json::string("audit.csv"),
        "media_type": Json::string("text/csv"),
      },
      {
        "path": Json::string("chart.svg"),
        "media_type": Json::string("image/svg+xml"),
      },
      {
        "path": Json::string("manifest.json"),
        "media_type": Json::string("application/json"),
      },
    ].map(fn(artifact) { Json::object(artifact) }),
  )
  Json::object(fields).stringify(indent=2)
}

///|
fn audit_csv_quote(value : String) -> String {
  let output = StringBuilder()
  output.write_char('"')
  for character in value.to_array() {
    if character == '"' {
      output.write_string("\"\"")
    } else {
      output.write_char(character)
    }
  }
  output.write_char('"')
  output.to_string()
}

///|
fn audit_csv_row(values : Array[String]) -> String {
  let output = StringBuilder()
  for index, value in values {
    if index > 0 {
      output.write_char(',')
    }
    output.write_string(audit_csv_quote(value))
  }
  output.to_string()
}

///|
fn audit_csv_headers() -> Array[String] {
  [
    "record_type", "assay_id", "program_unit", "program_precision", "run_id", "sequence",
    "status", "epoch_id", "reagent_lot", "control_lot", "calibration_id", "program_version",
    "rule", "scope", "reason", "issue_code", "issue_message", "point_run_id", "point_sequence",
    "point_timestamp", "control_level_id", "value_scaled", "mean_scaled", "standard_deviation_scaled",
    "threshold_scaled", "direction", "precision", "unit",
  ]
}

///|
fn audit_csv_base(
  program : @assay.AssayProgram,
  record_type : String,
  assessment : @evidence.RunAssessment?,
  run : @run.QCRun?,
) -> Array[String] {
  let epoch = match run {
    Some(value) => Some(value.epoch)
    None => None
  }
  let run_id = match assessment {
    Some(value) => value.run_id
    None =>
      match run {
        Some(value) => value.run_id
        None => ""
      }
  }
  let sequence = match assessment {
    Some(value) => value.sequence.to_string()
    None =>
      match run {
        Some(value) => value.sequence.to_string()
        None => ""
      }
  }
  let status = match assessment {
    Some(value) => value.status.name()
    None => ""
  }
  let (epoch_id, reagent_lot, control_lot, calibration_id, program_version) = match
    epoch {
    Some(value) =>
      (
        value.id,
        value.reagent_lot,
        value.control_lot,
        value.calibration_id,
        value.program_version,
      )
    None => ("", "", "", "", "")
  }
  [
    record_type,
    program.assay_id,
    program.unit,
    program.precision.to_string(),
    run_id,
    sequence,
    status,
    epoch_id,
    reagent_lot,
    control_lot,
    calibration_id,
    program_version,
  ] +
  Array::make(16, "")
}

///|
fn audit_csv_evidence(
  program : @assay.AssayProgram,
  assessment : @evidence.RunAssessment,
  run : @run.QCRun?,
  hit : @evidence.RuleHit,
  evidence : @evidence.EvidencePoint,
) -> Array[String] {
  let values = audit_csv_base(program, "evidence", Some(assessment), run)
  values[12] = hit.rule.name()
  values[13] = scope_name(hit.scope)
  values[14] = hit.reason
  values[17] = evidence.run_id
  values[18] = evidence.sequence.to_string()
  values[19] = evidence.timestamp
  values[20] = evidence.control_level_id
  values[21] = evidence.value.to_string()
  values[22] = evidence.mean.to_string()
  values[23] = evidence.standard_deviation.to_string()
  values[24] = evidence.threshold.to_string()
  values[25] = direction_name(evidence.direction)
  values[26] = evidence.precision.to_string()
  values[27] = evidence.unit
  values
}

///|
fn audit_csv_observation(
  program : @assay.AssayProgram,
  run : @run.QCRun,
  point : @run.ControlPoint,
) -> Array[String] {
  let values = audit_csv_base(program, "observation", None, Some(run))
  values[17] = run.run_id
  values[18] = run.sequence.to_string()
  values[19] = run.timestamp
  values[20] = point.control_level_id
  values[21] = point.value.to_string()
  values[26] = point.precision.to_string()
  values[27] = program.unit
  for level in program.levels {
    if level.id == point.control_level_id {
      values[22] = level.mean.to_string()
      values[23] = level.standard_deviation.to_string()
    }
  }
  values
}

///|
fn audit_csv_issue(
  program : @assay.AssayProgram,
  assessment : @evidence.RunAssessment?,
  run : @run.QCRun?,
  issue : @evidence.InputIssue,
  record_type : String,
) -> Array[String] {
  let values = audit_csv_base(program, record_type, assessment, run)
  values[15] = issue.code.name()
  values[16] = issue.message
  values
}

///|
/// Render one CSV row for each assessment, observation, evidence point, and issue.
/// Scaled measurement columns remain exact integers; `precision` states the scale.
pub fn render_audit_csv(
  program : @assay.AssayProgram,
  runs : Array[@run.QCRun],
  trajectory : @evidence.Trajectory,
) -> String {
  let output = StringBuilder()
  output.write_string(audit_csv_headers().join(","))
  output.write_char('\n')
  for index, run in runs {
    let assessment : @evidence.RunAssessment? = trajectory.assessments.get(
      index,
    )
    match assessment {
      Some(value) => {
        output.write_string(
          audit_csv_row(
            audit_csv_base(program, "assessment", Some(value), Some(run)),
          ),
        )
        output.write_char('\n')
      }
      None => ()
    }
    for point in run.observations {
      output.write_string(
        audit_csv_row(audit_csv_observation(program, run, point)),
      )
      output.write_char('\n')
    }
    match assessment {
      Some(value) => {
        for hit in value.hits {
          for evidence in hit.evidence {
            output.write_string(
              audit_csv_row(
                audit_csv_evidence(program, value, Some(run), hit, evidence),
              ),
            )
            output.write_char('\n')
          }
        }
        for issue in value.input_issues {
          output.write_string(
            audit_csv_row(
              audit_csv_issue(
                program,
                Some(value),
                Some(run),
                issue,
                "input_issue",
              ),
            ),
          )
          output.write_char('\n')
        }
      }
      None => ()
    }
  }
  for issue in trajectory.input_issues {
    let mut issue_run : @run.QCRun? = None
    for run in runs {
      if issue.run_id == Some(run.run_id) {
        issue_run = Some(run)
        break
      }
    }
    output.write_string(
      audit_csv_row(
        audit_csv_issue(program, None, issue_run, issue, "track_issue"),
      ),
    )
    output.write_char('\n')
  }
  output.to_string()
}

///|
/// Render Markdown with status, rule, and epoch counts before run evidence.
pub fn render_audit_markdown(
  program : @assay.AssayProgram,
  runs : Array[@run.QCRun],
  trajectory : @evidence.Trajectory,
) -> String {
  let summary = audit_summary(program, runs, trajectory)
  let output = StringBuilder()
  output.write_string("# QC replay — ")
  output.write_string(program.assay_id)
  output.write_string("\n\n## Summary\n\n")
  output.write_string("- Input runs: ")
  output.write_string(summary.input_runs.to_string())
  output.write_string("; assessed: ")
  output.write_string(summary.assessed_runs.to_string())
  output.write_string("\n- Statuses: InControl ")
  output.write_string(summary.in_control.to_string())
  output.write_string(", Warning ")
  output.write_string(summary.warning.to_string())
  output.write_string(", RequiresReview ")
  output.write_string(summary.requires_review.to_string())
  output.write_string(", Incomplete ")
  output.write_string(summary.incomplete.to_string())
  output.write_string("\n- Rule hits: ")
  if summary.total_rule_hits == 0 {
    output.write_string("none")
  } else {
    let mut first = true
    for entry in summary.rule_hits {
      if entry.count > 0 {
        if !first {
          output.write_string(", ")
        }
        output.write_string(entry.rule.name())
        output.write_char(' ')
        output.write_string(entry.count.to_string())
        first = false
      }
    }
  }
  output.write_string("\n- Epochs: ")
  if summary.epoch_runs.length() == 0 {
    output.write_string("none")
  } else {
    for index, entry in summary.epoch_runs {
      if index > 0 {
        output.write_string("; ")
      }
      output.write_string(entry.epoch.id)
      output.write_string(" ")
      output.write_string(entry.count.to_string())
      output.write_string(" run(s)")
    }
  }
  output.write_string("\n- Epoch rule hits: ")
  let mut has_epoch_rule_hit = false
  for entry in summary.epoch_rule_hits {
    if !has_epoch_rule_hit {
      has_epoch_rule_hit = true
    } else {
      output.write_string("; ")
    }
    output.write_string(entry.epoch.id)
    output.write_char(' ')
    output.write_string(entry.rule.name())
    output.write_char(' ')
    output.write_string(entry.count.to_string())
  }
  if !has_epoch_rule_hit {
    output.write_string("none")
  }
  output.write_string("\n- Epoch/control rule hits: ")
  let mut has_epoch_control_rule_hit = false
  for entry in summary.epoch_control_rule_hits {
    if !has_epoch_control_rule_hit {
      has_epoch_control_rule_hit = true
    } else {
      output.write_string("; ")
    }
    output.write_string(entry.epoch.id)
    output.write_char(' ')
    output.write_string(entry.control_level_id)
    output.write_char('/')
    output.write_string(entry.rule.name())
    output.write_char(' ')
    output.write_string(entry.count.to_string())
  }
  if !has_epoch_control_rule_hit {
    output.write_string("none")
  }
  output.write_string("\n- Track issues: ")
  output.write_string(summary.track_issues.to_string())
  output.write_string("\n\n## Assessments\n\n")
  output.write_string(render_assessment_details_markdown(trajectory))
  output.to_string()
}