///|
/// Runtime scheduler: unified cyclic loop driving PDO exchange,
/// ESM management, timeout governance, and diagnostic collection.

///|
/// Runtime orchestrator: manages the full lifecycle from scan to cyclic PDO.
pub(all) struct Runtime {
  /// Current state of the runtime.
  mut state : RuntimeState
  /// Telemetry counters.
  telemetry : Telemetry
  /// Master configuration.
  config : @hal.MasterConfig
  /// PDO context for cyclic exchange.
  mut pdo_ctx : @protocol.PdoContext?
  /// Expected working counter per cycle.
  mut expected_wkc : UInt
  /// Number of consecutive WKC errors before triggering recovery.
  max_wkc_errors : Int
  /// Current consecutive WKC error count.
  mut consecutive_wkc_errors : Int
  /// Number of consecutive receive timeouts before triggering recovery.
  mut max_consecutive_timeouts : Int
  /// Current consecutive receive timeout count.
  mut consecutive_timeouts : Int
  /// DC synchronization state.
  mut dc_state : @hil.DcState
  /// Reference station for FRMW drift compensation.
  mut dc_reference_station : UInt?
  /// DC station set used for runtime delay re-estimation.
  mut dc_stations : Array[UInt]
  /// Per-cycle counter for periodic delay re-estimation.
  mut dc_cycle_counter : Int
  /// Stations with mailbox support for Emergency polling.
  mut emergency_stations : Array[(UInt, @mailbox.MailboxConfig)]
  /// EoE Virtual Switch engine.
  eoe_switch : EoeSwitch
} derive(Debug)

///|
pub fn Runtime::new(config : @hal.MasterConfig) -> Runtime {
  {
    state: Idle,
    telemetry: Telemetry::new(),
    config,
    pdo_ctx: None,
    expected_wkc: 0U,
    max_wkc_errors: 5,
    consecutive_wkc_errors: 0,
    max_consecutive_timeouts: 5,
    consecutive_timeouts: 0,
    dc_state: @hil.Disabled,
    dc_reference_station: None,
    dc_stations: [],
    dc_cycle_counter: 0,
    emergency_stations: [],
    eoe_switch: EoeSwitch::new(),
  }
}

///|
/// Configure the threshold for consecutive receive timeouts.
pub fn Runtime::set_timeout_threshold(self : Runtime, threshold : Int) -> Unit {
  self.max_consecutive_timeouts = if threshold > 0 { threshold } else { 1 }
}

///|
/// Initialize distributed clocks for the configured station list.
/// Returns true when DC was initialized and locked.
pub fn[N : @hal.Nic] Runtime::init_dc(
  self : Runtime,
  nic : N,
  stations : Array[UInt],
  timeout : @hal.Duration,
) -> Bool raise @hal.EcError {
  if !self.config.dc_enabled {
    self.dc_state = @hil.Disabled
    self.dc_reference_station = None
    self.dc_stations = []
    return false
  }
  if stations.length() == 0 {
    self.dc_state = @hil.Disabled
    self.dc_reference_station = None
    self.dc_stations = []
    return false
  }
  self.dc_state = @hil.Measuring
  let init = @protocol.dc_configure_linear(
    nic,
    stations,
    self.config.cycle_period_ns.to_int().reinterpret_as_uint(),
    timeout,
    static_sync_iterations=200,
  )
  match init {
    Some(info) => {
      self.dc_reference_station = Some(info.reference_station)
      self.dc_stations = stations
      self.dc_cycle_counter = 0
      self.dc_state = @hil.Locked
      true
    }
    None => {
      self.dc_reference_station = None
      self.dc_stations = []
      self.dc_state = @hil.Disabled
      false
    }
  }
}

///|
/// Set the PDO context and expected WKC for cyclic operation.
pub fn Runtime::configure_pdo(
  self : Runtime,
  ctx : @protocol.PdoContext,
  expected_wkc : UInt,
) -> Unit {
  self.pdo_ctx = Some(ctx)
  self.expected_wkc = expected_wkc
}

