///|
pub(all) struct BatchConfig {
  config : RedactionConfig
  stop_on_error : Bool
  retain_empty : Bool
  max_documents : Int
} derive(Debug, Eq)

///|
pub fn BatchConfig::default() -> BatchConfig {
  {
    config: RedactionConfig::default(),
    stop_on_error: true,
    retain_empty: true,
    max_documents: 0,
  }
}

///|
pub(all) struct BatchDiagnostic {
  id : String
  ok : Bool
  message : String
  finding_count : Int
} derive(Debug, Eq)

///|
pub(all) struct BatchOutcome {
  results : Array[BatchResult]
  diagnostics : Array[BatchDiagnostic]
  totals : AuditTotals
} derive(Debug)

///|
pub fn batch_total_findings(results : Array[BatchResult]) -> Int {
  results.fold(init=0, (total, item) => total + item.result.findings.length())
}

///|
pub fn batch_total_applied(results : Array[BatchResult]) -> Int {
  results.fold(init=0, (total, item) => total + item.result.offsets.length())
}

///|
pub fn batch_kind_counts(results : Array[BatchResult]) -> Map[String, Int] {
  let counts : Map[String, Int] = Map([])
  for item in results {
    merge_counts(counts, item.result.audit.counts)
  }
  counts
}

///|
pub fn batch_risk_counts(results : Array[BatchResult]) -> Map[String, Int] {
  let counts : Map[String, Int] = Map([])
  for item in results {
    for finding in item.result.findings {
      let key = "\{risk_level(finding)}"
      counts[key] = counts.get_or_default(key, 0) + 1
    }
  }
  counts
}

///|
pub fn batch_totals(results : Array[BatchResult]) -> AuditTotals {
  let by_kind = batch_kind_counts(results)
  let by_risk = batch_risk_counts(results)
  let mut protected_count = 0
  for item in results {
    protected_count += item.result.audit.finding_count -
      item.result.audit.applied_count
  }
  {
    documents: results.length(),
    findings: batch_total_findings(results),
    applied: batch_total_applied(results),
    protected_count,
    by_kind,
    by_risk,
  }
}

///|
pub fn redact_batch_with_config(
  items : Array[BatchItem],
  batch_config : BatchConfig,
  rules? : Array[Rule] = comprehensive_rules(),
) -> BatchOutcome raise DeidError {
  let results = []
  let diagnostics = []
  let limit = if batch_config.max_documents <= 0 ||
    batch_config.max_documents > items.length() {
    items.length()
  } else {
    batch_config.max_documents
  }
  for i in 0.. {
          diagnostics.push({
            id: item.id,
            ok: false,
            message: "\{err}",
            finding_count: 0,
          })
          if batch_config.stop_on_error {
            raise err
          }
        }
      }
    }
  }
  { results, diagnostics, totals: batch_totals(results) }
}

///|
pub fn batch_failed_diagnostics(
  outcome : BatchOutcome,
) -> Array[BatchDiagnostic] {
  outcome.diagnostics.filter(fn(item) { !item.ok })
}

///|
pub fn batch_success_ratio(outcome : BatchOutcome) -> Float {
  if outcome.diagnostics.is_empty() {
    1.0
  } else {
    let passed = outcome.diagnostics.filter(fn(item) { item.ok }).length()
    Float::from_int(passed) / Float::from_int(outcome.diagnostics.length())
  }
}

///|
pub fn batch_contains_critical(outcome : BatchOutcome) -> Bool {
  outcome.results.any(fn(item) { audit_has_critical(item.result.audit) })
}

///|
pub fn batch_ids(results : Array[BatchResult]) -> Array[String] {
  results.map(fn(item) { item.id })
}

///|
pub fn batch_findings(results : Array[BatchResult]) -> Array[Finding] {
  let findings = []
  for item in results {
    findings.append(item.result.findings)
  }
  findings
}

///|
pub fn batch_audit_json(outcome : BatchOutcome) -> String {
  let items = outcome.results.map(fn(item) {
    "{\"id\":\{json_string(item.id)},\"result\":\{result_to_json(item.result)}}"
  })
  "{" +
  "\"documents\":\{outcome.totals.documents}," +
  "\"findings\":\{outcome.totals.findings}," +
  "\"applied\":\{outcome.totals.applied}," +
  "\"results\":[" +
  items.join(",") +
  "]," +
  "\"diagnostics\":[" +
  outcome.diagnostics
  .map(fn(item) {
    "{\"id\":\{json_string(item.id)},\"ok\":\{item.ok},\"message\":\{json_string(item.message)}}"
  })
  .join(",") +
  "]" +
  "}"
}

///|
pub fn batch_diagnostics_csv(outcome : BatchOutcome) -> String {
  let lines = ["id,ok,message,finding_count"]
  for item in outcome.diagnostics {
    lines.push(
      [
        csv_cell(item.id),
        "\{item.ok}",
        csv_cell(item.message),
        "\{item.finding_count}",
      ].join(","),
    )
  }
  lines.join("\n")
}

///|
pub fn deduplicate_batch(items : Array[BatchItem]) -> Array[BatchItem] {
  let result = []
  let seen : Map[String, Unit] = Map([])
  for item in items {
    let key = stable_hash(item.text)
    if !seen.contains(key) {
      seen[key] = ()
      result.push(item)
    }
  }
  result
}

///|
pub fn batch_partition(
  items : Array[BatchItem],
  size : Int,
) -> Array[Array[BatchItem]] {
  if size <= 0 {
    [items.copy()]
  } else {
    let groups = []
    let mut current = []
    for item in items {
      current.push(item)
      if current.length() == size {
        groups.push(current)
        current = []
      }
    }
    if !current.is_empty() {
      groups.push(current)
    }
    groups
  }
}

///|
pub fn merge_audit_totals(
  left : AuditTotals,
  right : AuditTotals,
) -> AuditTotals {
  let by_kind = left.by_kind.copy()
  merge_counts(by_kind, right.by_kind)
  let by_risk = left.by_risk.copy()
  merge_counts(by_risk, right.by_risk)
  {
    documents: left.documents + right.documents,
    findings: left.findings + right.findings,
    applied: left.applied + right.applied,
    protected_count: left.protected_count + right.protected_count,
    by_kind,
    by_risk,
  }
}

///|
pub fn merge_batch_outcomes(
  left : BatchOutcome,
  right : BatchOutcome,
) -> BatchOutcome {
  let results = left.results.copy()
  results.append(right.results)
  let diagnostics = left.diagnostics.copy()
  diagnostics.append(right.diagnostics)
  {
    results,
    diagnostics,
    totals: merge_audit_totals(left.totals, right.totals),
  }
}

///|
pub fn batch_consistency_issues(outcome : BatchOutcome) -> Array[String] {
  let issues = []
  if outcome.totals.documents != outcome.results.length() {
    issues.push("document total does not match result count")
  }
  if outcome.totals.findings != batch_total_findings(outcome.results) {
    issues.push("finding total does not match result count")
  }
  if outcome.totals.applied != batch_total_applied(outcome.results) {
    issues.push("applied total does not match result count")
  }
  issues
}