///|
pub(all) struct MatchQuality {
  finding_id : String
  lexical : Int
  contextual : Int
  structural : Int
  checksum : Int
  total : Int
  band : ConfidenceBand
} derive(Debug, Eq)

///|
pub fn lexical_score(finding : Finding) -> Int {
  let value = finding.text
  let mut score = 0
  if !value.is_empty() {
    score += 20
  }
  if value.length() >= 4 {
    score += 20
  }
  if value.length() >= 8 {
    score += 20
  }
  if contains_ascii_letter(value) {
    score += 10
  }
  if all_ascii_digits(value) {
    score += 20
  }
  if contains_cjk(value) {
    score += 10
  }
  if score > 100 {
    100
  } else {
    score
  }
}

///|
pub fn contextual_score(input : String, finding : Finding, radius : Int) -> Int {
  let window = finding_context(input, finding, radius)
  let mut score = 0
  let lower = window.text.to_lower()
  if lower.contains("name") ||
    lower.contains("姓名") ||
    lower.contains("patient") {
    score += 35
  }
  if lower.contains("phone") ||
    lower.contains("电话") ||
    lower.contains("tel") {
    score += 30
  }
  if lower.contains("email") || lower.contains("邮箱") {
    score += 25
  }
  if lower.contains("id") || lower.contains("号") {
    score += 20
  }
  if score > 100 {
    100
  } else {
    score
  }
}

///|
pub fn structural_score(finding : Finding) -> Int {
  let mut score = 0
  if finding.start >= 0 && finding.end > finding.start {
    score += 30
  }
  if finding.end - finding.start == finding.text.length() {
    score += 30
  }
  if finding.replacement.length() > 0 {
    score += 20
  }
  if finding.rule_id.length() > 0 {
    score += 20
  }
  score
}

///|
pub fn checksum_score(finding : Finding) -> Int {
  match finding.kind {
    IdNumber => if valid_chinese_id(finding.text) { 100 } else { 20 }
    Insurance =>
      if valid_bank_card(finding.text) || valid_iban_prefix(finding.text) {
        100
      } else {
        35
      }
    Phone => if valid_phone_number(finding.text) { 90 } else { 30 }
    Email => if valid_email_address(finding.text) { 95 } else { 25 }
    Custom(name) =>
      if name == "device_identifier" && valid_imei(finding.text) {
        100
      } else {
        40
      }
    _ => 50
  }
}

///|
pub fn match_quality(
  input : String,
  finding : Finding,
  radius : Int,
) -> MatchQuality {
  let lexical = lexical_score(finding)
  let contextual = contextual_score(input, finding, radius)
  let structural = structural_score(finding)
  let checksum = checksum_score(finding)
  let total = (lexical + contextual + structural + checksum) / 4
  {
    finding_id: finding.id,
    lexical,
    contextual,
    structural,
    checksum,
    total,
    band: confidence_band(total),
  }
}

///|
pub fn match_qualities(
  input : String,
  findings : Array[Finding],
  radius : Int,
) -> Array[MatchQuality] {
  findings.map(fn(item) { match_quality(input, item, radius) })
}

///|
pub fn quality_by_finding(qualities : Array[MatchQuality]) -> Map[String, Int] {
  let result : Map[String, Int] = Map([])
  for quality in qualities {
    result[quality.finding_id] = quality.total
  }
  result
}

///|
pub fn quality_average(qualities : Array[MatchQuality]) -> Float {
  if qualities.is_empty() {
    0.0
  } else {
    Float::from_int(qualities.fold(init=0, (sum, item) => sum + item.total)) /
    Float::from_int(qualities.length())
  }
}

///|
pub fn quality_minimum(qualities : Array[MatchQuality]) -> Int {
  match qualities.get(0) {
    None => 0
    Some(first) =>
      qualities.fold(init=first.total, (current, item) => {
        if item.total < current {
          item.total
        } else {
          current
        }
      })
  }
}

///|
pub fn quality_filter_findings(
  input : String,
  findings : Array[Finding],
  threshold : Int,
  radius : Int,
) -> Array[Finding] {
  let qualities = match_qualities(input, findings, radius)
  findings.filter(fn(item) {
    let mut total = -1
    for quality in qualities {
      if quality.finding_id == item.id {
        total = quality.total
      }
    }
    total >= threshold
  })
}

///|
pub fn quality_adjust_confidence(
  input : String,
  finding : Finding,
  radius : Int,
) -> Finding {
  let quality = match_quality(input, finding, radius)
  let confidence = (finding.confidence + quality.total) / 2
  { ..finding, confidence: if confidence > 100 { 100 } else { confidence } }
}

///|
pub fn quality_adjust_findings(
  input : String,
  findings : Array[Finding],
  radius : Int,
) -> Array[Finding] {
  findings.map(fn(item) { quality_adjust_confidence(input, item, radius) })
}

///|
pub fn match_quality_report(
  input : String,
  findings : Array[Finding],
  radius : Int,
) -> String {
  let qualities = match_qualities(input, findings, radius)
  let lines = [
    "findings=\{findings.length()}",
    "average=\{quality_average(qualities)}",
    "minimum=\{quality_minimum(qualities)}",
  ]
  for item in qualities {
    lines.push(
      "\{item.finding_id}: \{item.total} (\{confidence_band_name(item.band)})",
    )
  }
  lines.join("\n")
}

///|
pub fn quality_is_consistent(input : String, finding : Finding) -> Bool {
  validate_finding_text(input, finding) &&
  finding.confidence >= 0 &&
  finding.confidence <= 100
}

///|
pub fn quality_consistency_issues(
  input : String,
  findings : Array[Finding],
) -> Array[String] {
  findings
  .filter(fn(item) { !quality_is_consistent(input, item) })
  .map(fn(item) { item.id })
}

///|
pub fn quality_by_kind(
  input : String,
  findings : Array[Finding],
  radius : Int,
) -> Map[String, Float] {
  let buckets : Map[String, Array[Int]] = Map([])
  for item in match_qualities(input, findings, radius) {
    let key = item.finding_id
    let values = buckets.get_or_default(key, [])
    values.push(item.total)
    buckets[key] = values
  }
  let result : Map[String, Float] = Map([])
  for key, values in buckets {
    result[key] = Float::from_int(
        values.fold(init=0, (sum, item) => sum + item),
      ) /
      Float::from_int(values.length())
  }
  result
}