///|
/// A deterministic repair operation that can be audited before application.
pub enum RepairRule {
  SortChanges
  RemoveUnknownSignals
  ClampNegativeTime
  ClampPastEnd
  RemoveDuplicateDeclarations
  NormalizeValues
  DropZeroWidthSignals
} derive(Debug, Eq)

///|
/// Severity assigned to a repair finding.
pub enum RepairFindingLevel {
  Informational
  Corrected
  Rejected
  Fatal
} derive(Debug, Eq)

///|
/// One repair finding with source and resulting state.
pub struct RepairFinding {
  rule : RepairRule
  level : RepairFindingLevel
  signal_id : String?
  time : Int64?
  message : String
} derive(Debug)

///|
/// Repair policy for imported waveform data.
pub struct RepairOptions {
  rules : Array[RepairRule]
  preserve_unknown_values : Bool
  end_time : Int64?
  deduplicate_changes : Bool
} derive(Debug)

///|
/// Repaired trace and an audit trail of every material decision.
pub struct RepairResult {
  trace : Trace
  findings : Array[RepairFinding]
  changed : Bool
  rejected_changes : Int
  repaired_changes : Int
} derive(Debug)

///|
/// List every supported repair rule.
pub fn repair_rule_catalog() -> Array[RepairRule] {
  [
    SortChanges,
    RemoveUnknownSignals,
    ClampNegativeTime,
    ClampPastEnd,
    RemoveDuplicateDeclarations,
    NormalizeValues,
    DropZeroWidthSignals,
  ]
}

///|
/// List every finding level.
pub fn repair_finding_level_catalog() -> Array[RepairFindingLevel] {
  [Informational, Corrected, Rejected, Fatal]
}

///|
/// Create a conservative repair policy for parser output.
pub fn default_repair_options() -> RepairOptions {
  {
    rules: [SortChanges, RemoveUnknownSignals, NormalizeValues],
    preserve_unknown_values: true,
    end_time: None,
    deduplicate_changes: true,
  }
}

///|
/// Add one repair rule if it is not already present.
pub fn RepairOptions::with_rule(
  self : RepairOptions,
  rule : RepairRule,
) -> RepairOptions {
  if self.rules.contains(rule) {
    self
  } else {
    let rules = self.rules.copy()
    rules.push(rule)
    { ..self, rules, }
  }
}

///|
/// Set the expected trace end time used by time clamping.
pub fn RepairOptions::with_end_time(
  self : RepairOptions,
  end_time : Int64?,
) -> RepairOptions {
  { ..self, end_time, }
}

///|
/// Set whether unknown/X/Z values are retained.
pub fn RepairOptions::with_unknown_values(
  self : RepairOptions,
  preserve : Bool,
) -> RepairOptions {
  { ..self, preserve_unknown_values: preserve }
}

///|
/// Execute a repair policy and return an auditable result.
pub fn repair_trace_with_options(
  trace : Trace,
  options : RepairOptions,
) -> RepairResult {
  let findings : Array[RepairFinding] = []
  let active_ids = repair_unique_signal_ids(trace, findings, options)
  let signals = repair_signals(trace.signals, findings, options)
  let changes = repair_changes(trace, active_ids, findings, options)
  let sorted = if options.rules.contains(SortChanges) {
    repair_sort_changes(changes)
  } else {
    changes
  }
  let end_time = match options.end_time {
    Some(value) => Int64::max(0L, value)
    None => Int64::max(0L, trace.end_time)
  }
  let result_trace = {
    header: trace.header,
    signals,
    changes: sorted,
    end_time,
  }
  {
    trace: result_trace,
    repaired_changes: findings
    .filter(fn(item) { item.level is Corrected })
    .length(),
    rejected_changes: findings
    .filter(fn(item) { item.level is Rejected })
    .length(),
    changed: repair_trace_changed(trace, result_trace),
    findings,
  }
}

///|
/// Run the default repair policy.
pub fn repair_trace_default(trace : Trace) -> RepairResult {
  repair_trace_with_options(trace, default_repair_options())
}

///|
/// Return whether any finding indicates a correction or rejection.
pub fn RepairResult::has_changes(self : RepairResult) -> Bool {
  self.changed
}

///|
/// Return all findings for a particular rule.
pub fn RepairResult::findings_for(
  self : RepairResult,
  rule : RepairRule,
) -> Array[RepairFinding] {
  self.findings.filter(fn(item) { item.rule == rule })
}

