///|
/// Runtime diagnosis helpers that expose standard EtherCAT diagnostics
/// objects to applications via CoE SDO.

///|
/// SlaveDiagnosis aggregates a minimal diagnosis snapshot for one slave.
pub(all) struct SlaveDiagnosis {
  station_address : UInt
  /// Observed AL state when the snapshot was taken.
  al_state : @protocol.EsmState?
  /// AL Error Flag from AL Status register.
  al_error_flag : Bool?
  /// AL Status Code from register 0x0134.
  al_status_code : UInt?
  /// CiA-301 Error Register (0x1001:0)
  error_register : Byte
  /// Diagnosis history object counter (0x10F3:0), when available.
  diagnosis_counter : UInt?
  /// DL/Physical layer error counters (ESC 0x0300–0x0313), when available.
  dl_error_counters : @protocol.DlErrorCounters?
} derive(Eq, Debug)

///|
pub(all) enum DiagnosticLevel {
  Block
  Degrade
  Info
} derive(Eq, Debug)

///|
pub(all) enum DiagnosticSourceLayer {
  Registers
  Mailbox
  Topology
  Configuration
  RuntimeTelemetry
  StateTransition
  Monitor
} derive(Eq, Debug)

///|
pub(all) enum DiagnosticCategory {
  AlStatus
  ErrorRegister
  DiagnosisHistory
  Topology
  Identity
  Configuration
  RuntimeMetrics
  StateTransition
  MonitorVerdict
} derive(Eq, Debug)

///|
pub(all) struct StateSurfaceSummary {
  target_state : @protocol.EsmState?
  current_state : @protocol.EsmState?
  final_state : @protocol.EsmState?
  al_state : @protocol.EsmState?
  al_error_flag : Bool?
  al_status_code : UInt?
  wkc : UInt?
} derive(Eq, Debug)

///|
pub(all) struct ConfigurationSurfaceSummary {
  slave_count : Int
  station_addresses : Array[UInt]
  expected_source : ConfigurationSource?
  actual_source : ConfigurationSource?
  validation_grade : ValidateGrade?
  difference_count : Int
} derive(Eq, Debug)

///|
pub(all) struct RuntimeMetricsSummary {
  cycles : Int64
  tx_frames : Int64
  rx_frames : Int64
  timeouts : Int64
  retries : Int64
  wkc_errors : Int64
  irq_events : Int64
  dc_delay_updates : Int64
  emergency_events : Int64
} derive(Eq, Debug)

///|
pub(all) struct DiagnosticIssue {
  source : DiagnosticSourceLayer
  category : DiagnosticCategory
  level : DiagnosticLevel
  detail : String
} derive(Eq, Debug)

///|
pub(all) struct DiagnosticSurface {
  station_address : UInt?
  state_summary : StateSurfaceSummary
  configuration_summary : ConfigurationSurfaceSummary?
  error_register : Byte?
  diagnosis_counter : UInt?
  runtime_metrics : RuntimeMetricsSummary?
  issues : Array[DiagnosticIssue]
  overall_level : DiagnosticLevel
  monitor_verdicts : Array[MonitorResult]?
  overall_verdict : Verdict?
  topology_fingerprint : TopologyFingerprint?
} derive(Eq, Debug)

///|
/// DiagnosisSession captures the state needed around a diagnosis run.
pub(all) struct DiagnosisSession {
  original_state : @protocol.EsmState
  observed_state : @protocol.EsmState?
  observed_error_flag : Bool?
  observed_status_code : UInt?
  mailbox_config : @mailbox.MailboxConfig
}

///|
/// MailboxSession captures generic mailbox preparation state for one slave.
pub(all) struct MailboxSession {
  original_state : @protocol.EsmState
  mailbox_config : @mailbox.MailboxConfig
}

///|
priv struct AlStatusSnapshot {
  state : @protocol.EsmState
  error_flag : Bool
  status_code : UInt
}

///|
let diagnosis_mailbox_max_polls : Int = 32

///|
let diagnosis_sm_window_size : Int = 8

///|
let diagnosis_sdo_max_attempts : Int = 3

///|
let diagnosis_sii_word_count : Int = 128

///|
/// Parse a one-byte error register payload.
pub fn parse_error_register_payload(payload : Bytes) -> Byte raise @hal.EcError {
  if payload.length() < 1 {
    raise @hal.EcError::MailboxProtocol(
      "diagnosis: error register payload too short",
    )
  }
  payload[0]
}

///|
/// Parse a little-endian UInt32 payload.
pub fn parse_u32_payload(payload : Bytes) -> UInt raise @hal.EcError {
  if payload.length() < 4 {
    raise @hal.EcError::MailboxProtocol("diagnosis: u32 payload too short")
  }
  payload[0].to_uint() |
  (payload[1].to_uint() << 8) |
  (payload[2].to_uint() << 16) |
  (payload[3].to_uint() << 24)
}

///|
/// Read Error Register (0x1001:0) from one slave.
pub fn[N : @hal.Nic] read_error_register(
  nic : N,
  station : UInt,
  mb_config : @mailbox.MailboxConfig,
  rmsm : @mailbox.Rmsm,
  timeout : @hal.Duration,
) -> Byte raise @hal.EcError {
  let payload = diagnosis_timeout_step(
    Result::Ok(
      @protocol.sdo_upload_retry(
        nic, station, mb_config, rmsm, 0x1001U, b'\x00', timeout,
      ),
    ) catch {
      err => Result::Err(err)
    },
    "CoE 0x1001:0 upload",
  )
  parse_error_register_payload(payload)
}

