///|
fn quality_pair_confidence(
  pair : AlignmentPair,
  anchors : Array[LexicalAnchor],
) -> Double {
  let ratio = if pair.source_char_weight < 0.01 {
    1.0
  } else {
    pair.target_char_weight / pair.source_char_weight
  }
  let length_confidence = 1.0 / (1.0 + pair.score)
  let anchor_confidence = (anchor_bonus(pair, anchors) / 3.0).min(1.0)
  (0.7 * length_confidence +
  0.3 * anchor_confidence +
  (1.0 - (ratio - 1.0).abs().min(1.0)) * 0.15).min(1.0)
}

///|
fn quality_issue_for_ratio(report : AlignmentReport) -> QualityIssue? {
  if report.estimated_ratio < 0.35 || report.estimated_ratio > 3.2 {
    Some(
      quality_issue(
        code="unusual_ratio",
        severity="warning",
        message="Global character ratio is outside the normal review range.",
      ),
    )
  } else {
    None
  }
}

///|
fn quality_check_monotonicity(report : AlignmentReport) -> Double {
  if report.pairs.length() < 2 {
    return 1.0
  }
  let mut good = 0
  let mut previous_source = -1
  let mut previous_target = -1
  for pair in report.pairs {
    if pair.source_start >= previous_source &&
      pair.target_start >= previous_target {
      good += 1
    }
    previous_source = pair.source_start
    previous_target = pair.target_start
  }
  good.to_double() / report.pairs.length().to_double()
}

///|
fn quality_issue_for_gaps(report : AlignmentReport) -> Array[QualityIssue] {
  let issues = []
  for pair in report.pairs {
    if pair.source_end <= pair.source_start ||
      pair.target_end <= pair.target_start {
      issues.push(
        quality_issue(
          code="empty_span",
          severity="error",
          message="An alignment pair contains an empty span.",
          source_unit=pair.source_start,
          target_unit=pair.target_start,
        ),
      )
    }
    if pair.score > 4.0 {
      issues.push(
        quality_issue(
          code="low_confidence",
          severity="warning",
          message="The dynamic-programming score is high and needs review.",
          source_unit=pair.source_start,
          target_unit=pair.target_start,
        ),
      )
    }
  }
  issues
}

///|
/// Build a quality report from alignment output and lexical evidence.
pub fn assess_quality(
  report : AlignmentReport,
  anchors? : Array[LexicalAnchor] = [],
) -> AlignmentQuality {
  let mut exact = 0
  let mut merged = 0
  let mut anchored = 0
  let mut confidence_total = 0.0
  for pair in report.pairs {
    if pair.move_kind == "1-1" {
      exact += 1
    } else {
      merged += 1
    }
    if anchor_bonus(pair, anchors) > 0.0 {
      anchored += 1
    }
    confidence_total += quality_pair_confidence(pair, anchors)
  }
  let confidence = if report.pairs.is_empty() {
    0.0
  } else {
    confidence_total / report.pairs.length().to_double()
  }
  let monotonicity = quality_check_monotonicity(report)
  let issues = quality_issue_for_gaps(report)
  let issues = match quality_issue_for_ratio(report) {
    Some(issue) => {
      issues.push(issue)
      issues
    }
    None => issues
  }
  for warning in report.warnings {
    issues.push(
      quality_issue(code="report_warning", severity="warning", message=warning),
    )
  }
  let coverage = report.source_count.min(report.target_count).to_double() /
    report.source_count.max(report.target_count).max(1).to_double()
  let exact_rate = if report.pairs.is_empty() {
    0.0
  } else {
    exact.to_double() / report.pairs.length().to_double()
  }
  let issue_penalty = (issues.length().to_double() / 10.0).min(0.35)
  let score = (0.30 * coverage +
    0.25 * confidence +
    0.20 * monotonicity +
    0.15 * exact_rate +
    0.10 * (if report.warnings.is_empty() { 1.0 } else { 0.5 }) -
    issue_penalty)
    .max(0.0)
    .min(1.0)
  {
    source_units: report.source_count,
    target_units: report.target_count,
    aligned_pairs: report.pairs.length(),
    exact_one_to_one: exact,
    merged_pairs: merged,
    anchored_pairs: anchored,
    source_coverage: if report.source_count == 0 {
      1.0
    } else {
      report.pairs
      .fold(init=0, (total, pair) => total + pair.source_end - pair.source_start)
      .to_double() /
      report.source_count.to_double()
    },
    target_coverage: if report.target_count == 0 {
      1.0
    } else {
      report.pairs
      .fold(init=0, (total, pair) => total + pair.target_end - pair.target_start)
      .to_double() /
      report.target_count.to_double()
    },
    mean_confidence: confidence,
    monotonicity,
    score,
    issues,
  }
}

///|
/// Check whether a quality result is acceptable for automated ingestion.
pub fn passes_quality_gate(
  quality : AlignmentQuality,
  gate? : QualityGate = default_quality_gate(),
) -> Bool {
  quality.source_coverage >= gate.min_source_coverage &&
  quality.target_coverage >= gate.min_target_coverage &&
  quality.mean_confidence >= gate.min_mean_confidence &&
  quality.score >= gate.min_quality_score &&
  quality.issues.length() <= gate.max_warning_count
}

///|
/// Produce human-readable review actions from quality diagnostics.
pub fn quality_recommendations(quality : AlignmentQuality) -> Array[String] {
  let recommendations = []
  if quality.source_coverage < 0.99 {
    recommendations.push("Inspect unaligned source units.")
  }
  if quality.target_coverage < 0.99 {
    recommendations.push("Inspect unaligned target units.")
  }
  if quality.mean_confidence < 0.45 {
    recommendations.push("Review low-confidence pairs before export.")
  }
  if quality.merged_pairs > quality.aligned_pairs / 2 {
    recommendations.push("Try paragraph mode or improve sentence boundaries.")
  }
  if quality.anchored_pairs == 0 && quality.aligned_pairs > 0 {
    recommendations.push("Add shared terminology or identifier anchors.")
  }
  if recommendations.is_empty() {
    recommendations.push("No manual review action is currently required.")
  }
  recommendations
}

///|
/// Return a stable CSV row for quality dashboards.
pub fn quality_to_csv_row(id : String, quality : AlignmentQuality) -> String {
  "\{id},\{quality.source_units},\{quality.target_units},\{quality.aligned_pairs},\{quality.score},\{quality.mean_confidence},\{quality.source_coverage},\{quality.target_coverage},\{quality.issues.length()}"
}