///|
/// One issue found while comparing two CSV audit snapshots.
pub(all) struct CsvDriftIssue {
  severity : String
  category : String
  column : String
  message : String
} derive(Eq, Debug)

///|
/// Drift report comparing a baseline CSV and a candidate CSV.
pub(all) struct CsvDriftReport {
  baseline : CsvAuditReport
  candidate : CsvAuditReport
  baseline_score : CsvAuditScore
  candidate_score : CsvAuditScore
  score_delta : Int
  row_delta : Int
  column_delta : Int
  issues : Array[CsvDriftIssue]
} derive(Eq, Debug)

///|
/// Compare two CSV snapshots and report schema, score, and completeness drift.
pub fn audit_drift(
  baseline_input : String,
  candidate_input : String,
) -> CsvDriftReport {
  let baseline = audit_csv(baseline_input)
  let candidate = audit_csv(candidate_input)
  let baseline_score = audit_quality_score(baseline)
  let candidate_score = audit_quality_score(candidate)
  let issues : Array[CsvDriftIssue] = Array::new()
  drift_collect_score_issues(issues, baseline_score, candidate_score)
  drift_collect_shape_issues(issues, baseline, candidate)
  drift_collect_column_issues(issues, baseline, candidate)
  {
    baseline,
    candidate,
    baseline_score,
    candidate_score,
    score_delta: candidate_score.score - baseline_score.score,
    row_delta: candidate.table.rows.length() - baseline.table.rows.length(),
    column_delta: candidate.table.headers.length() -
    baseline.table.headers.length(),
    issues,
  }
}

///|
/// Compare two CSV snapshots and render Markdown.
pub fn audit_drift_markdown(
  baseline_input : String,
  candidate_input : String,
) -> String {
  audit_drift_report_markdown(audit_drift(baseline_input, candidate_input))
}

///|
/// Compare two CSV snapshots and render JSON.
pub fn audit_drift_json(
  baseline_input : String,
  candidate_input : String,
) -> String {
  audit_drift_report_json(audit_drift(baseline_input, candidate_input))
}

///|
/// Render a compact text summary for CLI and logs.
pub fn audit_drift_text(report : CsvDriftReport) -> String {
  "drift: baseline_score=\{report.baseline_score.score}, candidate_score=\{report.candidate_score.score}, score_delta=\{report.score_delta}, row_delta=\{report.row_delta}, column_delta=\{report.column_delta}, issues=\{report.issues.length()}"
}

///|
/// Render a drift report as Markdown.
pub fn audit_drift_report_markdown(report : CsvDriftReport) -> String {
  let out = StringBuilder()
  out.write_string("# CSV Drift Report\n\n")
  out.write_string("## Summary\n\n")
  out.write_string(
    "- Baseline score: \{report.baseline_score.score} (\{report.baseline_score.grade})\n",
  )
  out.write_string(
    "- Candidate score: \{report.candidate_score.score} (\{report.candidate_score.grade})\n",
  )
  out.write_string("- Score delta: \{drift_signed_int(report.score_delta)}\n")
  out.write_string("- Row delta: \{drift_signed_int(report.row_delta)}\n")
  out.write_string("- Column delta: \{drift_signed_int(report.column_delta)}\n")
  out.write_string("- Drift issues: \{report.issues.length()}\n")
  out.write_string("\n## Drift Issues\n\n")
  if report.issues.length() == 0 {
    out.write_string("No schema or quality drift detected.\n")
  } else {
    out.write_string("| severity | category | column | message |\n")
    out.write_string("| --- | --- | --- | --- |\n")
    for issue in report.issues {
      out.write_string("| ")
      drift_write_markdown_cell(out, issue.severity)
      out.write_string(" | ")
      drift_write_markdown_cell(out, issue.category)
      out.write_string(" | ")
      drift_write_markdown_cell(out, issue.column)
      out.write_string(" | ")
      drift_write_markdown_cell(out, issue.message)
      out.write_string(" |\n")
    }
  }
  out.to_string()
}

