///|
pub(all) struct RedactionConfig {
  policy : RedactionPolicy
  salt : String
  token_prefix : String
  keep_first : Int
  keep_last : Int
  keep_whitespace : Bool
} derive(Debug, Eq)

///|
pub fn RedactionConfig::default() -> RedactionConfig {
  {
    policy: RedactionPolicy::default(),
    salt: "moonbit-deid",
    token_prefix: "",
    keep_first: 1,
    keep_last: 1,
    keep_whitespace: false,
  }
}

///|
pub fn config_with_policy(policy : RedactionPolicy) -> RedactionConfig {
  { ..RedactionConfig::default(), policy, }
}

///|
pub fn config_with_salt(salt : String) -> RedactionConfig {
  { ..RedactionConfig::default(), salt, }
}

///|
pub fn mode_name(mode : ReplacementMode) -> String {
  match mode {
    Token => "token"
    PreserveLength => "preserve-length"
    Partial => "partial"
    StableHash => "stable-hash"
  }
}

///|
pub fn policy_allows_kind(policy : RedactionPolicy, kind : PhiKind) -> Bool {
  policy.allowed_kinds.is_empty() || policy.allowed_kinds.contains(kind)
}

///|
pub fn policy_blocks_rule(policy : RedactionPolicy, rule_id : String) -> Bool {
  policy.denied_rule_ids.contains(rule_id)
}

///|
pub fn policy_protects(policy : RedactionPolicy, finding : Finding) -> Bool {
  finding_intersects_any(
    finding,
    policy.protected_ranges.map(fn(item) { item.as_span() }),
  )
}

///|
pub fn policy_filter_findings(
  findings : Array[Finding],
  policy : RedactionPolicy,
) -> Array[Finding] {
  findings.filter(fn(item) {
    policy_allows_kind(policy, item.kind) &&
    !policy_blocks_rule(policy, item.rule_id) &&
    !policy_protects(policy, item)
  })
}

///|
pub fn policy_decisions(
  findings : Array[Finding],
  policy : RedactionPolicy,
) -> Array[PolicyDecision] {
  findings.map(fn(item) {
    let action = if policy_protects(policy, item) ||
      policy_blocks_rule(policy, item.rule_id) {
      Keep
    } else if !policy_allows_kind(policy, item.kind) {
      Keep
    } else {
      policy.action
    }
    { finding_id: item.id, action, reason: policy.name }
  })
}

///|
fn policy_rules(policy : RedactionPolicy, rules : Array[Rule]) -> Array[Rule] {
  rules_for_locale(rules, policy.locale).filter(fn(item) {
    item.enabled && !policy.denied_rule_ids.contains(item.id)
  })
}

///|
fn replacement_with_config(
  finding : Finding,
  config : RedactionConfig,
) -> String {
  let value = finding.text
  let base = match config.policy.mode {
    Token => finding.replacement
    PreserveLength => preserve_shape(value, config.keep_whitespace)
    Partial =>
      edge_mask(
        value,
        config.keep_first,
        config.keep_last,
        config.keep_whitespace,
      )
    StableHash =>
      "[\{phi_kind_name(finding.kind)}]#\{salted_hash(value, config.salt)}"
  }
  config.token_prefix + base
}

///|
fn preserve_shape(value : String, keep_whitespace : Bool) -> String {
  let builder = StringBuilder()
  for c in value {
    if keep_whitespace && is_blank_char(c) {
      builder.write_char(c)
    } else if c.is_ascii_digit() {
      builder.write_char('0')
    } else if c.is_ascii_alphabetic() {
      builder.write_char('X')
    } else if is_cjk_char(c) {
      builder.write_char('*')
    } else {
      builder.write_char('*')
    }
  }
  builder.to_string()
}

///|
fn edge_mask(
  value : String,
  first : Int,
  last : Int,
  keep_whitespace : Bool,
) -> String {
  let chars = value.to_array()
  let safe_first = if first < 0 {
    0
  } else if first > chars.length() {
    chars.length()
  } else {
    first
  }
  let safe_last = if last < 0 {
    0
  } else if last > chars.length() {
    chars.length()
  } else {
    last
  }
  if safe_first + safe_last >= chars.length() {
    preserve_shape(value, keep_whitespace)
  } else {
    let builder = StringBuilder()
    for i in 0..= chars.length() - safe_last
      if keep {
        builder.write_char(chars[i])
      } else if keep_whitespace && is_blank_char(chars[i]) {
        builder.write_char(chars[i])
      } else {
        builder.write_char('*')
      }
    }
    builder.to_string()
  }
}

