///|
/// Reproducible audit record for analytics decisions.
pub struct AuditEntry {
  sequence : Int
  operation : String
  input_count : Int
  output_count : Int
  score : Double
  parameters : Array[String]
}

///|
pub struct AuditTrail {
  name : String
  entries : Array[AuditEntry]
  mut checksum : Double
}

///|
pub fn audit_trail(name : String) -> AuditTrail {
  { name, entries: [], checksum: 0.0 }
}

///|
pub fn audit_append(
  trail : AuditTrail,
  operation : String,
  input_count : Int,
  output_count : Int,
  score : Double,
  parameters : Array[String],
) -> Unit {
  let sequence = trail.entries.length()
  trail.entries.push({
    sequence,
    operation,
    input_count,
    output_count,
    score,
    parameters,
  })
  trail.checksum += score +
    input_count.to_double() * 0.001 +
    output_count.to_double() * 0.0001
}

///|
pub fn audit_from_pipeline(
  result : DataPipelineResult,
  name : String,
) -> AuditTrail {
  let trail = audit_trail(name)
  for stage in result.stages {
    audit_append(
      trail,
      stage.name,
      stage.input_count,
      stage.output_count,
      stage.score,
      stage.notes,
    )
  }
  trail
}

///|
pub fn audit_entry_count(trail : AuditTrail) -> Int {
  trail.entries.length()
}

///|
pub fn audit_operations(trail : AuditTrail) -> Array[String] {
  let result = []
  for entry in trail.entries {
    result.push(entry.operation)
  }
  result
}

///|
pub fn audit_scores(trail : AuditTrail) -> Array[Double] {
  let result = []
  for entry in trail.entries {
    result.push(entry.score)
  }
  result
}

///|
pub fn audit_total_input(trail : AuditTrail) -> Int {
  let mut total = 0
  for entry in trail.entries {
    total += entry.input_count
  }
  total
}

///|
pub fn audit_total_output(trail : AuditTrail) -> Int {
  let mut total = 0
  for entry in trail.entries {
    total += entry.output_count
  }
  total
}

///|
pub fn audit_quality(trail : AuditTrail) -> Double {
  if trail.entries.length() == 0 {
    1.0
  } else {
    mean(audit_scores(trail))
  }
}

///|
pub fn audit_reproducible(
  left : AuditTrail,
  right : AuditTrail,
  tolerance : Double,
) -> Bool {
  if left.entries.length() != right.entries.length() {
    return false
  }
  if audit_operations(left) != audit_operations(right) {
    return false
  }
  abs_double(left.checksum - right.checksum) <= tolerance
}

///|
pub fn audit_lines(trail : AuditTrail) -> Array[String] {
  let lines = [
    "name=" + trail.name,
    "entries=" + trail.entries.length().to_string(),
    "checksum=" + trail.checksum.to_string(),
  ]
  for entry in trail.entries {
    lines.push(
      entry.sequence.to_string() +
      "|" +
      entry.operation +
      "|" +
      entry.input_count.to_string() +
      "|" +
      entry.output_count.to_string() +
      "|" +
      entry.score.to_string(),
    )
  }
  lines
}

///|
pub fn audit_string(trail : AuditTrail) -> String {
  audit_lines(trail).join("\n")
}

///|
pub fn audit_digest(trail : AuditTrail) -> Array[Double] {
  [
    trail.entries.length().to_double(),
    trail.checksum,
    audit_total_input(trail).to_double(),
    audit_total_output(trail).to_double(),
    audit_quality(trail),
  ]
}

///|
pub fn audit_last(trail : AuditTrail) -> AuditEntry {
  if trail.entries.length() == 0 {
    {
      sequence: -1,
      operation: "none",
      input_count: 0,
      output_count: 0,
      score: 0.0,
      parameters: [],
    }
  } else {
    trail.entries[trail.entries.length() - 1]
  }
}

///|
pub fn audit_filter(
  trail : AuditTrail,
  minimum_score : Double,
) -> Array[AuditEntry] {
  let result = []
  for entry in trail.entries {
    if entry.score >= minimum_score {
      result.push(entry)
    }
  }
  result
}

///|
pub fn audit_summary(trail : AuditTrail) -> Array[Double] {
  [
    audit_quality(trail),
    trail.entries.length().to_double(),
    trail.checksum,
    audit_total_input(trail).to_double(),
    audit_total_output(trail).to_double(),
  ]
}