///|
/// Deterministic Fabric load-profile fixture for W5B.

///|
pub(all) enum FabricLoadClass {
  ProfinetRtLoad
  BackgroundBestEffortLoad
  AcyclicRecordLoad
  NetloadPlaceholderLoad
} derive(Eq, Debug)

///|
pub fn FabricLoadClass::label(self : FabricLoadClass) -> String {
  match self {
    ProfinetRtLoad => "profinet-rt"
    BackgroundBestEffortLoad => "background-best-effort"
    AcyclicRecordLoad => "acyclic-record"
    NetloadPlaceholderLoad => "netload-placeholder"
  }
}

///|
pub(all) struct FabricBackgroundFlow {
  flow_id : Int
  source : @core.EndpointId
  target : @core.EndpointId
  label : String
  frame_size_octets : Int
  period_ns : Int64
  load_class : FabricLoadClass
} derive(Eq, Debug)

///|
pub fn FabricBackgroundFlow::is_rt(self : FabricBackgroundFlow) -> Bool {
  self.load_class == ProfinetRtLoad
}

///|
pub fn FabricBackgroundFlow::is_background(self : FabricBackgroundFlow) -> Bool {
  self.load_class == BackgroundBestEffortLoad ||
  self.load_class == AcyclicRecordLoad
}

///|
pub(all) struct FabricLoadProfile {
  name : String
  flows : Array[FabricBackgroundFlow]
  netload_semantics : String
  complete_switched_fabric : Bool
  real_netload_evidence : Bool
  art_pi_certification_evidence : Bool
} derive(Eq, Debug)

///|
pub fn FabricLoadProfile::background_load_enabled(
  self : FabricLoadProfile,
) -> Bool {
  self.flows.any(fn(flow) { flow.is_background() })
}

///|
pub fn FabricLoadProfile::declared_fixture_octets(
  self : FabricLoadProfile,
) -> Int {
  let mut total = 0
  for flow in self.flows {
    total += flow.frame_size_octets
  }
  total
}

///|
pub(all) struct FabricLoadProfileRun {
  seed : Int
  profile : FabricLoadProfile
  fabric_event_count : Int
  rt_event_count : Int
  background_event_count : Int
  netload_placeholder_event_count : Int
  trace : @trace.TraceLog
  digest : @core.SimDigest
} derive(Debug)

///|
pub fn FabricLoadProfileRun::passes(self : FabricLoadProfileRun) -> Bool {
  self.profile.background_load_enabled() &&
  self.fabric_event_count > 0 &&
  self.rt_event_count >= 2 &&
  self.background_event_count >= 2 &&
  self.netload_placeholder_event_count == 1 &&
  self.trace.len() ==
  self.fabric_event_count +
  self.rt_event_count +
  self.background_event_count +
  self.netload_placeholder_event_count &&
  !self.profile.complete_switched_fabric &&
  !self.profile.real_netload_evidence &&
  !self.profile.art_pi_certification_evidence
}

///|
pub(all) struct ProfinetRtLoadTraceReport {
  trace_event_count : Int
  replay_count : Int
  same_seed_stable : Bool
  digest : @core.SimDigest
  rt_event_count : Int
  background_event_count : Int
  netload_placeholder_event_count : Int
  complete_switched_fabric : Bool
  real_netload_evidence : Bool
  art_pi_certification_evidence : Bool
} derive(Eq, Debug)

///|
pub fn ProfinetRtLoadTraceReport::passes(
  self : ProfinetRtLoadTraceReport,
) -> Bool {
  self.trace_event_count > 0 &&
  self.replay_count == self.trace_event_count &&
  self.same_seed_stable &&
  self.rt_event_count >= 2 &&
  self.background_event_count >= 2 &&
  self.netload_placeholder_event_count == 1 &&
  !self.complete_switched_fabric &&
  !self.real_netload_evidence &&
  !self.art_pi_certification_evidence
}

///|
pub fn profinet_rt_load_profile(seed? : Int = 601) -> FabricLoadProfile {
  {
    name: "profinet-rt-load-profile-\{normalized_seed(seed)}",
    flows: [
      {
        flow_id: 1,
        source: @core.EndpointId(1),
        target: @core.EndpointId(2),
        label: "profinet.rt.cycle",
        frame_size_octets: 64,
        period_ns: 1_000_000L,
        load_class: ProfinetRtLoad,
      },
      {
        flow_id: 2,
        source: @core.EndpointId(3),
        target: @core.EndpointId(1),
        label: "background.snmp.poll",
        frame_size_octets: 96,
        period_ns: 100_000_000L,
        load_class: BackgroundBestEffortLoad,
      },
      {
        flow_id: 3,
        source: @core.EndpointId(1),
        target: @core.EndpointId(4),
        label: "background.dcp.identify",
        frame_size_octets: 128,
        period_ns: 500_000_000L,
        load_class: AcyclicRecordLoad,
      },
      {
        flow_id: 4,
        source: @core.EndpointId(0),
        target: @core.EndpointId(0),
        label: "fabric.netload.placeholder",
        frame_size_octets: 0,
        period_ns: 0L,
        load_class: NetloadPlaceholderLoad,
      },
    ],
    netload_semantics: "placeholder-only: deterministic background-flow vocabulary, not measured network load",
    complete_switched_fabric: false,
    real_netload_evidence: false,
    art_pi_certification_evidence: false,
  }
}