///|
/// Render an audit report for terminal output.
pub fn RepairResult::to_text(self : RepairResult) -> String {
  let out = StringBuilder()
  out.write_string("changed=" + self.changed.to_string())
  out.write_string(" repaired=" + self.repaired_changes.to_string())
  out.write_string(" rejected=" + self.rejected_changes.to_string() + "\n")
  for finding in self.findings {
    out.write_string(repair_rule_name(finding.rule))
    out.write_char(' ')
    out.write_string(repair_level_name(finding.level))
    out.write_string(": " + finding.message + "\n")
  }
  out.to_string()
}

///|
/// Render repair findings as CSV.
pub fn repair_findings_csv(findings : Array[RepairFinding]) -> String {
  let out = StringBuilder()
  out.write_string("rule,level,signal,time,message\n")
  for finding in findings {
    out.write_string(repair_rule_name(finding.rule))
    out.write_char(',')
    out.write_string(repair_level_name(finding.level))
    out.write_char(',')
    out.write_string(repair_optional_text(finding.signal_id))
    out.write_char(',')
    out.write_string(repair_optional_time(finding.time))
    out.write_char(',')
    out.write_string(escape_text(finding.message))
    out.write_char('\n')
  }
  out.to_string()
}

///|
/// Render the repair result as JSON.
pub fn RepairResult::to_json(self : RepairResult) -> String {
  let findings = self.findings.map(repair_finding_json).join(",")
  "{\"changed\":" +
  self.changed.to_string() +
  ",\"repaired_changes\":" +
  self.repaired_changes.to_string() +
  ",\"rejected_changes\":" +
  self.rejected_changes.to_string() +
  ",\"findings\":[" +
  findings +
  "]}"
}

///|
/// Remove duplicate declarations and collect the surviving identifiers.
fn repair_unique_signal_ids(
  trace : Trace,
  findings : Array[RepairFinding],
  options : RepairOptions,
) -> Array[String] {
  let ids : Array[String] = []
  for signal in trace.signals {
    if ids.contains(signal.id) {
      if options.rules.contains(RemoveDuplicateDeclarations) {
        findings.push({
          rule: RemoveDuplicateDeclarations,
          level: Corrected,
          signal_id: Some(signal.id),
          time: None,
          message: "duplicate signal declaration removed",
        })
      }
    } else {
      ids.push(signal.id)
    }
  }
  ids
}

///|
/// Apply declaration-level rules.
fn repair_signals(
  source : Array[Signal],
  findings : Array[RepairFinding],
  options : RepairOptions,
) -> Array[Signal] {
  let result : Array[Signal] = []
  let seen : Array[String] = []
  for signal in source {
    if seen.contains(signal.id) &&
      options.rules.contains(RemoveDuplicateDeclarations) {
      continue
    }
    if signal.width <= 0 && options.rules.contains(DropZeroWidthSignals) {
      findings.push({
        rule: DropZeroWidthSignals,
        level: Rejected,
        signal_id: Some(signal.id),
        time: None,
        message: "zero-width signal declaration removed",
      })
      continue
    }
    let normalized = if options.rules.contains(NormalizeValues) {
      {
        ..signal,
        width: Int::max(1, signal.width),
        kind: repair_kind(signal.kind),
      }
    } else {
      signal
    }
    result.push(normalized)
    seen.push(signal.id)
  }
  result
}