///|
/// Read diagnosis object counter (0x10F3:0) from one slave.
/// Some slaves may not implement this object; callers can handle errors.
pub fn[N : @hal.Nic] read_diagnosis_counter(
  nic : N,
  station : UInt,
  mb_config : @mailbox.MailboxConfig,
  rmsm : @mailbox.Rmsm,
  timeout : @hal.Duration,
) -> UInt raise @hal.EcError {
  let payload = diagnosis_timeout_step(
    Result::Ok(
      @protocol.sdo_upload_retry(
        nic, station, mb_config, rmsm, 0x10F3U, b'\x00', timeout,
      ),
    ) catch {
      err => Result::Err(err)
    },
    "CoE 0x10F3:0 upload",
  )
  parse_u32_payload(payload)
}

///|
/// Read Error Register (0x1001:0) from one slave via AP mailbox transport.
pub fn[N : @hal.Nic] read_error_register_ap(
  nic : N,
  position : UInt,
  station : UInt,
  mb_config : @mailbox.MailboxConfig,
  rmsm : @mailbox.Rmsm,
  timeout : @hal.Duration,
) -> Byte raise @hal.EcError {
  let payload = diagnosis_sdo_upload_ap(
    nic, position, station, mb_config, rmsm, 0x1001U, b'\x00', timeout, "AP CoE 0x1001:0 upload",
  )
  parse_error_register_payload(payload)
}

///|
/// Read diagnosis object counter (0x10F3:0) via AP mailbox transport.
pub fn[N : @hal.Nic] read_diagnosis_counter_ap(
  nic : N,
  position : UInt,
  station : UInt,
  mb_config : @mailbox.MailboxConfig,
  rmsm : @mailbox.Rmsm,
  timeout : @hal.Duration,
) -> UInt raise @hal.EcError {
  let payload = diagnosis_sdo_upload_ap(
    nic, position, station, mb_config, rmsm, 0x10F3U, b'\x00', timeout, "AP CoE 0x10F3:0 upload",
  )
  parse_u32_payload(payload)
}

///|
/// Read a diagnosis snapshot for one slave.
/// If 0x10F3 is unavailable, the counter is returned as None.
pub fn[N : @hal.Nic] read_slave_diagnosis(
  nic : N,
  station : UInt,
  mb_config : @mailbox.MailboxConfig,
  rmsm : @mailbox.Rmsm,
  timeout : @hal.Duration,
) -> SlaveDiagnosis raise @hal.EcError {
  let al_snapshot = Some(read_al_status_snapshot_fp(nic, station, timeout)) catch {
    _ => None
  }
  let err = read_error_register(nic, station, mb_config, rmsm, timeout)
  let diag = Some(
    read_diagnosis_counter(nic, station, mb_config, rmsm, timeout),
  ) catch {
    _ => None
  }
  let dl_counters : @protocol.DlErrorCounters? = Some(
    @protocol.read_dl_error_counters(nic, station, timeout),
  ) catch {
    _ => None
  }
  {
    station_address: station,
    al_state: match al_snapshot {
      Some(snapshot) => Some(snapshot.state)
      None => None
    },
    al_error_flag: match al_snapshot {
      Some(snapshot) => Some(snapshot.error_flag)
      None => None
    },
    al_status_code: match al_snapshot {
      Some(snapshot) => Some(snapshot.status_code)
      None => None
    },
    error_register: err,
    diagnosis_counter: diag,
    dl_error_counters: dl_counters,
  }
}

///|
/// Read a diagnosis snapshot for one slave via AP mailbox transport.
pub fn[N : @hal.Nic] read_slave_diagnosis_ap(
  nic : N,
  position : UInt,
  station : UInt,
  mb_config : @mailbox.MailboxConfig,
  rmsm : @mailbox.Rmsm,
  timeout : @hal.Duration,
) -> SlaveDiagnosis raise @hal.EcError {
  let al_snapshot = Some(read_al_status_snapshot_fp(nic, station, timeout)) catch {
    _ => None
  }
  let err = read_error_register_ap(
    nic, position, station, mb_config, rmsm, timeout,
  )
  let diag = Some(
    read_diagnosis_counter_ap(nic, position, station, mb_config, rmsm, timeout),
  ) catch {
    _ => None
  }
  let dl_counters : @protocol.DlErrorCounters? = Some(
    @protocol.read_dl_error_counters_ap(nic, position, timeout),
  ) catch {
    _ => None
  }
  {
    station_address: station,
    al_state: match al_snapshot {
      Some(snapshot) => Some(snapshot.state)
      None => None
    },
    al_error_flag: match al_snapshot {
      Some(snapshot) => Some(snapshot.error_flag)
      None => None
    },
    al_status_code: match al_snapshot {
      Some(snapshot) => Some(snapshot.status_code)
      None => None
    },
    error_register: err,
    diagnosis_counter: diag,
    dl_error_counters: dl_counters,
  }
}

