///|
/// Metadata supplied by the caller for a reproducible analysis run.
pub(all) struct AnalysisRequest {
  shipment_id : String
  source_name : String
  generated_at : Int64
  readings : Array[Reading]
  diagnostics : Array[Diagnostic]
  config : AnalysisConfig
} derive(Debug, Eq)

///|
/// Create a request with standard 2–8 °C behavior.
pub fn analysis_request(
  shipment_id : String,
  source_name : String,
  generated_at : Int64,
  readings : Array[Reading],
) -> AnalysisRequest {
  {
    shipment_id,
    source_name,
    generated_at,
    readings,
    diagnostics: [],
    config: default_analysis_config(),
  }
}

///|
/// Execute normalization, sensor health, excursions, metrics and risk.
pub fn analyze(request : AnalysisRequest) -> AnalysisReport {
  let normalized = normalize_readings(request.readings, request.config)
  let quality = analyze_sensor_quality(
    normalized.readings,
    normalized.gaps,
    request.config,
  )
  let diagnostics = copy_array(request.diagnostics)
  diagnostics.append(normalized.diagnostics)
  diagnostics.append(quality.diagnostics)
  if request.readings.length() == 0 {
    diagnostics.push(
      error_diagnostic(
        "analysis.no_readings", "analysis input contains no readings",
      ),
    )
  }
  let events = build_event_timeline(
    quality.readings,
    normalized.gaps,
    request.config.policy,
  )
  let statistics = compute_all_statistics(
    quality.readings,
    request.config.policy,
  )
  let base_risk = assess_risk(
    events,
    normalized.gaps,
    diagnostics,
    quality.health,
  )
  let risk = if quality.readings.length() == 0 {
    let reasons = copy_array(base_risk.reasons)
    reasons.push("no usable readings were available")
    { ..base_risk, band: Indeterminate, confidence_percent: 0, reasons }
  } else {
    base_risk
  }
  {
    schema: REPORT_SCHEMA,
    shipment_id: request.shipment_id,
    generated_at: request.generated_at,
    source_name: request.source_name,
    sensor_ids: unique_sensor_ids(quality.readings),
    readings: quality.readings,
    gaps: normalized.gaps,
    events,
    statistics,
    health: quality.health,
    risk,
    diagnostics,
  }
}

///|
/// Parse and analyze CSV in one library call.
pub fn analyze_csv(
  shipment_id : String,
  source_name : String,
  generated_at : Int64,
  input : String,
  config? : AnalysisConfig = default_analysis_config(),
) -> AnalysisReport {
  let batch = parse_readings_csv(input)
  analyze({
    shipment_id,
    source_name,
    generated_at,
    readings: batch.readings,
    diagnostics: batch.diagnostics,
    config,
  })
}

///|
/// Analyze a named simulation in one call.
pub fn analyze_simulation(
  scenario_name : String,
  generated_at? : Int64 = 1785916800L,
) -> AnalysisReport {
  let simulation = simulate_named(scenario_name)
  analyze({
    shipment_id: "simulation-\{scenario_name}",
    source_name: "deterministic simulation",
    generated_at,
    readings: simulation.readings,
    diagnostics: simulation.diagnostics,
    config: default_analysis_config(),
  })
}

///|
/// Differences between two analysis runs.
pub(all) struct ReportComparison {
  baseline_shipment_id : String
  candidate_shipment_id : String
  score_change : Int
  confidence_change : Int
  confirmed_event_change : Int
  gap_change : Int
  sensor_health_change : Int?
  summary : String
} derive(Debug, Eq)

///|
/// Compare average health when both reports contain sensors.
fn average_health_change(
  baseline : Array[SensorHealth],
  candidate : Array[SensorHealth],
) -> Int? {
  match (average_sensor_health(baseline), average_sensor_health(candidate)) {
    (Some(left), Some(right)) => Some(right - left)
    _ => None
  }
}

///|
/// Compare two reports without re-running their analyses.
pub fn compare_reports(
  baseline : AnalysisReport,
  candidate : AnalysisReport,
) -> ReportComparison {
  let score_change = candidate.risk.score - baseline.risk.score
  let confidence_change = candidate.risk.confidence_percent -
    baseline.risk.confidence_percent
  let event_change = confirmed_events(candidate.events).length() -
    confirmed_events(baseline.events).length()
  let gap_change = candidate.gaps.length() - baseline.gaps.length()
  let health_change = average_health_change(baseline.health, candidate.health)
  let direction = if score_change > 0 {
    "risk increased"
  } else if score_change < 0 {
    "risk decreased"
  } else {
    "risk was unchanged"
  }
  {
    baseline_shipment_id: baseline.shipment_id,
    candidate_shipment_id: candidate.shipment_id,
    score_change,
    confidence_change,
    confirmed_event_change: event_change,
    gap_change,
    sensor_health_change: health_change,
    summary: "\{direction} by \{abs(score_change)} points",
  }
}

///|
/// Local integer absolute value for report comparison.
fn abs(value : Int) -> Int {
  if value < 0 {
    -value
  } else {
    value
  }
}