///|
/// A coverage state is derived from all evidence linked to a requirement.
/// Verified evidence wins, because a requirement may retain historical failed
/// evidence alongside a newer passing test or release artifact.
pub(all) enum CoverageState {
  VerifiedCoverage
  PendingCoverage
  RejectedCoverage
  MissingCoverage
} derive(Eq, Debug)

///|
pub(all) struct TraceRow {
  requirement_id : String
  level : RequirementLevel
  text : String
  capability_ids : Array[String]
  evidence_ids : Array[String]
  verified_evidence_ids : Array[String]
  state : CoverageState
} derive(Eq, Debug)

///|
pub(all) struct EvidenceSummary {
  kind : EvidenceKind
  total : Int
  verified : Int
  pending : Int
  rejected : Int
} derive(Eq, Debug)

///|
pub(all) struct TraceMatrix {
  project : String
  rows : Array[TraceRow]
  evidence_summaries : Array[EvidenceSummary]
} derive(Eq, Debug)

///|
pub fn CoverageState::label(self : CoverageState) -> String {
  match self {
    VerifiedCoverage => "verified"
    PendingCoverage => "pending"
    RejectedCoverage => "rejected"
    MissingCoverage => "missing"
  }
}

///|
pub fn CoverageState::is_satisfied(self : CoverageState) -> Bool {
  self == VerifiedCoverage
}

///|
/// Build a requirement-to-evidence matrix that can be rendered independently of
/// the full validator report. It is useful when a maintainer wants to see which
/// promises lack evidence without scanning all findings.
pub fn build_trace_matrix(spec : BoundarySpec) -> TraceMatrix {
  let rows : Array[TraceRow] = []
  for requirement in spec.requirements {
    let linked = spec.evidence_for_requirement(requirement.id)
    let evidence_ids : Array[String] = []
    let verified_ids : Array[String] = []
    for item in linked {
      evidence_ids.push(item.id)
      if item.status == Verified {
        verified_ids.push(item.id)
      }
    }
    let evidence_ids = sort_strings_ascending(evidence_ids)
    let verified_ids = sort_strings_ascending(verified_ids)
    rows.push({
      requirement_id: requirement.id,
      level: requirement.level,
      text: requirement.text,
      capability_ids: requirement.capabilities.copy(),
      evidence_ids,
      verified_evidence_ids: verified_ids,
      state: coverage_state(linked),
    })
  }
  {
    project: spec.project,
    rows: sort_trace_rows(rows),
    evidence_summaries: build_evidence_summaries(spec),
  }
}

///|
pub fn TraceMatrix::row(
  self : TraceMatrix,
  requirement_id : StringView,
) -> TraceRow? {
  let expected = requirement_id.trim().to_owned()
  self.rows.iter().find_first(row => row.requirement_id == expected)
}

///|
pub fn TraceMatrix::rows_for_capability(
  self : TraceMatrix,
  capability_id : StringView,
) -> Array[TraceRow] {
  let expected = capability_id.trim().to_owned()
  self.rows.filter(row => row.capability_ids.any(id => id == expected))
}

///|
pub fn TraceMatrix::rows_in_state(
  self : TraceMatrix,
  state : CoverageState,
) -> Array[TraceRow] {
  self.rows.filter(row => row.state == state)
}

///|
pub fn TraceMatrix::covered_requirements(self : TraceMatrix) -> Int {
  self.rows.count_if(row => row.state.is_satisfied())
}

///|
pub fn TraceMatrix::coverage_percent(self : TraceMatrix) -> Int {
  if self.rows.is_empty() {
    0
  } else {
    self.covered_requirements() * 100 / self.rows.length()
  }
}

///|
pub fn TraceMatrix::must_requirements_ready(self : TraceMatrix) -> Bool {
  !self.rows.any(row => row.level == Must && !row.state.is_satisfied())
}

///|
pub fn TraceMatrix::outstanding_rows(self : TraceMatrix) -> Array[TraceRow] {
  self.rows.filter(row => !row.state.is_satisfied())
}

///|
pub fn TraceMatrix::summary(self : TraceMatrix) -> String {
  "coverage=" +
  self.covered_requirements().to_string() +
  "/" +
  self.rows.length().to_string() +
  " (" +
  self.coverage_percent().to_string() +
  "%), must-ready=" +
  bool_label(self.must_requirements_ready())
}

///|
pub fn TraceMatrix::to_markdown(self : TraceMatrix) -> String {
  let lines : Array[String] = []
  lines.push("# CapsuleTrace Evidence Matrix")
  lines.push("")
  lines.push("- Project: " + printable(self.project))
  lines.push("- " + self.summary())
  lines.push("")
  lines.push("## Requirements")
  lines.push("")
  lines.push(
    "| Requirement | Level | Capabilities | Evidence | Verified | State |",
  )
  lines.push("| --- | --- | --- | --- | --- | --- |")
  if self.rows.is_empty() {
    lines.push("| - | - | - | - | - | missing |")
  } else {
    for row in self.rows {
      lines.push(
        "| " +
        markdown_cell(row.requirement_id) +
        " | " +
        row.level.label() +
        " | " +
        markdown_cell(row.capability_ids.join(", ")) +
        " | " +
        markdown_cell(row.evidence_ids.join(", ")) +
        " | " +
        markdown_cell(row.verified_evidence_ids.join(", ")) +
        " | " +
        row.state.label() +
        " |",
      )
    }
  }
  lines.push("")
  lines.push("## Evidence Inventory")
  lines.push("")
  lines.push("| Kind | Total | Verified | Pending | Rejected |")
  lines.push("| --- | --- | --- | --- | --- |")
  for summary in self.evidence_summaries {
    lines.push(
      "| " +
      summary.kind.label() +
      " | " +
      summary.total.to_string() +
      " | " +
      summary.verified.to_string() +
      " | " +
      summary.pending.to_string() +
      " | " +
      summary.rejected.to_string() +
      " |",
    )
  }
  lines.join("\n")
}