///|
pub fn diagnostic_surface_from_slave_diagnosis(
  scan : ScanReport,
  report : SlaveDiagnosis,
) -> DiagnosticSurface {
  let issues : Array[DiagnosticIssue] = []
  match report.al_error_flag {
    Some(true) =>
      issues.push({
        source: Registers,
        category: AlStatus,
        level: Block,
        detail: "AL error flag set",
      })
    _ => ()
  }
  match report.al_status_code {
    Some(code) if code != 0U =>
      issues.push({
        source: Registers,
        category: AlStatus,
        level: Block,
        detail: "AL status code " + code.to_string(),
      })
    _ => ()
  }
  if report.error_register != b'\x00' {
    issues.push({
      source: Mailbox,
      category: ErrorRegister,
      level: Degrade,
      detail: "Error register " + report.error_register.to_uint().to_string(),
    })
  }
  match report.diagnosis_counter {
    Some(value) if value > 0U =>
      issues.push({
        source: Mailbox,
        category: DiagnosisHistory,
        level: Info,
        detail: "Diagnosis history count " + value.to_string(),
      })
    _ => ()
  }
  {
    station_address: Some(report.station_address),
    state_summary: {
      target_state: None,
      current_state: None,
      final_state: None,
      al_state: report.al_state,
      al_error_flag: report.al_error_flag,
      al_status_code: report.al_status_code,
      wkc: None,
    },
    configuration_summary: Some(
      configuration_surface_summary(scan, None, Some(OnlineScan), None, 0),
    ),
    error_register: Some(report.error_register),
    diagnosis_counter: report.diagnosis_counter,
    runtime_metrics: None,
    issues,
    overall_level: highest_diagnostic_level(issues, Info),
    monitor_verdicts: None,
    overall_verdict: None,
    topology_fingerprint: None,
  }
}

///|
pub fn diagnostic_surface_from_validate(
  scan : ScanReport,
  report : ValidateReport,
) -> DiagnosticSurface {
  let issues = report.differences.map(fn(diff) {
    validation_difference_issue(diff)
  })
  let baseline = diagnostic_level_from_validate_grade(report.grade)
  {
    station_address: None,
    state_summary: empty_state_surface_summary(),
    configuration_summary: Some(
      configuration_surface_summary(
        scan,
        Some(report.expected_source),
        Some(report.actual_source),
        Some(report.grade),
        report.differences.length(),
      ),
    ),
    error_register: None,
    diagnosis_counter: None,
    runtime_metrics: None,
    issues,
    overall_level: highest_diagnostic_level(issues, baseline),
    monitor_verdicts: None,
    overall_verdict: None,
    topology_fingerprint: None,
  }
}

///|
pub fn diagnostic_surface_from_run(report : RunReport) -> DiagnosticSurface {
  let base = diagnostic_surface_from_validate(report.scan, report.validation)
  let issues = base.issues
  let telemetry_issues = runtime_metrics_issues(
    report.telemetry,
    report.cycles_requested,
    report.cycles_ok,
  )
  for issue in telemetry_issues {
    issues.push(issue)
  }
  match report.fault {
    Some(fault) =>
      for issue in run_fault_issues(fault) {
        issues.push(issue)
      }
    None => ()
  }
  {
    ..base,
    runtime_metrics: Some(runtime_metrics_summary(report.telemetry)),
    issues,
    overall_level: highest_diagnostic_level(
      issues,
      highest_diagnostic_level(base.issues, base.overall_level),
    ),
    monitor_verdicts: None,
    overall_verdict: None,
    topology_fingerprint: None,
  }
}

///|
fn run_fault_issues(fault : RunFault) -> Array[DiagnosticIssue] {
  match fault {
    StartupTransitionFailed(
      station~,
      target~,
      current~,
      error_indication~,
      status_code~,
      raw_status_code~,
      control_wkc~
    ) => {
      let current_text = match current {
        Some(state) => @debug.to_string(state)
        None => "unknown"
      }
      let error_text = match error_indication {
        Some(true) => "set"
        Some(false) => "clear"
        None => "unknown"
      }
      let status_text = match status_code {
        Some(code) => code.to_string()
        None => "unknown"
      }
      let raw_status_text = match raw_status_code {
        Some(code) => code.to_string()
        None => "unknown"
      }
      let control_wkc_text = match control_wkc {
        Some(wkc) => wkc.to_string()
        None => "unknown"
      }
      [
        {
          source: StateTransition,
          category: StateTransition,
          level: Block,
          detail: "Startup transition failed for station " +
          station.to_string() +
          ": target=" +
          @debug.to_string(target) +
          " current=" +
          current_text +
          " error_flag=" +
          error_text +
          " status_code=" +
          status_text +
          " raw_status_code=" +
          raw_status_text +
          " control_wkc=" +
          control_wkc_text,
        },
      ]
    }
    TimeoutRecoveryRequired(observed_timeouts~, consecutive_timeouts~) =>
      [
        {
          source: RuntimeTelemetry,
          category: RuntimeMetrics,
          level: Block,
          detail: "Timeout recovery required: timeouts=" +
          observed_timeouts.to_string() +
          " consecutive_timeouts=" +
          consecutive_timeouts.to_string(),
        },
      ]
    RuntimeRecoveryRequired(expected_wkc~, observed_wkc_errors~) =>
      [
        {
          source: RuntimeTelemetry,
          category: RuntimeMetrics,
          level: Block,
          detail: "Runtime recovery required: expected_wkc=" +
          expected_wkc.to_string() +
          " wkc_errors=" +
          observed_wkc_errors.to_string(),
        },
      ]
    DcDrift(delta_ns~) =>
      [
        {
          source: RuntimeTelemetry,
          category: RuntimeMetrics,
          level: Block,
          detail: "DC drift detected: delta_ns=" + delta_ns.to_string(),
        },
      ]
  }
}