///|
/// Apply time, identifier, value and duplicate rules to changes.
fn repair_changes(
  trace : Trace,
  ids : Array[String],
  findings : Array[RepairFinding],
  options : RepairOptions,
) -> Array[Change] {
  let result : Array[Change] = []
  let end_time = match options.end_time {
    Some(value) => value
    None => trace.end_time
  }
  for change in trace.changes {
    if !ids.contains(change.id) && options.rules.contains(RemoveUnknownSignals) {
      findings.push({
        rule: RemoveUnknownSignals,
        level: Rejected,
        signal_id: Some(change.id),
        time: Some(change.time),
        message: "change references an undeclared signal",
      })
      continue
    }
    let mut time = change.time
    if time < 0L && options.rules.contains(ClampNegativeTime) {
      findings.push({
        rule: ClampNegativeTime,
        level: Corrected,
        signal_id: Some(change.id),
        time: Some(time),
        message: "negative timestamp clamped to zero",
      })
      time = 0L
    }
    if time > end_time && options.rules.contains(ClampPastEnd) {
      findings.push({
        rule: ClampPastEnd,
        level: Corrected,
        signal_id: Some(change.id),
        time: Some(time),
        message: "timestamp clamped to trace end",
      })
      time = Int64::max(0L, end_time)
    }
    let value = if options.rules.contains(NormalizeValues) {
      change.value.normalized()
    } else {
      change.value
    }
    if !options.preserve_unknown_values && value.is_unknown() {
      findings.push({
        rule: NormalizeValues,
        level: Rejected,
        signal_id: Some(change.id),
        time: Some(time),
        message: "unknown value rejected by policy",
      })
      continue
    }
    let repaired = { time, id: change.id, value }
    if options.deduplicate_changes && repair_duplicate_exists(result, repaired) {
      findings.push({
        rule: SortChanges,
        level: Rejected,
        signal_id: Some(change.id),
        time: Some(time),
        message: "duplicate change removed",
      })
    } else {
      result.push(repaired)
    }
  }
  result
}

///|
/// Sort changes by timestamp while preserving declaration order at ties.
fn repair_sort_changes(changes : Array[Change]) -> Array[Change] {
  let result : Array[Change] = []
  for change in changes {
    let mut inserted = false
    for index, current in result {
      if change.time < current.time {
        result.insert(index, change)
        inserted = true
        break
      }
    }
    if !inserted {
      result.push(change)
    }
  }
  result
}

///|
/// Detect an identical signal/time/value tuple already retained.
fn repair_duplicate_exists(changes : Array[Change], candidate : Change) -> Bool {
  changes.any(fn(item) {
    item.id == candidate.id &&
    item.time == candidate.time &&
    item.value == candidate.value
  })
}

///|
/// Normalize an absent kind label without changing a valid label.
fn repair_kind(kind : String) -> String {
  if kind.length() == 0 {
    "wire"
  } else {
    kind
  }
}

///|
/// Compare material trace fields for the result flag.
fn repair_trace_changed(left : Trace, right : Trace) -> Bool {
  left.end_time != right.end_time ||
  left.signals.length() != right.signals.length() ||
  left.changes.length() != right.changes.length() ||
  !traces_equal(left, right)
}

///|
/// Get a stable rule name.
pub fn repair_rule_name(rule : RepairRule) -> String {
  match rule {
    SortChanges => "sort-changes"
    RemoveUnknownSignals => "remove-unknown-signals"
    ClampNegativeTime => "clamp-negative-time"
    ClampPastEnd => "clamp-past-end"
    RemoveDuplicateDeclarations => "remove-duplicate-declarations"
    NormalizeValues => "normalize-values"
    DropZeroWidthSignals => "drop-zero-width-signals"
  }
}

///|
/// Get a stable finding level name.
pub fn repair_level_name(level : RepairFindingLevel) -> String {
  match level {
    Informational => "info"
    Corrected => "corrected"
    Rejected => "rejected"
    Fatal => "fatal"
  }
}

///|
/// Optional text field for CSV.
fn repair_optional_text(value : String?) -> String {
  match value {
    Some(item) => escape_text(item)
    None => ""
  }
}

///|
/// Optional timestamp field for CSV.
fn repair_optional_time(value : Int64?) -> String {
  match value {
    Some(item) => item.to_string()
    None => ""
  }
}

///|
/// Encode one finding as JSON.
fn repair_finding_json(item : RepairFinding) -> String {
  "{\"rule\":\"" +
  repair_rule_name(item.rule) +
  "\",\"level\":\"" +
  repair_level_name(item.level) +
  "\",\"signal\":" +
  repair_optional_json_text(item.signal_id) +
  ",\"time\":" +
  repair_optional_json_time(item.time) +
  ",\"message\":\"" +
  escape_text(item.message) +
  "\"}"
}

///|
/// Encode an optional string as JSON.
fn repair_optional_json_text(value : String?) -> String {
  match value {
    Some(item) => "\"" + escape_text(item) + "\""
    None => "null"
  }
}

///|
/// Encode an optional timestamp as JSON.
fn repair_optional_json_time(value : Int64?) -> String {
  match value {
    Some(item) => item.to_string()
    None => "null"
  }
}