///|
fn kind_name(kind : @lesion.LesionKind) -> String {
  match kind {
    NonNodal => "non_nodal"
    LymphNode => "lymph_node"
  }
}

///|
fn state_name(observation : @visit.Observation) -> String {
  match observation {
    Measured(_) => "measured"
    Absent => "absent"
    TooSmallToMeasure => "too_small"
    Missing => "missing"
    NotEvaluable(_) => "not_evaluable"
  }
}

///|
fn optional_int(value : Int?) -> Json {
  match value {
    Some(value) => value.to_json()
    None => Json::null()
  }
}

///|
fn optional_string(value : String?) -> Json {
  match value {
    Some(value) => value.to_json()
    None => Json::null()
  }
}

///|
fn optional_mm(value : Int?) -> Json {
  match value {
    Some(value) => format_diameter_mm(value).to_json()
    None => Json::null()
  }
}

///|
fn observation_json(observation : @visit.Observation) -> Json {
  let mm = match observation {
    Measured(size) => Some(size)
    _ => None
  }
  let note = match observation {
    NotEvaluable(reason) => Some(reason)
    _ => None
  }
  {
    "state": state_name(observation),
    "diameter_mm": optional_mm(mm),
    "note": optional_string(note),
  }
}

///|
fn study_json(study : @lesion.Study) -> Json {
  let targets : Array[Json] = study.targets.map(target => {
    "id": target.id,
    "organ": target.organ,
    "kind": kind_name(target.kind),
    "baseline_mm": format_diameter_mm(target.baseline_tenths_mm),
  })
  {
    "schema_version": 1,
    "study_id": study.id,
    "baseline_date": study.baseline_date,
    "technique_id": study.technique_id,
    "slice_thickness_mm": format_diameter_mm(study.slice_thickness_tenths_mm),
    "targets": targets.to_json(),
  }
}

///|
fn summary_json(trajectory : @evidence.Trajectory) -> Json {
  let counts : Map[String, Int] = {
    "CR": 0,
    "PR": 0,
    "SD": 0,
    "PD": 0,
    "NE": 0,
  }
  let mut missing = 0
  let mut defaults = 0
  for assessment in trajectory.assessments {
    let key = assessment.response.code()
    counts[key] = counts[key] + 1
    for finding in assessment.findings {
      if finding.code == "missing_measurement" {
        missing += 1
      }
      if finding.code == "default_5mm" {
        defaults += 1
      }
    }
  }
  {
    "target_count": trajectory.study.targets.length(),
    "visit_count": trajectory.assessments.length(),
    "response_counts": counts.to_json(),
    "missing_measurements": missing,
    "defaulted_measurements": defaults,
  }
}

///|
/// Deterministic, versioned evidence envelope. It retains original observations,
/// resolved measurement values and the exact prior nadir for every visit.
pub fn render_audit_json(trajectory : @evidence.Trajectory) -> String {
  let visits : Array[Json] = trajectory.visits.map(visit => {
    let measurements : Array[Json] = visit.measurements.map(measurement => {
      "lesion_id": measurement.lesion_id,
      "observation": observation_json(measurement.observation),
    })
    {
      "id": visit.id,
      "date": visit.date,
      "technique_id": visit.technique_id,
      "measurements": measurements.to_json(),
    }
  })
  let assessments : Array[Json] = trajectory.assessments.map(assessment => {
    let measurements : Array[Json] = assessment.measurements.map(measurement => {
      "lesion_id": measurement.lesion_id,
      "kind": kind_name(measurement.kind),
      "baseline_mm": format_diameter_mm(measurement.baseline_tenths_mm),
      "observation": observation_json(measurement.observation),
      "effective_mm": optional_mm(measurement.effective_tenths_mm),
    })
    let findings : Array[Json] = assessment.findings.map(finding => {
      "code": finding.code,
      "lesion_id": optional_string(finding.lesion_id),
      "message": finding.message,
    })
    {
      "visit_id": assessment.visit_id,
      "date": assessment.date,
      "target_response": assessment.response.code(),
      "sum_mm": optional_mm(assessment.sum_tenths_mm),
      "baseline_sum_mm": format_diameter_mm(assessment.baseline_sum_tenths_mm),
      "prior_nadir_mm": format_diameter_mm(assessment.nadir_sum_tenths_mm),
      "prior_nadir_visit_id": assessment.nadir_visit_id,
      "baseline_change_basis_points": optional_int(
        assessment.baseline_change_basis_points,
      ),
      "nadir_change_basis_points": optional_int(
        assessment.nadir_change_basis_points,
      ),
      "measurements": measurements.to_json(),
      "findings": findings.to_json(),
      "rule_checks": checks_json(assessment),
    }
  })
  let document : Json = {
    "schema_version": 1,
    "ruleset": "recist-1.1-target-subset-1",
    "scope": "target_lesions_only",
    "study": study_json(trajectory.study),
    "visits": visits.to_json(),
    "assessments": assessments.to_json(),
    "summary": summary_json(trajectory),
  }
  document.stringify(indent=2) + "\n"
}

///|
pub fn render_validation_json(
  diagnostics : Array[Diagnostic],
  issues : Array[@evidence.InputIssue],
  visit_count : Int,
) -> String {
  let diagnostics : Array[Json] = diagnostics.map(d => {
    "code": d.code,
    "message": d.message,
    "line": optional_int(d.line),
    "column": optional_int(d.column),
    "field": optional_string(d.field),
  })
  let issues : Array[Json] = issues.map(i => {
    "code": i.code.code(),
    "message": i.message,
    "visit_id": optional_string(i.visit_id),
    "lesion_id": optional_string(i.lesion_id),
  })
  let result : Json = {
    "schema_version": 1,
    "valid": diagnostics.is_empty() && issues.is_empty(),
    "visit_count": visit_count,
    "diagnostics": diagnostics.to_json(),
    "input_issues": issues.to_json(),
  }
  result.stringify(indent=2) + "\n"
}