///|
pub fn diagnostic_surface_from_state_transition(
  station : UInt?,
  target : @protocol.EsmState,
  current : @protocol.EsmState?,
  final_state : @protocol.EsmState?,
  al_error_flag : Bool?,
  status_code : UInt?,
  wkc : UInt?,
) -> DiagnosticSurface {
  let issues : Array[DiagnosticIssue] = []
  match final_state {
    Some(state) if state != target =>
      issues.push({
        source: StateTransition,
        category: StateTransition,
        level: Block,
        detail: "Final state " +
        @debug.to_string(state) +
        " does not match target " +
        @debug.to_string(target),
      })
    _ => ()
  }
  match status_code {
    Some(code) if code != 0U =>
      issues.push({
        source: Registers,
        category: AlStatus,
        level: Block,
        detail: "AL status code " + code.to_string(),
      })
    _ => ()
  }
  match al_error_flag {
    Some(true) =>
      issues.push({
        source: Registers,
        category: AlStatus,
        level: Block,
        detail: "AL error indication is set",
      })
    _ => ()
  }
  match wkc {
    Some(value) if value == 0U =>
      issues.push({
        source: StateTransition,
        category: StateTransition,
        level: Degrade,
        detail: "WKC is zero during state transition",
      })
    _ => ()
  }
  {
    station_address: station,
    state_summary: {
      target_state: Some(target),
      current_state: current,
      final_state,
      al_state: None,
      al_error_flag,
      al_status_code: status_code,
      wkc,
    },
    configuration_summary: None,
    error_register: None,
    diagnosis_counter: None,
    runtime_metrics: None,
    issues,
    overall_level: highest_diagnostic_level(issues, Info),
    monitor_verdicts: None,
    overall_verdict: None,
    topology_fingerprint: None,
  }
}

///|
fn empty_state_surface_summary() -> StateSurfaceSummary {
  {
    target_state: None,
    current_state: None,
    final_state: None,
    al_state: None,
    al_error_flag: None,
    al_status_code: None,
    wkc: None,
  }
}

///|
fn configuration_surface_summary(
  scan : ScanReport,
  expected_source : ConfigurationSource?,
  actual_source : ConfigurationSource?,
  validation_grade : ValidateGrade?,
  difference_count : Int,
) -> ConfigurationSurfaceSummary {
  {
    slave_count: scan.slave_count,
    station_addresses: scan.slaves.map(fn(slave) { slave.station_address }),
    expected_source,
    actual_source,
    validation_grade,
    difference_count,
  }
}

///|
fn runtime_metrics_summary(telemetry : Telemetry) -> RuntimeMetricsSummary {
  {
    cycles: telemetry.cycles,
    tx_frames: telemetry.tx_frames,
    rx_frames: telemetry.rx_frames,
    timeouts: telemetry.timeouts,
    retries: telemetry.retries,
    wkc_errors: telemetry.wkc_errors,
    irq_events: telemetry.irq_events,
    dc_delay_updates: telemetry.dc_delay_updates,
    emergency_events: telemetry.emergency_events,
  }
}

///|
fn validation_difference_issue(
  diff : ConfigurationDifference,
) -> DiagnosticIssue {
  let (source, category) = match validation_difference_category(diff) {
    ValidateDifferenceCategory::Topology =>
      (DiagnosticSourceLayer::Topology, DiagnosticCategory::Topology)
    ValidateDifferenceCategory::Identity =>
      (DiagnosticSourceLayer::Configuration, DiagnosticCategory::Identity)
    ValidateDifferenceCategory::Configuration =>
      (DiagnosticSourceLayer::Configuration, DiagnosticCategory::Configuration)
    ValidateDifferenceCategory::Informational =>
      (DiagnosticSourceLayer::Configuration, DiagnosticCategory::Configuration)
  }
  {
    source,
    category,
    level: diagnostic_level_from_validate_grade(
      validation_difference_grade(diff),
    ),
    detail: @debug.to_string(diff),
  }
}

///|
fn diagnostic_level_from_validate_grade(
  grade : ValidateGrade,
) -> DiagnosticLevel {
  match grade {
    Pass => Info
    Warn => Degrade
    Block => Block
  }
}

///|
fn runtime_metrics_issues(
  telemetry : Telemetry,
  cycles_requested : Int,
  cycles_ok : Int,
) -> Array[DiagnosticIssue] {
  let issues : Array[DiagnosticIssue] = []
  if telemetry.timeouts > 0L {
    issues.push({
      source: RuntimeTelemetry,
      category: RuntimeMetrics,
      level: Degrade,
      detail: "Timeout count " + telemetry.timeouts.to_string(),
    })
  }
  if telemetry.wkc_errors > 0L {
    issues.push({
      source: RuntimeTelemetry,
      category: RuntimeMetrics,
      level: Degrade,
      detail: "WKC error count " + telemetry.wkc_errors.to_string(),
    })
  }
  if telemetry.retries > 0L {
    issues.push({
      source: RuntimeTelemetry,
      category: RuntimeMetrics,
      level: Info,
      detail: "Retry count " + telemetry.retries.to_string(),
    })
  }
  if cycles_ok < cycles_requested {
    issues.push({
      source: RuntimeTelemetry,
      category: RuntimeMetrics,
      level: Degrade,
      detail: "Cycle completion " +
      cycles_ok.to_string() +
      "/" +
      cycles_requested.to_string(),
    })
  }
  issues
}