///|
pub fn TraceMatrix::to_json_string(self : TraceMatrix) -> String {
  Json::object(
    Map([
      ("project", Json::string(self.project)),
      (
        "covered_requirements",
        Json::number(
          self.covered_requirements().to_double(),
          repr=self.covered_requirements().to_string(),
        ),
      ),
      (
        "total_requirements",
        Json::number(
          self.rows.length().to_double(),
          repr=self.rows.length().to_string(),
        ),
      ),
      (
        "coverage_percent",
        Json::number(
          self.coverage_percent().to_double(),
          repr=self.coverage_percent().to_string(),
        ),
      ),
      ("must_requirements_ready", Json::boolean(self.must_requirements_ready())),
      ("rows", trace_rows_to_json(self.rows)),
      (
        "evidence_summaries",
        evidence_summaries_to_json(self.evidence_summaries),
      ),
    ]),
  ).stringify(indent=2)
}

///|
pub fn EvidenceSummary::is_healthy(self : EvidenceSummary) -> Bool {
  self.total > 0 && self.rejected == 0
}

///|
pub fn TraceMatrix::summary_for_kind(
  self : TraceMatrix,
  kind : EvidenceKind,
) -> EvidenceSummary? {
  self.evidence_summaries.iter().find_first(summary => summary.kind == kind)
}

///|
fn coverage_state(items : Array[Evidence]) -> CoverageState {
  let mut has_pending = false
  let mut has_rejected = false
  for item in items {
    match item.status {
      Verified => return VerifiedCoverage
      Pending => has_pending = true
      Rejected => has_rejected = true
    }
  }
  if has_rejected {
    RejectedCoverage
  } else if has_pending {
    PendingCoverage
  } else {
    MissingCoverage
  }
}

///|
fn sort_trace_rows(values : Array[TraceRow]) -> Array[TraceRow] {
  let mut sorted : Array[TraceRow] = []
  for value in values {
    let next : Array[TraceRow] = []
    let mut inserted = false
    for existing in sorted {
      if !inserted &&
        value.requirement_id.lexical_compare(existing.requirement_id) < 0 {
        next.push(value)
        inserted = true
      }
      next.push(existing)
    }
    if !inserted {
      next.push(value)
    }
    sorted = next
  }
  sorted
}

///|
fn build_evidence_summaries(spec : BoundarySpec) -> Array[EvidenceSummary] {
  [
    evidence_summary_for(spec.evidence, Test),
    evidence_summary_for(spec.evidence, Example),
    evidence_summary_for(spec.evidence, CI),
    evidence_summary_for(spec.evidence, Doc),
    evidence_summary_for(spec.evidence, Release),
    evidence_summary_for(spec.evidence, Design),
  ]
}

///|
fn evidence_summary_for(
  evidence : Array[Evidence],
  kind : EvidenceKind,
) -> EvidenceSummary {
  let mut total = 0
  let mut verified = 0
  let mut pending = 0
  let mut rejected = 0
  for item in evidence {
    if item.kind == kind {
      total = total + 1
      match item.status {
        Verified => verified = verified + 1
        Pending => pending = pending + 1
        Rejected => rejected = rejected + 1
      }
    }
  }
  { kind, total, verified, pending, rejected }
}

///|
fn trace_rows_to_json(items : Array[TraceRow]) -> Json {
  let values : Array[Json] = []
  for item in items {
    values.push(
      Json::object(
        Map([
          ("requirement_id", Json::string(item.requirement_id)),
          ("level", Json::string(item.level.label())),
          ("text", Json::string(item.text)),
          ("capability_ids", strings_to_json(item.capability_ids)),
          ("evidence_ids", strings_to_json(item.evidence_ids)),
          ("verified_evidence_ids", strings_to_json(item.verified_evidence_ids)),
          ("state", Json::string(item.state.label())),
        ]),
      ),
    )
  }
  Json::array(values)
}

///|
fn evidence_summaries_to_json(items : Array[EvidenceSummary]) -> Json {
  let values : Array[Json] = []
  for item in items {
    values.push(
      Json::object(
        Map([
          ("kind", Json::string(item.kind.label())),
          (
            "total",
            Json::number(item.total.to_double(), repr=item.total.to_string()),
          ),
          (
            "verified",
            Json::number(
              item.verified.to_double(),
              repr=item.verified.to_string(),
            ),
          ),
          (
            "pending",
            Json::number(
              item.pending.to_double(),
              repr=item.pending.to_string(),
            ),
          ),
          (
            "rejected",
            Json::number(
              item.rejected.to_double(),
              repr=item.rejected.to_string(),
            ),
          ),
        ]),
      ),
    )
  }
  Json::array(values)
}