///|
/// Build execution outcome used by frontends, CI summaries, and telemetry.
pub(all) enum BuildReportStatus {
  Pending
  Succeeded
  Failed
} derive(Debug, Eq)

///|
/// A compact, deterministic summary of one materialized build.
pub(all) struct BuildReport {
  status : BuildReportStatus
  target : String
  waves : Int
  commands : Int
  cache_hits : Int
  cache_misses : Int
  failures : Array[String]
  outputs : Array[String]
} derive(Debug, Eq)

///|
/// Start an empty report for a target.
pub fn build_report(target : String) -> BuildReport {
  {
    status: Pending,
    target,
    waves: 0,
    commands: 0,
    cache_hits: 0,
    cache_misses: 0,
    failures: [],
    outputs: [],
  }
}

///|
/// Record a scheduled wave without changing the report's target.
pub fn BuildReport::record_wave(
  self : BuildReport,
  command_count : Int,
) -> BuildReport {
  { ..self, waves: self.waves + 1, commands: self.commands + command_count }
}

///|
/// Record one cache hit and its materialized output.
pub fn BuildReport::record_cache_hit(
  self : BuildReport,
  output : String,
) -> BuildReport {
  let outputs = self.outputs.copy()
  if !outputs.contains(output) {
    outputs.push(output)
  }
  { ..self, cache_hits: self.cache_hits + 1, outputs }
}

///|
/// Record one cache miss and its materialized output.
pub fn BuildReport::record_cache_miss(
  self : BuildReport,
  output : String,
) -> BuildReport {
  let outputs = self.outputs.copy()
  if !outputs.contains(output) {
    outputs.push(output)
  }
  { ..self, cache_misses: self.cache_misses + 1, outputs }
}

///|
/// Record a failure and retain its first-seen order.
pub fn BuildReport::record_failure(
  self : BuildReport,
  message : String,
) -> BuildReport {
  let failures = self.failures.copy()
  failures.push(message)
  { ..self, status: Failed, failures }
}

///|
/// Mark a report successful only when no failure was recorded.
pub fn BuildReport::finish(self : BuildReport) -> BuildReport {
  if self.failures.is_empty() {
    { ..self, status: Succeeded }
  } else {
    self
  }
}

///|
/// Whether this report can be used as a successful incremental state.
pub fn BuildReport::is_success(self : BuildReport) -> Bool {
  self.status == Succeeded && self.failures.is_empty()
}

///|
/// Number of commands that actually missed the cache.
pub fn BuildReport::executed_commands(self : BuildReport) -> Int {
  self.cache_misses
}

///|
/// Number of cache observations recorded for this report.
pub fn BuildReport::cache_observations(self : BuildReport) -> Int {
  self.cache_hits + self.cache_misses
}

///|
/// Integer cache hit percentage, stable for empty reports.
pub fn BuildReport::cache_hit_percent(self : BuildReport) -> Int {
  let total = self.cache_observations()
  if total == 0 {
    100
  } else {
    self.cache_hits * 100 / total
  }
}

///|
/// Return a report with all counters and collections combined.
pub fn BuildReport::merge(
  self : BuildReport,
  other : BuildReport,
) -> BuildReport {
  let failures = self.failures.copy()
  for failure in other.failures {
    failures.push(failure)
  }
  let outputs = self.outputs.copy()
  for output in other.outputs {
    if !outputs.contains(output) {
      outputs.push(output)
    }
  }
  let status = if !failures.is_empty() {
    Failed
  } else if self.status == Succeeded && other.status == Succeeded {
    Succeeded
  } else {
    Pending
  }
  {
    status,
    target: self.target,
    waves: self.waves + other.waves,
    commands: self.commands + other.commands,
    cache_hits: self.cache_hits + other.cache_hits,
    cache_misses: self.cache_misses + other.cache_misses,
    failures,
    outputs,
  }
}

///|
fn report_status_text(status : BuildReportStatus) -> String {
  match status {
    Pending => "pending"
    Succeeded => "succeeded"
    Failed => "failed"
  }
}

///|
/// Render a machine-readable, line-oriented summary for CI logs.
pub fn BuildReport::to_text(self : BuildReport) -> String {
  let lines : Array[String] = []
  lines.push("status=" + report_status_text(self.status))
  lines.push("target=" + self.target)
  lines.push("waves=" + self.waves.to_string())
  lines.push("commands=" + self.commands.to_string())
  lines.push("executed=" + self.executed_commands().to_string())
  lines.push("cache_hits=" + self.cache_hits.to_string())
  lines.push("cache_misses=" + self.cache_misses.to_string())
  lines.push("cache_hit_percent=" + self.cache_hit_percent().to_string())
  lines.push("outputs=" + self.outputs.length().to_string())
  lines.push("failures=" + self.failures.length().to_string())
  for failure in self.failures {
    lines.push("failure=" + failure)
  }
  lines.join("\n")
}

///|
/// Validate invariants before persisting a report as build state.
pub fn BuildReport::validate(self : BuildReport) -> Result[Unit, String] {
  if self.waves < 0 {
    Err("wave count cannot be negative")
  } else if self.commands < 0 {
    Err("command count cannot be negative")
  } else if self.cache_hits < 0 || self.cache_misses < 0 {
    Err("cache counters cannot be negative")
  } else if self.cache_observations() > self.commands {
    Err("cache observations exceed scheduled commands")
  } else if self.status == Succeeded && !self.failures.is_empty() {
    Err("successful report cannot contain failures")
  } else {
    Ok(())
  }
}

///|
/// Whether the report has reached a terminal state.
pub fn BuildReport::is_terminal(self : BuildReport) -> Bool {
  self.status == Succeeded || self.status == Failed
}

///|
/// Whether at least one reusable artifact was observed.
pub fn BuildReport::has_cache_reuse(self : BuildReport) -> Bool {
  self.cache_hits > 0
}

///|
/// Return a stable one-line failure summary for dashboards.
pub fn BuildReport::failure_summary(self : BuildReport) -> String {
  if self.failures.is_empty() {
    "none"
  } else {
    self.failures.join(" | ")
  }
}

///|
/// Check whether an output was recorded by this build.
pub fn BuildReport::contains_output(
  self : BuildReport,
  output : String,
) -> Bool {
  self.outputs.contains(output)
}