///|
/// Convert a Verdict severity to a DiagnosticLevel.
pub fn verdict_to_diagnostic_level(v : Verdict) -> DiagnosticLevel {
  match v {
    Pass => Info
    Warn(_) => Info
    Fail(_) => Degrade
    Block(_) => Block
  }
}

///|
/// Convert a non-Pass MonitorResult into a DiagnosticIssue.
/// Returns None for Pass verdicts.
pub fn monitor_result_to_issue(r : MonitorResult) -> DiagnosticIssue? {
  match r.verdict {
    Pass => None
    Warn(msg) =>
      Some({
        source: Monitor,
        category: MonitorVerdict,
        level: Info,
        detail: r.name + ": " + msg,
      })
    Fail(msg) =>
      Some({
        source: Monitor,
        category: MonitorVerdict,
        level: Degrade,
        detail: r.name + ": " + msg,
      })
    Block(msg) =>
      Some({
        source: Monitor,
        category: MonitorVerdict,
        level: Block,
        detail: r.name + ": " + msg,
      })
  }
}

///|
/// Enrich an existing DiagnosticSurface with monitor verdicts.
/// Monitor-originated issues are appended to the issue list and
/// the overall level is recomputed.
pub fn enrich_surface_with_monitors(
  surface : DiagnosticSurface,
  results : Array[MonitorResult],
  verdict : Verdict,
) -> DiagnosticSurface {
  let issues = surface.issues.copy()
  for r in results {
    match monitor_result_to_issue(r) {
      Some(issue) => issues.push(issue)
      None => ()
    }
  }
  {
    ..surface,
    issues,
    overall_level: highest_diagnostic_level(issues, surface.overall_level),
    monitor_verdicts: Some(results),
    overall_verdict: Some(verdict),
  }
}

///|
/// Enrich an existing DiagnosticSurface with a topology fingerprint.
pub fn enrich_surface_with_fingerprint(
  surface : DiagnosticSurface,
  fingerprint : TopologyFingerprint,
) -> DiagnosticSurface {
  { ..surface, topology_fingerprint: Some(fingerprint) }
}

///|
fn highest_diagnostic_level(
  issues : Array[DiagnosticIssue],
  fallback : DiagnosticLevel,
) -> DiagnosticLevel {
  let mut level = fallback
  for issue in issues {
    if diagnostic_level_rank(issue.level) > diagnostic_level_rank(level) {
      level = issue.level
    }
  }
  level
}

///|
fn diagnostic_level_rank(level : DiagnosticLevel) -> Int {
  match level {
    Info => 0
    Degrade => 1
    Block => 2
  }
}

///|
/// Prepare one slave for mailbox-based diagnosis via AP discovery and FP state control.
///
/// This flow is intentionally diagnosis-specific: it normalizes the slave back to
/// Init before entering PreOp so mailbox SM programming happens from a known base state.
pub fn[N : @hal.Nic] prepare_slave_diagnosis_ap(
  nic : N,
  position : UInt,
  station : UInt,
  timeout : @hal.Duration,
) -> DiagnosisSession raise @hal.EcError {
  let original_snapshot_result : Result[AlStatusSnapshot, Error] = Result::Ok(
    diagnosis_timeout_step(
      Result::Ok(read_al_status_snapshot_fp(nic, station, timeout)) catch {
        err => Result::Err(err)
      },
      "diagnosis.prepare.read_al_status_fp",
    ),
  ) catch {
    err => Result::Err(err)
  }
  let original_state = match original_snapshot_result {
    Ok(snapshot) => snapshot.state
    Err(_) => @protocol.Init
  }
  let eeprom = diagnosis_timeout_step(
    Result::Ok(
      read_sii_with_metadata_ap(
        nic, position, diagnosis_sii_word_count, timeout,
      ),
    ) catch {
      err => Result::Err(err)
    },
    "diagnosis.prepare.eeprom_read_ap",
  )
  let sii = @mailbox.parse_sii_header(eeprom)
  let mailbox_config = @mailbox.MailboxConfig::from_sii(sii)
  if !mailbox_config.is_valid() {
    raise @hal.EcError::ConfigMismatch(
      "diagnosis requires mailbox-capable slave at station " +
      station.to_string(),
    )
  }
  configure_diagnosis_mailbox_sms(nic, station, eeprom, timeout)
  enter_diagnosis_preop(nic, station, original_state, timeout)
  {
    original_state,
    observed_state: match original_snapshot_result {
      Ok(snapshot) => Some(snapshot.state)
      Err(_) => None
    },
    observed_error_flag: match original_snapshot_result {
      Ok(snapshot) => Some(snapshot.error_flag)
      Err(_) => None
    },
    observed_status_code: match original_snapshot_result {
      Ok(snapshot) => Some(snapshot.status_code)
      Err(_) => None
    },
    mailbox_config,
  }
}