///|
/// Render a drift report as compact JSON.
pub fn audit_drift_report_json(report : CsvDriftReport) -> String {
  let out = StringBuilder()
  out.write_char('{')
  out.write_string("\"summary\":{")
  out.write_string("\"baseline_score\":\{report.baseline_score.score}")
  out.write_string(",\"candidate_score\":\{report.candidate_score.score}")
  out.write_string(",\"score_delta\":\{report.score_delta}")
  out.write_string(",\"baseline_rows\":\{report.baseline.table.rows.length()}")
  out.write_string(
    ",\"candidate_rows\":\{report.candidate.table.rows.length()}",
  )
  out.write_string(",\"row_delta\":\{report.row_delta}")
  out.write_string(
    ",\"baseline_columns\":\{report.baseline.table.headers.length()}",
  )
  out.write_string(
    ",\"candidate_columns\":\{report.candidate.table.headers.length()}",
  )
  out.write_string(",\"column_delta\":\{report.column_delta}")
  out.write_string(",\"issue_count\":\{report.issues.length()}")
  out.write_char('}')
  out.write_string(",\"issues\":")
  drift_write_issues_json(out, report.issues)
  out.write_char('}')
  out.to_string()
}

///|
fn drift_collect_score_issues(
  issues : Array[CsvDriftIssue],
  baseline_score : CsvAuditScore,
  candidate_score : CsvAuditScore,
) -> Unit {
  let delta = candidate_score.score - baseline_score.score
  if delta <= -25 {
    issues.push({
      severity: "error",
      category: "score",
      column: "*",
      message: "Quality score dropped by \{0 - delta} point(s), from \{baseline_score.score} to \{candidate_score.score}.",
    })
  } else if delta <= -8 {
    issues.push({
      severity: "warning",
      category: "score",
      column: "*",
      message: "Quality score dropped by \{0 - delta} point(s), from \{baseline_score.score} to \{candidate_score.score}.",
    })
  } else if delta >= 8 {
    issues.push({
      severity: "info",
      category: "score",
      column: "*",
      message: "Quality score improved by \{delta} point(s), from \{baseline_score.score} to \{candidate_score.score}.",
    })
  }
}

///|
fn drift_collect_shape_issues(
  issues : Array[CsvDriftIssue],
  baseline : CsvAuditReport,
  candidate : CsvAuditReport,
) -> Unit {
  let row_delta = candidate.table.rows.length() - baseline.table.rows.length()
  if row_delta != 0 {
    let severity = if drift_percent_change_abs(
        row_delta,
        baseline.table.rows.length(),
      ) >=
      50 {
      "warning"
    } else {
      "info"
    }
    issues.push({
      severity,
      category: "shape",
      column: "*",
      message: "Row count changed from \{baseline.table.rows.length()} to \{candidate.table.rows.length()} (\{drift_signed_int(row_delta)}).",
    })
  }
  let column_delta = candidate.table.headers.length() -
    baseline.table.headers.length()
  if column_delta != 0 {
    issues.push({
      severity: "warning",
      category: "shape",
      column: "*",
      message: "Column count changed from \{baseline.table.headers.length()} to \{candidate.table.headers.length()} (\{drift_signed_int(column_delta)}).",
    })
  }
}

///|
fn drift_collect_column_issues(
  issues : Array[CsvDriftIssue],
  baseline : CsvAuditReport,
  candidate : CsvAuditReport,
) -> Unit {
  let baseline_headers = drift_unique_headers(baseline.table.headers)
  let candidate_headers = drift_unique_headers(candidate.table.headers)
  for column in baseline_headers {
    if !drift_contains(candidate_headers, column) {
      issues.push({
        severity: "error",
        category: "schema",
        column,
        message: "Column `\{column}` was removed from the candidate data.",
      })
    }
  }
  for column in candidate_headers {
    if !drift_contains(baseline_headers, column) {
      issues.push({
        severity: "info",
        category: "schema",
        column,
        message: "Column `\{column}` was added in the candidate data.",
      })
    }
  }
  for column in baseline_headers {
    if drift_contains(candidate_headers, column) {
      drift_collect_common_column_issues(issues, baseline, candidate, column)
    }
  }
}

