///|
pub fn finding_span(finding : Finding) -> Span {
  { start: finding.start, end: finding.end }
}

///|
pub fn finding_length(finding : Finding) -> Int {
  finding.end - finding.start
}

///|
pub fn finding_is_valid(finding : Finding, input_length : Int) -> Bool {
  finding_span(finding).is_valid(input_length) && !finding.text.is_empty()
}

///|
pub fn finding_score(finding : Finding) -> Int {
  finding.confidence * 1000 + finding_length(finding)
}

///|
pub fn sort_findings(findings : Array[Finding]) -> Array[Finding] {
  let sorted = findings.copy()
  sorted.sort_by(fn(left : Finding, right : Finding) {
    if left.start != right.start {
      left.start - right.start
    } else if left.confidence != right.confidence {
      right.confidence - left.confidence
    } else {
      finding_length(right) - finding_length(left)
    }
  })
  sorted
}

///|
pub fn sort_findings_by_priority(findings : Array[Finding]) -> Array[Finding] {
  let sorted = findings.copy()
  sorted.sort_by(fn(left : Finding, right : Finding) {
    let score_difference = finding_score(right) - finding_score(left)
    if score_difference != 0 {
      score_difference
    } else {
      left.start - right.start
    }
  })
  sorted
}

///|
pub fn unique_findings(findings : Array[Finding]) -> Array[Finding] {
  let sorted = sort_findings(findings)
  let result : Array[Finding] = []
  for finding in sorted {
    if !result.any(fn(existing) {
        existing.start == finding.start &&
        existing.end == finding.end &&
        existing.kind == finding.kind
      }) {
      result.push(finding)
    }
  }
  result
}

///|
pub fn merge_spans(spans : Array[Span]) -> Array[Span] {
  let sorted = spans.filter(fn(item) { !item.is_empty() })
  sorted.sort_by(fn(left, right) {
    if left.start == right.start {
      left.end - right.end
    } else {
      left.start - right.start
    }
  })
  let result = []
  for current in sorted {
    match result.get(result.length() - 1) {
      None => result.push(current)
      Some(last) =>
        if current.start <= last.end {
          result[result.length() - 1] = {
            start: last.start,
            end: if current.end > last.end {
              current.end
            } else {
              last.end
            },
          }
        } else {
          result.push(current)
        }
    }
  }
  result
}

///|
pub fn subtract_span(source : Span, blocked : Array[Span]) -> Array[Span] {
  let result = [source]
  let ordered = merge_spans(blocked)
  for cut in ordered {
    let next = []
    for piece in result {
      if !piece.overlaps(cut) {
        next.push(piece)
      } else {
        if piece.start < cut.start {
          next.push({ start: piece.start, end: cut.start })
        }
        if cut.end < piece.end {
          next.push({ start: cut.end, end: piece.end })
        }
      }
    }
    result.clear()
    result.append(next)
  }
  result.filter(fn(item) { !item.is_empty() })
}

///|
pub fn subtract_spans(
  sources : Array[Span],
  blocked : Array[Span],
) -> Array[Span] {
  let result = []
  for source in sources {
    result.append(subtract_span(source, blocked))
  }
  merge_spans(result)
}

///|
pub fn finding_intersects_any(finding : Finding, spans : Array[Span]) -> Bool {
  let target = finding_span(finding)
  spans.any(fn(item) { target.overlaps(item) })
}

///|
pub fn filter_protected_findings(
  findings : Array[Finding],
  protected_ranges : Array[ProtectedRange],
) -> Array[Finding] {
  let spans = protected_ranges.map(fn(item) { item.as_span() })
  findings.filter(fn(item) { !finding_intersects_any(item, spans) })
}

///|
pub fn filter_findings_by_kind(
  findings : Array[Finding],
  kinds : Array[PhiKind],
) -> Array[Finding] {
  if kinds.is_empty() {
    findings
  } else {
    findings.filter(fn(item) { kinds.contains(item.kind) })
  }
}

///|
pub fn filter_findings_by_confidence(
  findings : Array[Finding],
  threshold : Int,
) -> Array[Finding] {
  findings.filter(fn(item) { item.confidence >= threshold })
}

///|
pub fn findings_in_span(
  findings : Array[Finding],
  window : Span,
) -> Array[Finding] {
  findings.filter(fn(item) { window.contains_span(finding_span(item)) })
}

///|
pub fn findings_overlapping_span(
  findings : Array[Finding],
  window : Span,
) -> Array[Finding] {
  findings.filter(fn(item) { window.overlaps(finding_span(item)) })
}

///|
pub fn findings_for_rule(
  findings : Array[Finding],
  rule_id : String,
) -> Array[Finding] {
  findings.filter(fn(item) { item.rule_id == rule_id })
}

///|
pub fn findings_for_kind(
  findings : Array[Finding],
  kind : PhiKind,
) -> Array[Finding] {
  findings.filter(fn(item) { item.kind == kind })
}