///|
/// Prepare one slave for generic mailbox-based access via AP discovery and FP state control.
/// Unlike diagnosis-specific helpers, this only returns the mailbox configuration and
/// original state needed for restoring the slave after read-only mailbox operations.
pub fn[N : @hal.Nic] prepare_slave_mailbox_ap(
  nic : N,
  position : UInt,
  station : UInt,
  timeout : @hal.Duration,
) -> MailboxSession raise @hal.EcError {
  let original_snapshot_result : Result[AlStatusSnapshot, Error] = Result::Ok(
    diagnosis_timeout_step(
      Result::Ok(read_al_status_snapshot_fp(nic, station, timeout)) catch {
        err => Result::Err(err)
      },
      "mailbox.prepare.read_al_status_fp",
    ),
  ) catch {
    err => Result::Err(err)
  }
  let original_state = match original_snapshot_result {
    Ok(snapshot) => snapshot.state
    Err(_) => @protocol.Init
  }
  let eeprom = diagnosis_timeout_step(
    Result::Ok(
      read_sii_with_metadata_ap(
        nic, position, diagnosis_sii_word_count, timeout,
      ),
    ) catch {
      err => Result::Err(err)
    },
    "mailbox.prepare.eeprom_read_ap",
  )
  let sii = @mailbox.parse_sii_header(eeprom)
  let mailbox_config = @mailbox.MailboxConfig::from_sii(sii)
  if !mailbox_config.is_valid() {
    raise @hal.EcError::ConfigMismatch(
      "mailbox access requires mailbox-capable slave at station " +
      station.to_string(),
    )
  }
  configure_diagnosis_mailbox_sms(nic, station, eeprom, timeout)
  enter_diagnosis_preop(nic, station, original_state, timeout)
  { original_state, mailbox_config }
}

///|
/// Restore a slave after diagnosis using a legal path from PreOp back to the original state.
pub fn[N : @hal.Nic] restore_slave_diagnosis_state(
  nic : N,
  station : UInt,
  original_state : @protocol.EsmState,
  timeout : @hal.Duration,
) -> @protocol.EsmState raise @hal.EcError {
  let restore_path = diagnosis_restore_path(original_state)
  diagnosis_timeout_step(
    Result::Ok(
      @protocol.transition_through(nic, station, restore_path, timeout),
    ) catch {
      err => Result::Err(err)
    },
    "diagnosis.restore_original_state_fp",
  )
}

///|
/// Restore a slave after generic mailbox-based access using the legal path from
/// PreOp back to the original state.
pub fn[N : @hal.Nic] restore_slave_mailbox_state(
  nic : N,
  station : UInt,
  original_state : @protocol.EsmState,
  timeout : @hal.Duration,
) -> @protocol.EsmState raise @hal.EcError {
  let restore_path = diagnosis_restore_path(original_state)
  diagnosis_timeout_step(
    Result::Ok(
      @protocol.transition_through(nic, station, restore_path, timeout),
    ) catch {
      err => Result::Err(err)
    },
    "mailbox.restore_original_state_fp",
  )
}

///|
/// Build the diagnosis-specific restore path from PreOp back to the target state.
pub fn diagnosis_restore_path(
  target : @protocol.EsmState,
) -> Array[@protocol.EsmState] {
  match target {
    @protocol.PreOp => [@protocol.PreOp]
    @protocol.SafeOp => [@protocol.PreOp, @protocol.SafeOp]
    @protocol.Op => [@protocol.PreOp, @protocol.SafeOp, @protocol.Op]
    @protocol.Init => [@protocol.PreOp, @protocol.Init]
    @protocol.Boot => [@protocol.PreOp, @protocol.Init, @protocol.Boot]
  }
}

///|
fn[T] diagnosis_timeout_step(
  result : Result[T, Error],
  stage : String,
) -> T raise @hal.EcError {
  match result {
    Ok(value) => value
    Err(@hal.EcError::RecvTimeout) =>
      raise @hal.EcError::Internal("timeout at " + stage + " (RecvTimeout)")
    Err(@hal.EcError::MailboxTimeout) =>
      raise @hal.EcError::Internal("timeout at " + stage + " (MailboxTimeout)")
    Err(@hal.EcError::Internal(message)) if message.contains("timeout at ") =>
      raise @hal.EcError::Internal(stage + " -> " + message)
    Err(err) => raise @hal.EcError::Internal(err.to_string())
  }
}

///|
fn[N : @hal.Nic] read_al_status_snapshot_fp(
  nic : N,
  station : UInt,
  timeout : @hal.Duration,
) -> AlStatusSnapshot raise @hal.EcError {
  let pdu = @protocol.read_register_fp(
    nic, station, @protocol.reg_al_status, 6, timeout,
  )
  if pdu.data.length() < 1 {
    raise @hal.EcError::EsmStateError("empty AL Status response")
  }
  let state = @protocol.EsmState::from_byte(pdu.data[0])
  let error_flag = (pdu.data[0] & b'\x10') != b'\x00'
  let status_code = if pdu.data.length() >= 6 {
    pdu.data[4].to_int().reinterpret_as_uint() |
    (pdu.data[5].to_int().reinterpret_as_uint() << 8)
  } else {
    0U
  }
  { state, error_flag, status_code }
}

///|
fn[N : @hal.Nic] configure_diagnosis_mailbox_sms(
  nic : N,
  station : UInt,
  eeprom : Bytes,
  timeout : @hal.Duration,
) -> Unit raise @hal.EcError {
  let mapping = @mailbox.calculate_slave_mapping(eeprom, 0U)
  for i = 0; i < mapping.sm_configs.length(); i = i + 1 {
    let sm = mapping.sm_configs[i]
    match sm.sm_type {
      @mailbox.MailboxOut | @mailbox.MailboxIn => {
        let sm_addr = @protocol.reg_sm_base +
          i.reinterpret_as_uint() *
          @protocol.sm_entry_size.reinterpret_as_uint()
        let _ = @protocol.write_register_fp(
          nic,
          station,
          sm_addr,
          sm.to_bytes(),
          timeout,
        )
      }
      _ => ()
    }
  }
}

