///|
/// A source location or visit anchor. Missing rows have no fabricated field location.
pub(all) struct ReviewNotice {
  code : String
  message : String
  source : String
  field : String?
  line : Int?
  column : Int?
  visit_id : String?
  lesion_id : String?
} derive(Debug, Eq)

///|
pub extend ReviewNotice with @debug.Debug::{to_repr}

///|
pub extend ReviewNotice with Eq::{equal, not_equal}

///|
/// Parse errors and structural errors block replay; review hints do not.
pub(all) struct PreflightReport {
  visit_count : Int
  diagnostics : Array[ReviewNotice]
  input_issues : Array[ReviewNotice]
  review_hints : Array[ReviewNotice]
} derive(Debug, Eq)

///|
pub extend PreflightReport with @debug.Debug::{to_repr}

///|
pub extend PreflightReport with Eq::{equal, not_equal}

///|
pub fn PreflightReport::is_valid(self : PreflightReport) -> Bool {
  self.diagnostics.is_empty() && self.input_issues.is_empty()
}

///|
fn notice(
  code : String,
  message : String,
  visit_id? : String,
  lesion_id? : String,
) -> ReviewNotice {
  {
    code,
    message,
    source: "memory",
    field: None,
    line: None,
    column: None,
    visit_id,
    lesion_id,
  }
}

///|
/// Inspect the validated track using the same replay findings as the report.
/// This preflight supplies no response categories and never changes input values.
pub fn preflight_track(
  study : @lesion.Study,
  visits : Array[@visit.Visit],
) -> PreflightReport {
  let issues = @replay.validate_track(study, visits).map(i => {
    notice(
      i.code.code(),
      i.message,
      visit_id?=i.visit_id,
      lesion_id?=i.lesion_id,
    )
  })
  let hints = []
  if issues.is_empty() {
    if visits.is_empty() {
      hints.push(notice("no_followup", "No follow-up visits were supplied."))
    }
    if @replay.replay_track(study, visits) is Ok(trajectory) {
      for a in trajectory.assessments {
        for f in a.findings {
          if [
              "missing_measurement", "reader_not_evaluable", "default_5mm", "technique_mismatch",
              "track_requires_review", "reappearance_after_target_cr", "prior_pr_context_required",
            ].contains(f.code) {
            hints.push(
              notice(
                f.code,
                f.message,
                visit_id=a.visit_id,
                lesion_id?=f.lesion_id,
              ),
            )
          }
        }
      }
    }
  }
  {
    visit_count: visits.length(),
    diagnostics: [],
    input_issues: issues,
    review_hints: hints,
  }
}

///|
fn id_valid(text : String) -> Bool {
  !text.is_blank() &&
  text == text.trim().to_owned() &&
  text.length() <= 128 &&
  text.iter().all(c => c.to_int() >= 32 && c.to_int() != 127)
}

///|
fn locate_notice(
  item : ReviewNotice,
  study : @lesion.Study,
  rows : Array[Array[CsvCell]],
) -> ReviewNotice {
  if item.visit_id is Some(id) {
    let matches = rows.filter(row => {
      row.length() == 7 &&
      row[0].text == id &&
      (match item.lesion_id {
        Some(target) => row[3].text == target
        None => true
      })
    })
    let mut field = match item.code {
      "invalid_date" | "visit_order" => 1
      "unknown_target" | "duplicate_id" =>
        if item.lesion_id is Some(_) {
          3
        } else {
          0
        }
      "invalid_study" =>
        if !matches.is_empty() && !id_valid(matches[0][0].text) {
          0
        } else {
          2
        }
      "invalid_measurement" => 5
      "technique_mismatch" => 2
      "reader_not_evaluable" => 6
      "track_requires_review"
      | "reappearance_after_target_cr"
      | "prior_pr_context_required" => 0
      _ => 4
    }
    if !matches.is_empty() {
      let row = if item.code == "duplicate_id" {
        matches[matches.length() - 1]
      } else {
        matches[0]
      }
      if item.code == "invalid_measurement" && row[4].text == "not_evaluable" {
        field = 6
      }
      let names = [
        "visit_id", "date", "technique_id", "lesion_id", "state", "diameter_mm",
        "note",
      ]
      return {
        ..item,
        source: "followup_csv",
        field: Some(names[field]),
        line: Some(row[field].line),
        column: Some(row[field].column),
      }
    }
    // An omitted target has no cell: point at the visit, without naming a field.
    for row in rows {
      if row.length() == 7 && row[0].text == id {
        return {
          ..item,
          source: "followup_csv",
          line: Some(row[0].line),
          column: Some(row[0].column),
        }
      }
    }
    return { ..item, source: "followup_csv", }
  }
  let mut field = match item.code {
    "invalid_date" => "$.baseline_date"
    "target_count" => "$.targets"
    "no_followup" =>
      return { ..item, source: "followup_csv", line: Some(1), column: Some(1), }
    _ =>
      if item.message.has_prefix("This version supports CT slices") {
        "$.slice_thickness_mm"
      } else if !id_valid(study.id) {
        "$.study_id"
      } else {
        "$.technique_id"
      }
  }
  if item.lesion_id is Some(id) {
    for i, target in study.targets {
      if target.id == id {
        let part = match item.code {
          "duplicate_id" => "id"
          "organ_target_count" => "organ"
          "invalid_target" if item.message.has_prefix("Baseline diameter") =>
            "baseline_mm"
          "invalid_target" if item.message.has_prefix("All lymph-node targets") =>
            "organ"
          _ =>
            if !id_valid(target.id) {
              "id"
            } else if !id_valid(target.organ) ||
              (target.kind is LymphNode) != (target.organ == "lymph_nodes") {
              "organ"
            } else {
              "baseline_mm"
            }
        }
        field = "$.targets[\{i}].\{part}"
        if item.code != "duplicate_id" {
          break
        }
      }
    }
  }
  { ..item, source: "study_json", field: Some(field), }
}