///|
fn drift_collect_common_column_issues(
  issues : Array[CsvDriftIssue],
  baseline : CsvAuditReport,
  candidate : CsvAuditReport,
  column : String,
) -> Unit {
  match
    (
      drift_find_rule(baseline.inferred_schema, column),
      drift_find_rule(candidate.inferred_schema, column),
    ) {
    (Some(base_rule), Some(next_rule)) => {
      if base_rule.kind != next_rule.kind {
        issues.push({
          severity: "warning",
          category: "type",
          column,
          message: "Column `\{column}` type changed from \{drift_column_type_name(base_rule.kind)} to \{drift_column_type_name(next_rule.kind)}.",
        })
      }
      if next_rule.empty > base_rule.empty {
        issues.push({
          severity: "warning",
          category: "missing",
          column,
          message: "Column `\{column}` missing values increased from \{base_rule.empty} to \{next_rule.empty}.",
        })
      } else if base_rule.empty > 0 && next_rule.empty == 0 {
        issues.push({
          severity: "info",
          category: "missing",
          column,
          message: "Column `\{column}` no longer has missing values.",
        })
      }
    }
    _ => ()
  }
}

///|
fn drift_unique_headers(headers : Array[String]) -> Array[String] {
  let values : Array[String] = Array::new()
  for header in headers {
    let label = if header.is_empty() { "#empty" } else { header }
    if !drift_contains(values, label) {
      values.push(label)
    }
  }
  values
}

///|
fn drift_find_rule(
  rules : Array[CsvInferredColumnRule],
  column : String,
) -> CsvInferredColumnRule? {
  for rule in rules {
    let label = if rule.name.is_empty() { "#empty" } else { rule.name }
    if label == column {
      return Some(rule)
    }
  }
  None
}

///|
fn drift_contains(values : Array[String], value : String) -> Bool {
  for item in values {
    if item == value {
      return true
    }
  }
  false
}

///|
fn drift_percent_change_abs(delta : Int, baseline : Int) -> Int {
  if baseline == 0 {
    if delta == 0 {
      0
    } else {
      100
    }
  } else {
    drift_abs(delta) * 100 / baseline
  }
}

///|
fn drift_abs(value : Int) -> Int {
  if value < 0 {
    0 - value
  } else {
    value
  }
}

///|
fn drift_signed_int(value : Int) -> String {
  if value > 0 {
    "+\{value}"
  } else {
    value.to_string()
  }
}

///|
fn drift_column_type_name(kind : CsvColumnType) -> String {
  match kind {
    Text => "text"
    Integer => "integer"
    Float => "float"
    Boolean => "boolean"
  }
}

///|
fn drift_write_issues_json(
  out : StringBuilder,
  issues : Array[CsvDriftIssue],
) -> Unit {
  out.write_char('[')
  for i in 0.. 0 {
      out.write_char(',')
    }
    let issue = issues[i]
    out.write_char('{')
    out.write_string("\"severity\":")
    drift_write_json_string(out, issue.severity)
    out.write_string(",\"category\":")
    drift_write_json_string(out, issue.category)
    out.write_string(",\"column\":")
    drift_write_json_string(out, issue.column)
    out.write_string(",\"message\":")
    drift_write_json_string(out, issue.message)
    out.write_char('}')
  }
  out.write_char(']')
}

///|
fn drift_write_markdown_cell(out : StringBuilder, value : String) -> Unit {
  for ch in value.iter() {
    if ch == '|' {
      out.write_string("\\|")
    } else if ch == '\n' || ch == '\r' {
      out.write_char(' ')
    } else {
      out.write_char(ch)
    }
  }
}

///|
fn drift_write_json_string(out : StringBuilder, value : String) -> Unit {
  out.write_char('"')
  for ch in value.iter() {
    if ch == '"' {
      out.write_string("\\\"")
    } else if ch == '\\' {
      out.write_string("\\\\")
    } else if ch == '\n' {
      out.write_string("\\n")
    } else if ch == '\r' {
      out.write_string("\\r")
    } else if ch == '\t' {
      out.write_string("\\t")
    } else {
      out.write_char(ch)
    }
  }
  out.write_char('"')
}