///|
fn[N : @hal.Nic] enter_diagnosis_preop(
  nic : N,
  station : UInt,
  original_state : @protocol.EsmState,
  timeout : @hal.Duration,
) -> Unit raise @hal.EcError {
  if original_state != @protocol.Init {
    ignore(
      diagnosis_timeout_step(
        Result::Ok(
          @protocol.transition_through(
            nic,
            station,
            @protocol.build_shutdown_path(original_state),
            timeout,
          ),
        ) catch {
          err => Result::Err(err)
        },
        "diagnosis.prepare.request_init_fp",
      ),
    )
  }
  let preop_snapshot = diagnosis_timeout_step(
    Result::Ok(@protocol.read_al_status_snapshot(nic, station, timeout)) catch {
      err => Result::Err(err)
    },
    "diagnosis.prepare.read_al_status_before_preop_fp",
  )
  if preop_snapshot.error_indication {
    ignore(
      diagnosis_timeout_step(
        Result::Ok(
          @protocol.recover_al_error_for_transition(nic, station, timeout),
        ) catch {
          err => Result::Err(err)
        },
        "diagnosis.prepare.clear_al_error_fp",
      ),
    )
  }
  let confirmed_preop = diagnosis_timeout_step(
    Result::Ok(
      @protocol.transition_to_target_strict(
        nic,
        station,
        @protocol.PreOp,
        timeout,
      ),
    ) catch {
      err => Result::Err(err)
    },
    "diagnosis.prepare.request_preop_fp",
  )
  if confirmed_preop != @protocol.PreOp {
    let status_code = Result::Ok(
      @protocol.read_al_status_code(nic, station, timeout),
    ) catch {
      err => Result::Err(err)
    }
    let status_code_text = match status_code {
      Ok(code) => code.to_string()
      Err(err) => @debug.to_string(err)
    }
    raise @hal.EcError::Internal(
      "diagnosis failed to enter PreOp: actual=" +
      @debug.to_string(confirmed_preop) +
      ", AL status code=" +
      status_code_text,
    )
  }
}

///|
fn[N : @hal.Nic] diagnosis_sdo_upload_ap(
  nic : N,
  position : UInt,
  station : UInt,
  mb_config : @mailbox.MailboxConfig,
  rmsm : @mailbox.Rmsm,
  index : UInt,
  subindex : Byte,
  timeout : @hal.Duration,
  stage : String,
) -> Bytes raise @hal.EcError {
  for attempt = 0; attempt < diagnosis_sdo_max_attempts; attempt = attempt + 1 {
    let result = Result::Ok(
      diagnosis_sdo_upload_ap_once(
        nic, position, station, mb_config, rmsm, index, subindex, timeout, stage,
      ),
    ) catch {
      err => Result::Err(err)
    }
    match result {
      Ok(payload) => return payload
      Err(@hal.EcError::Internal(message)) =>
        if message.contains("MailboxTimeout") &&
          attempt + 1 < diagnosis_sdo_max_attempts {
          rmsm.reset()
          diagnosis_drain_mailbox_fp(nic, station, mb_config)
        } else {
          raise @hal.EcError::Internal(message)
        }
      Err(@hal.EcError::MailboxTimeout) =>
        if attempt + 1 < diagnosis_sdo_max_attempts {
          rmsm.reset()
          diagnosis_drain_mailbox_fp(nic, station, mb_config)
        } else {
          raise @hal.EcError::MailboxTimeout
        }
      Err(err) => raise err
    }
  }
  raise @hal.EcError::MailboxTimeout
}

///|
fn[N : @hal.Nic] diagnosis_sdo_upload_ap_once(
  nic : N,
  position : UInt,
  station : UInt,
  mb_config : @mailbox.MailboxConfig,
  rmsm : @mailbox.Rmsm,
  index : UInt,
  subindex : Byte,
  timeout : @hal.Duration,
  stage : String,
) -> Bytes raise @hal.EcError {
  ignore(position)
  let counter = diagnosis_timeout_step(
    Result::Ok(rmsm.begin_transaction()) catch {
      err => Result::Err(err)
    },
    stage + ".begin_transaction",
  )
  let txn = @mailbox.build_sdo_upload(counter, station, index, subindex)
  let frame = txn.encode()
  diagnosis_drain_mailbox_fp(nic, station, mb_config)
  let _ = diagnosis_timeout_step(
    Result::Ok(@protocol.mailbox_send(nic, station, mb_config, frame, timeout)) catch {
      err => Result::Err(err)
    },
    stage + ".mailbox_send_fp",
  )
  for _stale_attempt = 0
      _stale_attempt < 4
      _stale_attempt = _stale_attempt + 1 {
    let response_raw = diagnosis_mailbox_recv_fp(
      nic,
      station,
      mb_config,
      rmsm,
      timeout,
      stage + ".mailbox_recv_fp",
    )
    let decoded = diagnosis_timeout_step(
      Result::Ok(@mailbox.decode_sdo_response(response_raw)) catch {
        err => Result::Err(err)
      },
      stage + ".decode_sdo_response",
    )
    let (mbh, resp) = decoded
    if !diagnosis_accept_response_counter(rmsm, mbh.counter) {
      continue
    }
    if resp.command != @mailbox.SdoCommand::UploadResponse {
      raise @hal.EcError::MailboxProtocol(
        stage +
        ": expected UploadResponse, got " +
        @debug.to_string(resp.command),
      )
    }
    if resp.index != index || resp.subindex != subindex {
      raise @hal.EcError::MailboxProtocol(
        stage + ": index/subindex mismatch in response",
      )
    }
    if resp.segmented {
      raise @hal.EcError::MailboxProtocol(
        stage + ": segmented response is not supported in diagnosis path",
      )
    }
    return resp.data
  }
  raise @hal.EcError::MailboxProtocol("RMSM: too many stale frames received")
}

