///|
pub(all) struct ChunkPolicy {
  context_window : Int
  separator : String
  config : RedactionConfig
} derive(Debug, Eq)

///|
pub fn ChunkPolicy::default() -> ChunkPolicy {
  { context_window: 32, separator: "", config: RedactionConfig::default() }
}

///|
pub(all) struct ChunkResult {
  chunk_id : String
  original_length : Int
  redacted_length : Int
  result : DeidResult
} derive(Debug)

///|
pub(all) struct StreamingRedactor {
  config : RedactionConfig
  context_window : Int
  mut carry : String
  mut closed : Bool
} derive(Debug)

///|
pub fn StreamingRedactor::new(
  config : RedactionConfig,
  context_window : Int,
) -> StreamingRedactor {
  {
    config,
    context_window: if context_window < 0 {
      0
    } else {
      context_window
    },
    carry: "",
    closed: false,
  }
}

///|
fn stable_prefix(text : String, context_window : Int) -> (String, String) {
  let keep = if context_window < 0 { 0 } else { context_window }
  if text.length() <= keep {
    ("", text)
  } else {
    let split = text.length() - keep
    (text[:split].to_owned(), text[split:].to_owned())
  }
}

///|
pub fn StreamingRedactor::push(
  self : StreamingRedactor,
  chunk : String,
) -> String raise DeidError {
  if self.closed {
    raise DeidError("stream is already closed")
  }
  let combined = self.carry + chunk
  let (stable, carry) = stable_prefix(combined, self.context_window)
  self.carry = carry
  let result = redact_with_config(stable, self.config)
  result.text
}

///|
pub fn StreamingRedactor::finish(
  self : StreamingRedactor,
) -> String raise DeidError {
  if self.closed {
    ""
  } else {
    self.closed = true
    let result = redact_with_config(self.carry, self.config)
    self.carry = ""
    result.text
  }
}

///|
pub fn redact_chunks(
  chunks : Array[String],
  policy : ChunkPolicy,
) -> String raise DeidError {
  let stream = StreamingRedactor::new(policy.config, policy.context_window)
  let output = StringBuilder()
  for chunk in chunks {
    output.write_string(stream.push(chunk))
    output.write_string(policy.separator)
  }
  output.write_string(stream.finish())
  output.to_string()
}

///|
pub fn redact_lines(
  text : String,
  config : RedactionConfig,
) -> Array[String] raise DeidError {
  let lines = split_lines_with_offsets(text)
  lines.map(line => redact_with_config(line.text, config).text)
}

///|
pub fn redact_lines_joined(
  text : String,
  config : RedactionConfig,
  separator : String,
) -> String raise DeidError {
  redact_lines(text, config).join(separator)
}

///|
pub fn redact_chunk_results(
  items : Array[BatchItem],
  config : RedactionConfig,
) -> Array[ChunkResult] raise DeidError {
  let results = []
  for item in items {
    let result = redact_with_config(item.text, config)
    results.push({
      chunk_id: item.id,
      original_length: item.text.length(),
      redacted_length: result.text.length(),
      result,
    })
  }
  results
}

///|
pub fn chunk_results_text(
  results : Array[ChunkResult],
  separator : String,
) -> String {
  results.map(fn(item) { item.result.text }).join(separator)
}

///|
pub fn chunk_results_findings(results : Array[ChunkResult]) -> Array[Finding] {
  let findings = []
  let mut offset = 0
  for item in results {
    findings.append(shift_findings(item.result.findings, offset))
    offset += item.original_length
  }
  findings
}

///|
pub fn chunk_policy_summary(policy : ChunkPolicy) -> String {
  [
    "context_window=\{policy.context_window}",
    "separator=\{quote_for_log(policy.separator)}",
    policy_summary(policy.config.policy),
  ].join("\n")
}

///|
pub fn split_fixed_chunks(text : String, width : Int) -> Array[String] {
  if width <= 0 || text.is_empty() {
    [text]
  } else {
    let chunks = []
    let mut start = 0
    while start < text.length() {
      let end = if start + width > text.length() {
        text.length()
      } else {
        start + width
      }
      chunks.push(text[start:end].to_owned())
      start = end
    }
    chunks
  }
}

///|
pub fn split_on_blank_lines(text : String) -> Array[String] {
  text.split("\n\n").map(fn(item) { item.to_owned() }).to_array()
}

///|
pub fn stream_has_sensitive_boundary(
  text : String,
  rules : Array[Rule],
) -> Bool raise DeidError {
  let findings = scan(text, rules~)
  findings.any(fn(item) { item.start == 0 || item.end == text.length() })
}

///|
pub fn carry_for_boundary(text : String, width : Int) -> String {
  if width <= 0 {
    ""
  } else if text.length() <= width {
    text
  } else {
    text[text.length() - width:].to_owned()
  }
}