///|
pub fn first_finding(findings : Array[Finding]) -> Finding? {
  sort_findings(findings).get(0)
}

///|
pub fn last_finding(findings : Array[Finding]) -> Finding? {
  let sorted = sort_findings(findings)
  if sorted.is_empty() {
    None
  } else {
    sorted.get(sorted.length() - 1)
  }
}

///|
pub fn total_finding_length(findings : Array[Finding]) -> Int {
  let spans = findings.map(finding_span)
  merge_spans(spans).fold(init=0, (total, item) => total + item.length())
}

///|
pub fn finding_density(findings : Array[Finding], input_length : Int) -> Float {
  if input_length <= 0 {
    0.0
  } else {
    Float::from_int(total_finding_length(findings)) /
    Float::from_int(input_length)
  }
}

///|
pub fn finding_context(
  input : String,
  finding : Finding,
  radius : Int,
) -> TextWindow {
  let center = (finding.start + finding.end) / 2
  text_window(input, center, radius)
}

///|
pub fn shift_findings(
  findings : Array[Finding],
  amount : Int,
) -> Array[Finding] {
  findings.map(fn(item) {
    {
      ..item,
      id: "\{item.id}@\{amount}",
      start: item.start + amount,
      end: item.end + amount,
    }
  })
}

///|
pub fn relabel_findings(
  findings : Array[Finding],
  label_prefix : String,
) -> Array[Finding] {
  findings.map(fn(item) { { ..item, label: label_prefix + item.label } })
}

///|
pub fn apply_confidence_floor(
  findings : Array[Finding],
  floor : Int,
) -> Array[Finding] {
  findings.map(fn(item) {
    {
      ..item,
      confidence: if item.confidence < floor {
        floor
      } else {
        item.confidence
      },
    }
  })
}

///|
pub fn finding_kind_counts(findings : Array[Finding]) -> Map[String, Int] {
  let result : Map[String, Int] = Map([])
  for item in findings {
    let key = phi_kind_name(item.kind)
    result[key] = result.get_or_default(key, 0) + 1
  }
  result
}

///|
pub fn finding_rule_counts(findings : Array[Finding]) -> Map[String, Int] {
  let result : Map[String, Int] = Map([])
  for item in findings {
    result[item.rule_id] = result.get_or_default(item.rule_id, 0) + 1
  }
  result
}

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

///|
pub fn finding_ids(findings : Array[Finding]) -> Array[String] {
  findings.map(fn(item) { item.id })
}

///|
pub fn finding_texts(findings : Array[Finding]) -> Array[String] {
  findings.map(fn(item) { item.text })
}

///|
pub fn finding_replacements(findings : Array[Finding]) -> Array[String] {
  findings.map(fn(item) { item.replacement })
}

///|
pub fn validate_finding_order(findings : Array[Finding]) -> Bool {
  let sorted = sort_findings(findings)
  for i in 1.. sorted[i].start {
      return false
    }
  }
  true
}

///|
pub fn validate_finding_text(input : String, finding : Finding) -> Bool {
  finding.start >= 0 &&
  finding.end <= input.length() &&
  finding.end > finding.start &&
  input[finding.start:finding.end].to_owned() == finding.text
}

///|
pub fn validate_findings(
  input : String,
  findings : Array[Finding],
) -> Array[String] {
  let issues = []
  for item in findings {
    if !finding_is_valid(item, input.length()) {
      issues.push("\{item.id}: invalid span")
    }
    if !validate_finding_text(input, item) {
      issues.push("\{item.id}: text mismatch")
    }
  }
  if !validate_finding_order(findings) {
    issues.push("findings overlap or are unsorted")
  }
  issues
}

///|
pub fn split_findings_by_risk(
  findings : Array[Finding],
) -> (Array[Finding], Array[Finding], Array[Finding]) {
  let critical = []
  let high = []
  let other = []
  for item in findings {
    match risk_level(item) {
      Critical => critical.push(item)
      High => high.push(item)
      Medium | Low => other.push(item)
    }
  }
  (critical, high, other)
}

///|
pub fn overlap_pairs(findings : Array[Finding]) -> Array[(String, String)] {
  let pairs = []
  for i in 0.. Finding? {
  if findings.is_empty() {
    None
  } else {
    let mut best = findings[0]
    let mut best_distance = if position < best.start {
      best.start - position
    } else if position > best.end {
      position - best.end
    } else {
      0
    }
    for item in findings[1:] {
      let distance = if position < item.start {
        item.start - position
      } else if position > item.end {
        position - item.end
      } else {
        0
      }
      if distance < best_distance {
        best = item
        best_distance = distance
      }
    }
    Some(best)
  }
}

///|
pub fn findings_to_spans(findings : Array[Finding]) -> Array[Span] {
  findings.map(finding_span)
}

///|
pub fn span_summary(spans : Array[Span]) -> String {
  let merged = merge_spans(spans)
  let total = merged.fold(init=0, (sum, item) => sum + item.length())
  "spans=\{spans.length()} merged=\{merged.length()} covered=\{total}"
}