///|
fn salted_hash(value : String, salt : String) -> String {
  let combined = salt + "\u{1f}" + value
  stable_hash(combined)
}

///|
pub fn redact_with_config(
  input : String,
  config : RedactionConfig,
  rules? : Array[Rule] = comprehensive_rules(),
) -> DeidResult raise DeidError {
  let selected_rules = policy_rules(config.policy, rules)
  let scan_options = {
    mode: config.policy.mode,
    min_confidence: config.policy.min_confidence,
    include_disabled_rules: false,
  }
  let candidates = scan(input, rules=selected_rules, options=scan_options)
  let findings = policy_filter_findings(candidates, config.policy)
  let replacement_findings = findings.map(fn(item) {
    { ..item, replacement: replacement_with_config(item, config) }
  })
  match config.policy.action {
    Redact => {
      let (text, offsets) = apply_findings(input, replacement_findings)
      {
        text,
        findings: replacement_findings,
        offsets,
        audit: build_audit(input, text, replacement_findings, offsets),
      }
    }
    ReviewOnly | Keep =>
      {
        text: input,
        findings: replacement_findings,
        offsets: [],
        audit: build_audit(input, input, replacement_findings, []),
      }
  }
}

///|
pub fn redact_with_policy(
  input : String,
  policy : RedactionPolicy,
  rules? : Array[Rule] = comprehensive_rules(),
) -> DeidResult raise DeidError {
  redact_with_config(input, config_with_policy(policy), rules~)
}

///|
pub fn redact_with_salt(
  input : String,
  salt : String,
  rules? : Array[Rule] = comprehensive_rules(),
) -> DeidResult raise DeidError {
  redact_with_config(input, config_with_salt(salt), rules~)
}

///|
pub fn mask_value(value : String, mode : ReplacementMode) -> String {
  let placeholder = "[REDACTED]"
  match mode {
    Token => placeholder
    PreserveLength => preserve_shape(value, false)
    Partial => edge_mask(value, 1, 1, false)
    StableHash => placeholder + "#" + stable_hash(value)
  }
}

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

///|
pub fn policy_summary(policy : RedactionPolicy) -> String {
  [
    "name=\{policy.name}",
    "locale=\{locale_name(policy.locale)}",
    "action=\{action_name(policy.action)}",
    "mode=\{mode_name(policy.mode)}",
    "min_confidence=\{policy.min_confidence}",
    "protected=\{policy.protected_ranges.length()}",
    "denied_rules=\{policy.denied_rule_ids.length()}",
  ].join("\n")
}

///|
pub fn add_protected_range(
  policy : RedactionPolicy,
  start : Int,
  end : Int,
  reason : String,
) -> RedactionPolicy {
  let ranges = policy.protected_ranges.copy()
  ranges.push({ start, end, reason })
  { ..policy, protected_ranges: ranges }
}

///|
pub fn allow_only_kinds(
  policy : RedactionPolicy,
  kinds : Array[PhiKind],
) -> RedactionPolicy {
  { ..policy, allowed_kinds: kinds.copy() }
}

///|
pub fn deny_rules(
  policy : RedactionPolicy,
  rule_ids : Array[String],
) -> RedactionPolicy {
  let denied = policy.denied_rule_ids.copy()
  denied.append(rule_ids)
  { ..policy, denied_rule_ids: denied }
}

///|
pub fn with_policy_mode(
  policy : RedactionPolicy,
  mode : ReplacementMode,
) -> RedactionPolicy {
  { ..policy, mode, }
}

///|
pub fn with_policy_confidence(
  policy : RedactionPolicy,
  min_confidence : Int,
) -> RedactionPolicy {
  { ..policy, min_confidence, }
}

///|
pub fn with_policy_locale(
  policy : RedactionPolicy,
  locale : LocaleHint,
) -> RedactionPolicy {
  { ..policy, locale, }
}