///|
/// A structured preflight report for deployment and CI diagnostics.
pub(all) struct PreflightReport {
  errors : Array[String]
  warnings : Array[String]
  mut checked_frames : Int
  mut checked_devices : Int
}

///|
pub fn PreflightReport::new() -> PreflightReport {
  { errors: [], warnings: [], checked_frames: 0, checked_devices: 0 }
}

///|
pub fn PreflightReport::ok(self : PreflightReport) -> Bool {
  self.errors.length() == 0
}

///|
pub fn PreflightReport::error_count(self : PreflightReport) -> Int {
  self.errors.length()
}

///|
pub fn PreflightReport::warning_count(self : PreflightReport) -> Int {
  self.warnings.length()
}

///|
pub fn PreflightReport::errors(self : PreflightReport) -> Array[String] {
  copy_strings(self.errors)
}

///|
pub fn PreflightReport::warnings(self : PreflightReport) -> Array[String] {
  copy_strings(self.warnings)
}

///|
fn copy_strings(values : Array[String]) -> Array[String] {
  let out : Array[String] = []
  for value in values {
    out.push(value)
  }
  out
}

///|
/// Check a frame's unit, PDU shape, and transport envelope constraints.
pub fn preflight_frame(
  report : PreflightReport,
  mode : Mode,
  frame : Frame,
) -> Unit {
  report.checked_frames += 1
  match validate_frame(frame, false) {
    Ok(_) => ()
    Err(error) =>
      report.errors.push(
        function_name(frame.pdu.function) + ": " + error_name(error),
      )
  }
  if encoded_length(mode, frame) > limits_for(mode).max_adu {
    report.errors.push("encoded frame exceeds " + mode_name(mode) + " limit")
  }
  if frame.pdu.data.length() == 0 {
    report.warnings.push(
      function_name(frame.pdu.function) + " has an empty data payload",
    )
  }
}

///|
/// Check a device's table capacities and unit id.
pub fn preflight_device(report : PreflightReport, device : Device) -> Unit {
  report.checked_devices += 1
  if !is_valid_unit_id(device.unit_id(), broadcast=false) {
    report.errors.push("device unit id is invalid")
  }
  if device.memory().coils().length() == 0 {
    report.warnings.push("device has no coil capacity")
  }
  if device.memory().holding_registers().length() == 0 {
    report.warnings.push("device has no holding-register capacity")
  }
}

///|
/// Check every job in a polling plan.
pub fn preflight_plan(
  report : PreflightReport,
  plan : PollPlan,
  mode : Mode,
) -> Unit {
  match validate_poll_plan(plan) {
    Ok(_) => ()
    Err(error) => report.errors.push("poll plan: " + error_name(error))
  }
  for job in plan.snapshot() {
    preflight_frame(report, mode, job.request)
  }
}

///|
/// Validate a complete server configuration and its registered devices.
pub fn preflight_server(server : Server) -> PreflightReport {
  let report = PreflightReport::new()
  match validate_server_config(server.config()) {
    Ok(_) => ()
    Err(error) => report.errors.push("server config: " + error_name(error))
  }
  for unit_id in 1..<=247 {
    match server.device(unit_id.to_byte()) {
      Some(device) => preflight_device(report, device)
      None => ()
    }
  }
  report
}

///|
/// Return a compact result line for CI logs.
pub fn preflight_summary(report : PreflightReport) -> String {
  "preflight errors=" +
  report.error_count().to_string() +
  " warnings=" +
  report.warning_count().to_string() +
  " frames=" +
  report.checked_frames.to_string() +
  " devices=" +
  report.checked_devices.to_string()
}