///|
/// Transition the runtime state machine.
pub fn Runtime::set_state(self : Runtime, new_state : RuntimeState) -> Unit {
  self.state = new_state
}

///|
/// Configure stations eligible for Emergency mailbox polling during PDO cycles.
pub fn Runtime::configure_emergency_stations(
  self : Runtime,
  stations : Array[(UInt, @mailbox.MailboxConfig)],
) -> Unit {
  self.emergency_stations = stations
}

///|
/// Poll all configured emergency stations for unsolicited mailbox data.
/// Returns the number of Emergency messages received this cycle.
fn[N : @hal.Nic] Runtime::poll_emergencies(
  self : Runtime,
  nic : N,
  timeout : @hal.Duration,
) -> Int {
  let mut count = 0
  for item in self.emergency_stations {
    let (station, mb_config) = item
    let has_data = @protocol.mailbox_poll(nic, station, mb_config, timeout) catch {
      _ => false
    }
    if has_data {
      let frame = @protocol.mailbox_recv(
        nic,
        station,
        mb_config,
        timeout,
        max_polls=1,
      ) catch {
        _ => continue
      }
      // Try to decode as CoE Emergency; ignore non-Emergency mailbox data.
      let _ = @mailbox.decode_emergency_frame(frame) catch { _ => continue }
      count = count + 1
    }
  }
  count
}

///|
/// Execute one PDO cycle in the runtime loop.
/// Updates telemetry and tracks consecutive errors.
pub fn[N : @hal.Nic] Runtime::run_cycle(
  self : Runtime,
  nic : N,
  timeout : @hal.Duration,
) -> @hil.CycleResult raise @hal.EcError {
  guard self.pdo_ctx is Some(ctx) else {
    raise @hal.EcError::Internal("PDO context not configured")
  }
  self.telemetry.cycles = self.telemetry.cycles + 1L
  self.telemetry.tx_frames = self.telemetry.tx_frames + 1L
  // FRMW drift compensation frame before PDO cycle when DC is active.
  if self.config.dc_enabled {
    match self.dc_reference_station {
      Some(station) =>
        @protocol.dc_compensate_cycle(nic, station, timeout) catch {
          _ => self.dc_state = @hil.Drifting(1L)
        }
      None => ()
    }
    self.dc_cycle_counter = self.dc_cycle_counter + 1
    if self.dc_cycle_counter >= 100 {
      self.dc_cycle_counter = 0
      match self.dc_reference_station {
        Some(station) =>
          if self.dc_stations.length() > 0 {
            @protocol.dc_reestimate_propagation_delays(
              nic,
              station,
              self.dc_stations,
              timeout,
            ) catch {
              _ => self.dc_state = @hil.Drifting(2L)
            }
            self.telemetry.dc_delay_updates = self.telemetry.dc_delay_updates +
              1L
          }
        None => ()
      }
    }
  }
  let pdo_result = @protocol.pdo_exchange_result(nic, ctx, timeout)
  let wkc = pdo_result.wkc
  self.telemetry.rx_frames = self.telemetry.rx_frames + 1L
  if pdo_result.irq != 0U {
    self.telemetry.irq_events = self.telemetry.irq_events + 1L
  }
  // Poll unsolicited Emergency mailbox after PDO exchange.
  if self.emergency_stations.length() > 0 {
    let emg_count = self.poll_emergencies(nic, timeout)
    if emg_count > 0 {
      self.telemetry.emergency_events = self.telemetry.emergency_events +
        emg_count.to_int64()
    }
  }
  // Poll EoE stations for incoming Ethernet-over-EtherCAT frames.
  if self.eoe_switch.stations.length() > 0 {
    let _eoe_count = self.eoe_switch.poll(nic, timeout)
  }
  self.consecutive_timeouts = 0
  let ok = wkc >= self.expected_wkc
  if ok {
    self.consecutive_wkc_errors = 0
  } else {
    self.consecutive_wkc_errors = self.consecutive_wkc_errors + 1
    self.telemetry.wkc_errors = self.telemetry.wkc_errors + 1L
  }
  { wkc, expected_wkc: self.expected_wkc, ok }
}