///|
fn[N : @hal.Nic] diagnosis_mailbox_recv_fp(
  nic : N,
  station : UInt,
  mb_config : @mailbox.MailboxConfig,
  rmsm : @mailbox.Rmsm,
  timeout : @hal.Duration,
  stage : String,
) -> Bytes raise @hal.EcError {
  let recv_result = Result::Ok(
    @protocol.mailbox_recv_rmsm(
      nic,
      station,
      mb_config,
      rmsm,
      timeout,
      max_polls=diagnosis_mailbox_max_polls,
    ),
  ) catch {
    err => Result::Err(err)
  }
  match recv_result {
    Ok(frame) => return frame
    Err(@hal.EcError::MailboxTimeout) => ()
    Err(err) => raise err
  }
  let sm0_status = diagnosis_register_snapshot(
    Result::Ok(
      @protocol.read_register_fp(
        nic, station, @protocol.reg_sm_base, diagnosis_sm_window_size, timeout,
      ),
    ) catch {
      err => Result::Err(err)
    },
  )
  let sm1_status = diagnosis_register_snapshot(
    Result::Ok(
      @protocol.read_register_fp(
        nic,
        station,
        @protocol.reg_sm_base + @protocol.sm_entry_size.reinterpret_as_uint(),
        diagnosis_sm_window_size,
        timeout,
      ),
    ) catch {
      err => Result::Err(err)
    },
  )
  let al_status = diagnosis_register_snapshot(
    Result::Ok(
      @protocol.read_register_fp(
        nic, station, @protocol.reg_al_status, 2, timeout,
      ),
    ) catch {
      err => Result::Err(err)
    },
  )
  raise @hal.EcError::Internal(
    "timeout at " +
    stage +
    ".read_frame (MailboxTimeout; SM0=" +
    sm0_status +
    "; SM1=" +
    sm1_status +
    "; AL=" +
    al_status +
    ")",
  )
}

///|
fn[N : @hal.Nic] diagnosis_drain_mailbox_fp(
  nic : N,
  station : UInt,
  mb_config : @mailbox.MailboxConfig,
) -> Unit {
  let zero_timeout = @hal.Duration(0L)
  let poll_result = Result::Ok(
    @protocol.mailbox_poll(nic, station, mb_config, zero_timeout),
  ) catch {
    err => Result::Err(err)
  }
  match poll_result {
    Ok(true) =>
      ignore(
        Result::Ok(
          @protocol.mailbox_recv(
            nic,
            station,
            mb_config,
            zero_timeout,
            max_polls=1,
          ),
        ) catch {
          err => Result::Err(err)
        },
      )
    _ => ()
  }
}

///|
fn diagnosis_register_snapshot(result : Result[@protocol.Pdu, Error]) -> String {
  match result {
    Ok(pdu) => diagnosis_bytes_preview(pdu.data)
    Err(err) => "error=" + err.to_string()
  }
}

///|
fn diagnosis_accept_response_counter(
  rmsm : @mailbox.Rmsm,
  counter : Byte,
) -> Bool raise @hal.EcError {
  let validation = Result::Ok(rmsm.validate_response(counter)) catch {
    err => Result::Err(err)
  }
  match validation {
    Ok(is_new) => is_new
    Err(@hal.EcError::MailboxProtocol(message)) =>
      if message.contains("counter mismatch") {
        rmsm.reset()
        false
      } else {
        raise @hal.EcError::MailboxProtocol(message)
      }
    Err(err) => raise err
  }
}

///|
fn diagnosis_bytes_preview(data : Bytes) -> String {
  if data.length() == 0 {
    return ""
  }
  let mut rendered = ""
  for i = 0; i < data.length(); i = i + 1 {
    if i > 0 {
      rendered = rendered + " "
    }
    rendered = rendered + diagnosis_hex_byte(data[i])
  }
  rendered
}

///|
fn diagnosis_hex_byte(value : Byte) -> String {
  "0x" +
  diagnosis_hex_digit((value.to_int() >> 4) & 0xF) +
  diagnosis_hex_digit(value.to_int() & 0xF)
}

///|
fn diagnosis_hex_digit(value : Int) -> String {
  match value {
    0 => "0"
    1 => "1"
    2 => "2"
    3 => "3"
    4 => "4"
    5 => "5"
    6 => "6"
    7 => "7"
    8 => "8"
    9 => "9"
    10 => "A"
    11 => "B"
    12 => "C"
    13 => "D"
    14 => "E"
    _ => "F"
  }
}