///|
pub fn run_profinet_rt_load_profile(seed? : Int = 601) -> FabricLoadProfileRun {
  let profile = profinet_rt_load_profile(seed~)
  let baseline = run_fabric_partition_heal(seed~)
  let log = @trace.TraceLog::new()
  let mut event_id = 1
  let mut rt_event_count = 0
  let mut background_event_count = 0
  let mut netload_placeholder_event_count = 0
  for event in baseline.trace.events {
    append_baseline_fabric_event(log, event, event_id~, seed~)
    event_id += 1
  }
  for flow in profile.flows {
    match flow.load_class {
      ProfinetRtLoad => {
        append_load_flow_event(log, event_id~, flow, @trace.Tx, seed~)
        event_id += 1
        append_load_flow_event(log, event_id~, flow, @trace.Rx, seed~)
        event_id += 1
        rt_event_count += 2
      }
      BackgroundBestEffortLoad | AcyclicRecordLoad => {
        append_load_flow_event(log, event_id~, flow, @trace.Probe, seed~)
        event_id += 1
        background_event_count += 1
      }
      NetloadPlaceholderLoad => {
        append_load_flow_event(log, event_id~, flow, @trace.Probe, seed~)
        event_id += 1
        netload_placeholder_event_count += 1
      }
    }
  }
  {
    seed,
    profile,
    fabric_event_count: baseline.trace.len(),
    rt_event_count,
    background_event_count,
    netload_placeholder_event_count,
    trace: log,
    digest: log.portable_digest(seed~),
  }
}

///|
pub fn profinet_rt_load_trace_fixture(seed? : Int = 601) -> @trace.TraceLog {
  run_profinet_rt_load_profile(seed~).trace
}

///|
pub fn profinet_rt_load_profile_report(
  seed? : Int = 601,
) -> ProfinetRtLoadTraceReport {
  let run = run_profinet_rt_load_profile(seed~)
  let same_seed = run.digest == run_profinet_rt_load_profile(seed~).digest
  {
    trace_event_count: run.trace.len(),
    replay_count: replay_count(run.trace),
    same_seed_stable: same_seed,
    digest: run.digest,
    rt_event_count: run.rt_event_count,
    background_event_count: run.background_event_count,
    netload_placeholder_event_count: run.netload_placeholder_event_count,
    complete_switched_fabric: run.profile.complete_switched_fabric,
    real_netload_evidence: run.profile.real_netload_evidence,
    art_pi_certification_evidence: run.profile.art_pi_certification_evidence,
  }
}

///|
fn append_baseline_fabric_event(
  log : @trace.TraceLog,
  event : @trace.TraceEvent,
  event_id~ : Int,
  seed~ : Int,
) -> Unit {
  let direction = match event.direction {
    @trace.Fault => @trace.Probe
    other => other
  }
  log.append(
    @trace.TraceEvent::make(
      event_id~,
      parent_id=event.parent_id,
      vtime=event.vtime,
      clock_domain="sim",
      raw_ns=event.raw_ns,
      node_id="fabric",
      medium_id="fabric-load-min",
      channel_id=event.channel_id,
      direction~,
      payload_digest=event.payload_digest,
      rng_step=event_id,
      seed~,
      backend=@core.SimNative,
      label="fabric.load.baseline." + event.label,
    ),
  )
}

///|
fn append_load_flow_event(
  log : @trace.TraceLog,
  event_id~ : Int,
  flow : FabricBackgroundFlow,
  direction : @trace.TraceDirection,
  seed~ : Int,
) -> Unit {
  let time_ns = 200L + Int64::from_int(event_id) * 10L
  log.append(
    @trace.TraceEvent::make(
      event_id~,
      parent_id=None,
      vtime=@core.VTime::from_ns(time_ns),
      clock_domain="sim",
      raw_ns=time_ns,
      node_id="fabric-load",
      medium_id="fabric-load-min",
      channel_id=Some(@core.ChannelId(flow.flow_id)),
      direction~,
      payload_digest=Some(
        flow.frame_size_octets + flow.load_class.code() * 1000,
      ),
      rng_step=event_id,
      seed~,
      backend=@core.SimNative,
      label="fabric.load." + flow.label + "." + direction.label(),
    ),
  )
}

///|
fn FabricLoadClass::code(self : FabricLoadClass) -> Int {
  match self {
    ProfinetRtLoad => 1
    BackgroundBestEffortLoad => 2
    AcyclicRecordLoad => 3
    NetloadPlaceholderLoad => 4
  }
}

///|
fn replay_count(log : @trace.TraceLog) -> Int {
  let replay = @trace.Replay::from_log(log)
  let mut count = 0
  while !replay.is_exhausted() {
    ignore(replay.next_event())
    count += 1
  }
  count
}