///|
/// Check if the runtime needs recovery due to excessive WKC errors.
pub fn Runtime::needs_recovery(self : Runtime) -> Bool {
  self.consecutive_wkc_errors >= self.max_wkc_errors ||
  self.consecutive_timeouts >= self.max_consecutive_timeouts ||
  self.dc_state is @hil.Drifting(_)
}

///|
/// Reset the WKC error counter after recovery.
pub fn Runtime::reset_error_counter(self : Runtime) -> Unit {
  self.consecutive_wkc_errors = 0
  self.consecutive_timeouts = 0
}

///|
/// Record a timeout event in telemetry.
pub fn Runtime::record_timeout(self : Runtime) -> Unit {
  self.telemetry.timeouts = self.telemetry.timeouts + 1L
  self.consecutive_timeouts = self.consecutive_timeouts + 1
}

///|
/// Record a retry event in telemetry.
pub fn Runtime::record_retry(self : Runtime) -> Unit {
  self.telemetry.retries = self.telemetry.retries + 1L
}

///|
/// Get a snapshot of the current telemetry.
pub fn Runtime::get_telemetry(self : Runtime) -> Telemetry {
  {
    cycles: self.telemetry.cycles,
    tx_frames: self.telemetry.tx_frames,
    rx_frames: self.telemetry.rx_frames,
    timeouts: self.telemetry.timeouts,
    retries: self.telemetry.retries,
    wkc_errors: self.telemetry.wkc_errors,
    irq_events: self.telemetry.irq_events,
    dc_delay_updates: self.telemetry.dc_delay_updates,
    emergency_events: self.telemetry.emergency_events,
  }
}

///|
/// Run N PDO cycles. Returns the number of successful cycles.
pub fn[N : @hal.Nic] Runtime::run_cycles(
  self : Runtime,
  nic : N,
  count : Int,
  timeout : @hal.Duration,
) -> Int raise @hal.EcError {
  self.run_cycles_with_reporting(nic, count, timeout, None)
}

///|
/// Run N PDO cycles and optionally emit progress snapshots.
pub fn[N : @hal.Nic] Runtime::run_cycles_with_reporting(
  self : Runtime,
  nic : N,
  count : Int,
  timeout : @hal.Duration,
  progress_every_cycles : Int?,
) -> Int raise @hal.EcError {
  self.run_cycles_with_reporting_to(
    nic,
    count,
    timeout,
    progress_every_cycles,
    None,
    NullRunProgressSink::new(),
  )
}

///|
/// Run N PDO cycles and emit structured progress snapshots to the provided sink.
pub fn[N : @hal.Nic, S : RunProgressSink] Runtime::run_cycles_with_reporting_to(
  self : Runtime,
  nic : N,
  count : Int,
  timeout : @hal.Duration,
  progress_every_cycles : Int?,
  output_period_ns : Int?,
  sink : S,
) -> Int raise @hal.EcError {
  let mut success = 0
  for _i = 0; _i < count; _i = _i + 1 {
    let result = self.run_cycle(nic, timeout) catch {
      @hal.EcError::RecvTimeout => {
        self.record_timeout()
        emit_runtime_progress(
          self,
          success,
          progress_every_cycles,
          output_period_ns,
          Some(count),
          false,
          sink,
        )
        if self.needs_recovery() {
          self.set_state(Error(@hal.EcError::RecvTimeout))
          break
        }
        continue
      }
      err => raise err
    }
    if result.ok {
      success = success + 1
    }
    emit_runtime_progress(
      self,
      success,
      progress_every_cycles,
      output_period_ns,
      Some(count),
      false,
      sink,
    )
    if self.needs_recovery() {
      match self.dc_state {
        @hil.Drifting(delta_ns) =>
          self.set_state(
            Error(
              @hal.EcError::Internal(
                "dc drift detected: " + delta_ns.to_string(),
              ),
            ),
          )
        _ =>
          if self.consecutive_timeouts >= self.max_consecutive_timeouts {
            self.set_state(Error(@hal.EcError::RecvTimeout))
          } else {
            self.set_state(
              Error(
                @hal.EcError::UnexpectedWkc(
                  expected=self.expected_wkc.reinterpret_as_int(),
                  actual=0,
                ),
              ),
            )
          }
      }
      break
    }
  }
  success
}

