///|
pub fn findings_with_mode(
  findings : Array[Finding],
  mode : ReplacementMode,
) -> Array[Finding] {
  findings.map(fn(item) {
    { ..item, replacement: replacement_preview(item, mode) }
  })
}

///|
pub fn apply_findings_with_mode(
  input : String,
  findings : Array[Finding],
  mode : ReplacementMode,
) -> (String, Array[OffsetMap]) {
  apply_findings(input, findings_with_mode(findings, mode))
}

///|
pub fn redact_only_kinds(
  input : String,
  kinds : Array[PhiKind],
  mode : ReplacementMode,
) -> DeidResult raise DeidError {
  let policy = with_policy_mode(
    allow_only_kinds(RedactionPolicy::default(), kinds),
    mode,
  )
  redact_with_policy(input, policy)
}

///|
pub fn redact_except_kinds(
  input : String,
  excluded : Array[PhiKind],
  mode : ReplacementMode,
) -> DeidResult raise DeidError {
  let findings = scan(input)
  let selected = findings.filter(fn(item) { !excluded.contains(item.kind) })
  let changed = findings_with_mode(selected, mode)
  let (text, offsets) = apply_findings(input, changed)
  {
    text,
    findings: changed,
    offsets,
    audit: build_audit(input, text, changed, offsets),
  }
}

///|
pub fn redact_range(
  input : String,
  range : Span,
  config : RedactionConfig,
) -> DeidResult raise DeidError {
  let bounded = range.clamp(input.length())
  let fragment = input[bounded.start:bounded.end].to_owned()
  let fragment_result = redact_with_config(fragment, config)
  let findings = shift_findings(fragment_result.findings, bounded.start)
  let changed = findings.map(fn(item) {
    { ..item, replacement: item.replacement }
  })
  let (text, offsets) = apply_findings(input, changed)
  {
    text,
    findings: changed,
    offsets,
    audit: build_audit(input, text, changed, offsets),
  }
}

///|
pub fn redact_prefix(
  input : String,
  length : Int,
  config : RedactionConfig,
) -> DeidResult raise DeidError {
  redact_range(input, { start: 0, end: length }, config)
}

///|
pub fn redact_suffix(
  input : String,
  length : Int,
  config : RedactionConfig,
) -> DeidResult raise DeidError {
  let start = input.length() - length
  redact_range(input, { start, end: input.length() }, config)
}

///|
pub fn redact_windowed(
  input : String,
  radius : Int,
  config : RedactionConfig,
) -> DeidResult raise DeidError {
  let initial = scan(input)
  let windows = initial.map(fn(item) {
    finding_span(item).shift(-radius).clamp(input.length())
  })
  let union = merge_spans(windows)
  let candidates = []
  for window in union {
    let result = redact_range(input, window, config)
    candidates.append(result.findings)
  }
  let final_findings = resolve_overlaps(candidates)
  let (text, offsets) = apply_findings(input, final_findings)
  {
    text,
    findings: final_findings,
    offsets,
    audit: build_audit(input, text, final_findings, offsets),
  }
}

///|
pub fn preview_redaction(
  input : String,
  config : RedactionConfig,
) -> Array[Finding] raise DeidError {
  let result = redact_with_config(input, {
    ..config,
    policy: { ..config.policy, action: ReviewOnly },
  })
  result.findings
}

///|
pub fn redact_until_stable(
  input : String,
  config : RedactionConfig,
  maximum_rounds : Int,
) -> DeidResult raise DeidError {
  let mut current = redact_with_config(input, config)
  let rounds = if maximum_rounds <= 0 { 1 } else { maximum_rounds }
  for _ in 1.. DeidResult raise DeidError {
  let policy = protected_spans.fold(init=config.policy, (current, item) => {
    add_protected_range(current, item.start, item.end, "allowlist")
  })
  redact_with_config(input, { ..config, policy, })
}

///|
pub fn redact_with_deny_rules(
  input : String,
  denied_rule_ids : Array[String],
  config : RedactionConfig,
) -> DeidResult raise DeidError {
  let policy = deny_rules(config.policy, denied_rule_ids)
  redact_with_config(input, { ..config, policy, })
}

///|
pub fn replacement_for_finding(
  finding : Finding,
  mode : ReplacementMode,
) -> String {
  replacement_preview(finding, mode)
}

///|
pub fn redaction_changed_text(input : String, result : DeidResult) -> String {
  let changed = StringBuilder()
  let mut cursor = 0
  for offset in result.offsets {
    if offset.original_start > cursor {
      changed.write_string(input[cursor:offset.original_start].to_owned())
    }
    cursor = offset.original_end
  }
  changed.to_string()
}

///|
pub fn redaction_is_idempotent(
  input : String,
  config : RedactionConfig,
) -> Bool raise DeidError {
  let first = redact_with_config(input, config)
  let second = redact_with_config(first.text, config)
  first.text == second.text
}

///|
pub fn redaction_preserves_newlines(
  input : String,
  result : DeidResult,
) -> Bool {
  count_char(input, '\n') == count_char(result.text, '\n')
}

///|
pub fn redaction_preserves_separators(
  input : String,
  result : DeidResult,
  separators : String,
) -> Bool {
  for c in separators {
    if count_char(input, c) != count_char(result.text, c) {
      return false
    }
  }
  true
}

///|
pub fn redaction_summary(input : String, result : DeidResult) -> String {
  [
    audit_summary(result.audit),
    "newlines_preserved=\{redaction_preserves_newlines(input, result)}",
    "integrity=\{result_integrity_score(input, result)}",
    "checksum=\{stable_result_checksum(result)}",
  ].join("\n")
}