///|
/// Combined JSON/CSV preflight with physical CSV locations and JSON field paths.
/// Review hints are only derived after both inputs parse and validate in full.
pub fn preflight_inputs(
  study_text : String,
  csv_text : String,
) -> PreflightReport {
  let diagnostics = []
  let study = match parse_study_json(study_text) {
    Ok(value) => Some(value)
    Err(errors) => {
      for d in errors {
        diagnostics.push({
          ..notice(d.code, d.message),
          source: "study_json",
          field: d.field,
          line: d.line,
          column: d.column,
        })
      }
      None
    }
  }
  let visits = match parse_followup_csv(csv_text) {
    Ok(value) => Some(value)
    Err(errors) => {
      for d in errors {
        diagnostics.push({
          ..notice(d.code, d.message),
          source: "followup_csv",
          field: d.field,
          line: d.line,
          column: d.column,
        })
      }
      None
    }
  }
  guard study is Some(study) && visits is Some(visits) else {
    return {
      visit_count: match visits {
        Some(v) => v.length()
        None => 0
      },
      diagnostics,
      input_issues: [],
      review_hints: [],
    }
  }
  let result = preflight_track(study, visits)
  let rows = match csv_rows(csv_text) {
    Ok(rows) => rows[1:].to_owned()
    Err(_) => []
  }
  {
    ..result,
    input_issues: result.input_issues.map(i => locate_notice(i, study, rows)),
    review_hints: result.review_hints.map(i => locate_notice(i, study, rows)),
  }
}

///|
fn notice_json(item : ReviewNotice) -> Json {
  {
    "code": item.code,
    "message": item.message,
    "source": item.source,
    "field": optional_string(item.field),
    "line": optional_int(item.line),
    "column": optional_int(item.column),
    "visit_id": optional_string(item.visit_id),
    "lesion_id": optional_string(item.lesion_id),
  }
}

///|
/// Additive validation schema: machine-readable messages and codes are language independent.
pub fn render_preflight_json(report : PreflightReport) -> String {
  let document : Json = {
    "schema_version": 1,
    "valid": report.is_valid(),
    "visit_count": report.visit_count,
    "scope": "input_quality_and_review_hints",
    "diagnostics": report.diagnostics.map(notice_json).to_json(),
    "input_issues": report.input_issues.map(notice_json).to_json(),
    "review_hints": report.review_hints.map(notice_json).to_json(),
  }
  document.stringify(indent=2) + "\n"
}

///|
pub fn render_preflight_markdown(
  report : PreflightReport,
  language? : ReportLanguage = English,
) -> String {
  let out = StringBuilder()
  out.write_string(
    tr(language, "# Input quality preflight\n\n", "# 输入质量预检\n\n"),
  )
  out.write_string(
    tr(language, "Visits", "随访数") +
    ": \{report.visit_count}; " +
    tr(language, "Parse issues", "解析问题") +
    ": \{report.diagnostics.length()}; " +
    tr(language, "Structural issues", "结构问题") +
    ": \{report.input_issues.length()}; " +
    tr(language, "Review hints", "复核提示") +
    ": \{report.review_hints.length()}\n\n",
  )
  out.write_string(
    tr(
      language, "Valid input does not establish clinical comparability or overall response. Reader notes are preserved.\n",
      "输入有效不代表已确认临床可比性或整体疗效。读片者原始说明保留。\n",
    ),
  )
  for
    (heading, items) in [
      (tr(language, "Parse issues", "解析问题"), report.diagnostics),
      (tr(language, "Structural issues", "结构问题"), report.input_issues),
      (tr(language, "Review hints", "人工复核提示"), report.review_hints),
    ] {
    out.write_string("\n## " + heading + "\n\n")
    if items.is_empty() {
      out.write_string(tr(language, "None.\n", "无。\n"))
    }
    for item in items {
      let location = match (item.line, item.column) {
        (Some(line), Some(column)) => ":\{line}:\{column}"
        _ => ""
      }
      let detail = if heading ==
        tr(language, "Review hints", "人工复核提示") {
        finding_label(
          { code: item.code, message: item.message, lesion_id: item.lesion_id, },
          language,
        )
      } else {
        diagnostic_message(item.code, item.message, language)
      }
      out.write_string(
        "- `" +
        item.code +
        "` " +
        markdown_escape(
          item.source +
          location +
          " " +
          item.field.unwrap_or("") +
          " " +
          item.visit_id.unwrap_or("") +
          " " +
          item.lesion_id.unwrap_or(""),
        ) +
        ": " +
        markdown_escape(detail) +
        "\n",
      )
    }
  }
  out.to_string()
}