///|
/// Run PDO cycles until a runtime recovery condition occurs.
pub fn[N : @hal.Nic] Runtime::run_until_fault(
  self : Runtime,
  nic : N,
  timeout : @hal.Duration,
) -> Int raise @hal.EcError {
  self.run_until_fault_with_reporting(nic, timeout, None)
}

///|
/// Run PDO cycles until a runtime recovery condition occurs and optionally
/// emit progress snapshots.
pub fn[N : @hal.Nic] Runtime::run_until_fault_with_reporting(
  self : Runtime,
  nic : N,
  timeout : @hal.Duration,
  progress_every_cycles : Int?,
) -> Int raise @hal.EcError {
  self.run_until_fault_with_reporting_to(
    nic,
    timeout,
    progress_every_cycles,
    None,
    NullRunProgressSink::new(),
  )
}

///|
/// Run PDO cycles until a runtime recovery condition occurs and emit
/// structured progress snapshots to the provided sink.
pub fn[N : @hal.Nic, S : RunProgressSink] Runtime::run_until_fault_with_reporting_to(
  self : Runtime,
  nic : N,
  timeout : @hal.Duration,
  progress_every_cycles : Int?,
  output_period_ns : Int?,
  sink : S,
) -> Int raise @hal.EcError {
  let mut success = 0
  while true {
    let result = self.run_cycle(nic, timeout) catch {
      @hal.EcError::RecvTimeout => {
        self.record_timeout()
        emit_runtime_progress(
          self,
          success,
          progress_every_cycles,
          output_period_ns,
          None,
          true,
          sink,
        )
        if self.needs_recovery() {
          self.set_state(Error(@hal.EcError::RecvTimeout))
          break
        }
        continue
      }
      err => raise err
    }
    if result.ok {
      success = success + 1
    }
    emit_runtime_progress(
      self,
      success,
      progress_every_cycles,
      output_period_ns,
      None,
      true,
      sink,
    )
    if self.needs_recovery() {
      match self.dc_state {
        @hil.Drifting(delta_ns) =>
          self.set_state(
            Error(
              @hal.EcError::Internal(
                "dc drift detected: " + delta_ns.to_string(),
              ),
            ),
          )
        _ =>
          if self.consecutive_timeouts >= self.max_consecutive_timeouts {
            self.set_state(Error(@hal.EcError::RecvTimeout))
          } else {
            self.set_state(
              Error(
                @hal.EcError::UnexpectedWkc(
                  expected=self.expected_wkc.reinterpret_as_int(),
                  actual=0,
                ),
              ),
            )
          }
      }
      break
    }
  }
  success
}

///|
fn[S : RunProgressSink] emit_runtime_progress(
  runtime : Runtime,
  success : Int,
  progress_every_cycles : Int?,
  output_period_ns : Int?,
  cycles_requested : Int?,
  until_fault : Bool,
  sink : S,
) -> Unit {
  match progress_every_cycles {
    Some(every) =>
      if every > 0 && runtime.telemetry.cycles % every.to_int64() == 0L {
        match output_period_ns {
          Some(period_ns) =>
            sink.emit({
              cycles_completed: runtime.telemetry.cycles,
              cycles_ok: success,
              cycles_requested,
              until_fault,
              cycle_period_ns: runtime.config.cycle_period_ns,
              output_period_ns: period_ns,
              telemetry: runtime.get_telemetry(),
              consecutive_timeouts: runtime.consecutive_timeouts,
            })
          None => ()
        }
      }
    _ => ()
  }
}