//! Methods on [`super::WorkType`] — naming, parsing, default suggestions,
//! per-variant worker-group sizing, shared-memory requirements, and
//! related helpers. Split from the enum catalogue in
//! [`work_type`](super::work_type) so the impl block can grow without
//! visually drowning the variant definitions.

use std::time::Duration;

use crate::workload::config::{
    AluWidth, FutexLockMode, ReapMode, SchedClass, WakeMechanism, defaults,
};
use crate::workload::schbench::run::SchbenchConfig;
use crate::workload::taobench::run::TaobenchConfig;

use crate::workload::WorkerReport;

use super::{CustomCfg, WorkPhase, WorkType, WorkTypeValidationError, WorkerCtx};

impl WorkType {
    /// PascalCase names for all built-in variants, matching the enum arm names.
    ///
    /// Generated by `strum::VariantNames` at compile time from the
    /// `WorkType` enum definition, so a new variant appears here
    /// automatically. Includes `"Sequence"` and `"Custom"` even though
    /// [`from_name`](Self::from_name) cannot construct them (sequences
    /// require explicit phases; custom requires a function pointer).
    pub const ALL_NAMES: &'static [&'static str] = <Self as strum::VariantNames>::VARIANTS;

    /// PascalCase name of this variant, matching [`ALL_NAMES`](Self::ALL_NAMES).
    /// For [`Custom`](Self::Custom), returns the user-provided `name`
    /// field instead.
    pub fn name(&self) -> &str {
        match self {
            WorkType::SpinWait => "SpinWait",
            WorkType::YieldHeavy => "YieldHeavy",
            WorkType::Mixed => "Mixed",
            WorkType::IoSyncWrite => "IoSyncWrite",
            WorkType::IoRandRead => "IoRandRead",
            WorkType::IoConvoy => "IoConvoy",
            WorkType::Bursty { .. } => "Bursty",
            WorkType::PipeIo { .. } => "PipeIo",
            WorkType::FutexPingPong { .. } => "FutexPingPong",
            WorkType::CachePressure { .. } => "CachePressure",
            WorkType::CacheYield { .. } => "CacheYield",
            WorkType::CachePipe { .. } => "CachePipe",
            WorkType::FutexFanOut { .. } => "FutexFanOut",
            WorkType::Sequence { .. } => "Sequence",
            WorkType::ForkExit => "ForkExit",
            WorkType::NiceSweep => "NiceSweep",
            WorkType::AffinityChurn { .. } => "AffinityChurn",
            WorkType::CrossAffinityChurn { .. } => "CrossAffinityChurn",
            WorkType::PolicyChurn { .. } => "PolicyChurn",
            WorkType::FanOutCompute { .. } => "FanOutCompute",
            WorkType::Schbench { .. } => "Schbench",
            WorkType::Taobench { .. } => "Taobench",
            WorkType::PageFaultChurn { .. } => "PageFaultChurn",
            WorkType::MutexContention { .. } => "MutexContention",
            WorkType::Custom { name, .. } => name.as_str(),
            WorkType::ThunderingHerd { .. } => "ThunderingHerd",
            WorkType::PriorityInversion { .. } => "PriorityInversion",
            WorkType::ProducerConsumerImbalance { .. } => "ProducerConsumerImbalance",
            WorkType::RtStarvation { .. } => "RtStarvation",
            WorkType::AsymmetricWaker { .. } => "AsymmetricWaker",
            WorkType::WakeChain { .. } => "WakeChain",
            WorkType::NumaWorkingSetSweep { .. } => "NumaWorkingSetSweep",
            WorkType::CgroupChurn { .. } => "CgroupChurn",
            WorkType::CgroupAttachStorm { .. } => "CgroupAttachStorm",
            WorkType::SignalStorm { .. } => "SignalStorm",
            WorkType::PreemptStorm { .. } => "PreemptStorm",
            WorkType::EpollStorm { .. } => "EpollStorm",
            WorkType::NumaMigrationChurn { .. } => "NumaMigrationChurn",
            WorkType::IdleChurn { .. } => "IdleChurn",
            WorkType::AluHot { .. } => "AluHot",
            WorkType::SmtSiblingSpin => "SmtSiblingSpin",
            WorkType::IpcVariance { .. } => "IpcVariance",
            WorkType::TimerLatency { .. } => "TimerLatency",
            WorkType::NetTraffic { .. } => "NetTraffic",
            WorkType::IrqWake { .. } => "IrqWake",
        }
    }

    /// Look up a variant by PascalCase name and return it with default
    /// parameters. Returns `None` for unknown names, `"Sequence"`
    /// (requires explicit phases), and `"Custom"` (requires a function
    /// pointer).
    pub fn from_name(s: &str) -> Option<WorkType> {
        match s {
            "SpinWait" => Some(WorkType::SpinWait),
            "YieldHeavy" => Some(WorkType::YieldHeavy),
            "Mixed" => Some(WorkType::Mixed),
            "IoSyncWrite" => Some(WorkType::IoSyncWrite),
            "IoRandRead" => Some(WorkType::IoRandRead),
            "IoConvoy" => Some(WorkType::IoConvoy),
            "Bursty" => Some(WorkType::Bursty {
                burst_duration: defaults::BURSTY_BURST_DURATION,
                sleep_duration: defaults::BURSTY_SLEEP_DURATION,
            }),
            "PipeIo" => Some(WorkType::PipeIo {
                burst_iters: defaults::PIPE_IO_BURST_ITERS,
            }),
            "FutexPingPong" => Some(WorkType::FutexPingPong {
                spin_iters: defaults::FUTEX_PING_PONG_SPIN_ITERS,
            }),
            "CachePressure" => Some(WorkType::CachePressure {
                size_kib: defaults::CACHE_PRESSURE_SIZE_KIB,
                stride: defaults::CACHE_PRESSURE_STRIDE,
            }),
            "CacheYield" => Some(WorkType::CacheYield {
                size_kib: defaults::CACHE_YIELD_SIZE_KIB,
                stride: defaults::CACHE_YIELD_STRIDE,
            }),
            "CachePipe" => Some(WorkType::CachePipe {
                size_kib: defaults::CACHE_PIPE_SIZE_KIB,
                burst_iters: defaults::CACHE_PIPE_BURST_ITERS,
            }),
            "FutexFanOut" => Some(WorkType::FutexFanOut {
                fan_out: defaults::FUTEX_FAN_OUT_FAN_OUT,
                spin_iters: defaults::FUTEX_FAN_OUT_SPIN_ITERS,
            }),
            "ForkExit" => Some(WorkType::ForkExit),
            "NiceSweep" => Some(WorkType::NiceSweep),
            "AffinityChurn" => Some(WorkType::AffinityChurn {
                spin_iters: defaults::AFFINITY_CHURN_SPIN_ITERS,
            }),
            "CrossAffinityChurn" => Some(WorkType::CrossAffinityChurn {
                spin_iters: defaults::CROSS_AFFINITY_CHURN_SPIN_ITERS,
            }),
            "PolicyChurn" => Some(WorkType::PolicyChurn {
                spin_iters: defaults::POLICY_CHURN_SPIN_ITERS,
            }),
            "FanOutCompute" => Some(WorkType::FanOutCompute {
                fan_out: defaults::FAN_OUT_COMPUTE_FAN_OUT,
                cache_footprint_kib: defaults::FAN_OUT_COMPUTE_CACHE_FOOTPRINT_KIB,
                operations: defaults::FAN_OUT_COMPUTE_OPERATIONS,
                sleep_usec: defaults::FAN_OUT_COMPUTE_SLEEP_USEC,
            }),
            "Schbench" => Some(WorkType::Schbench {
                config: SchbenchConfig::default(),
            }),
            "Taobench" => Some(WorkType::Taobench {
                config: TaobenchConfig::default(),
            }),
            "PageFaultChurn" => Some(WorkType::PageFaultChurn {
                region_kib: defaults::PAGE_FAULT_CHURN_REGION_KIB,
                touches_per_cycle: defaults::PAGE_FAULT_CHURN_TOUCHES_PER_CYCLE,
                spin_iters: defaults::PAGE_FAULT_CHURN_SPIN_ITERS,
            }),
            "MutexContention" => Some(WorkType::MutexContention {
                contenders: defaults::MUTEX_CONTENTION_CONTENDERS,
                hold_iters: defaults::MUTEX_CONTENTION_HOLD_ITERS,
                work_iters: defaults::MUTEX_CONTENTION_WORK_ITERS,
            }),
            "ThunderingHerd" => Some(WorkType::ThunderingHerd {
                waiters: defaults::THUNDERING_HERD_WAITERS,
                batches: defaults::THUNDERING_HERD_BATCHES,
                inter_batch_ms: defaults::THUNDERING_HERD_INTER_BATCH_MS,
            }),
            "PriorityInversion" => Some(WorkType::PriorityInversion {
                high_count: defaults::PRIORITY_INVERSION_HIGH_COUNT,
                medium_count: defaults::PRIORITY_INVERSION_MEDIUM_COUNT,
                low_count: defaults::PRIORITY_INVERSION_LOW_COUNT,
                hold_iters: defaults::PRIORITY_INVERSION_HOLD_ITERS,
                work_iters: defaults::PRIORITY_INVERSION_WORK_ITERS,
                pi_mode: defaults::PRIORITY_INVERSION_PI_MODE,
            }),
            "ProducerConsumerImbalance" => Some(WorkType::ProducerConsumerImbalance {
                producers: defaults::PRODUCER_CONSUMER_PRODUCERS,
                consumers: defaults::PRODUCER_CONSUMER_CONSUMERS,
                produce_rate_hz: defaults::PRODUCER_CONSUMER_PRODUCE_RATE_HZ,
                consume_iters: defaults::PRODUCER_CONSUMER_CONSUME_ITERS,
                queue_depth_target: defaults::PRODUCER_CONSUMER_QUEUE_DEPTH_TARGET,
            }),
            "RtStarvation" => Some(WorkType::RtStarvation {
                rt_workers: defaults::RT_STARVATION_RT_WORKERS,
                cfs_workers: defaults::RT_STARVATION_CFS_WORKERS,
                rt_priority: defaults::RT_STARVATION_RT_PRIORITY,
                burst_iters: defaults::RT_STARVATION_BURST_ITERS,
            }),
            "AsymmetricWaker" => Some(WorkType::AsymmetricWaker {
                waker_class: SchedClass::default(),
                wakee_class: SchedClass::default(),
                burst_iters: defaults::ASYMMETRIC_WAKER_BURST_ITERS,
            }),
            "WakeChain" => Some(WorkType::WakeChain {
                depth: defaults::WAKE_CHAIN_DEPTH,
                wake: defaults::WAKE_CHAIN_WAKE,
                work_per_hop: defaults::WAKE_CHAIN_WORK_PER_HOP,
            }),
            "NumaWorkingSetSweep" => Some(WorkType::NumaWorkingSetSweep {
                region_kib: defaults::NUMA_WORKING_SET_SWEEP_REGION_KIB,
                sweep_period_ms: defaults::NUMA_WORKING_SET_SWEEP_SWEEP_PERIOD_MS,
                // Empty list — single-node default leaves binding
                // disabled, matching `node_set()` defaults from
                // `MemPolicy::Default`. Users opt-in with a node
                // list via the constructor.
                target_nodes: Vec::new(),
            }),
            "CgroupChurn" => Some(WorkType::CgroupChurn {
                groups: defaults::CGROUP_CHURN_GROUPS,
                cycle_ms: defaults::CGROUP_CHURN_CYCLE_MS,
            }),
            "CgroupAttachStorm" => Some(WorkType::CgroupAttachStorm {
                dest: defaults::CGROUP_ATTACH_STORM_DEST.into(),
                reap: ReapMode::default(),
            }),
            "SignalStorm" => Some(WorkType::SignalStorm {
                signals_per_iter: defaults::SIGNAL_STORM_SIGNALS_PER_ITER,
                work_iters: defaults::SIGNAL_STORM_WORK_ITERS,
            }),
            "PreemptStorm" => Some(WorkType::PreemptStorm {
                cfs_workers: defaults::PREEMPT_STORM_CFS_WORKERS,
                rt_burst_iters: defaults::PREEMPT_STORM_RT_BURST_ITERS,
                rt_sleep_us: defaults::PREEMPT_STORM_RT_SLEEP_US,
            }),
            "EpollStorm" => Some(WorkType::EpollStorm {
                producers: defaults::EPOLL_STORM_PRODUCERS,
                consumers: defaults::EPOLL_STORM_CONSUMERS,
                events_per_burst: defaults::EPOLL_STORM_EVENTS_PER_BURST,
            }),
            "NumaMigrationChurn" => Some(WorkType::NumaMigrationChurn {
                period_ms: defaults::NUMA_MIGRATION_CHURN_PERIOD_MS,
            }),
            "IdleChurn" => Some(WorkType::IdleChurn {
                burst_duration: defaults::IDLE_CHURN_BURST_DURATION,
                sleep_duration: defaults::IDLE_CHURN_SLEEP_DURATION,
                precise_timing: defaults::IDLE_CHURN_PRECISE_TIMING,
            }),
            "AluHot" => Some(WorkType::AluHot {
                width: defaults::ALU_HOT_WIDTH,
            }),
            "SmtSiblingSpin" => Some(WorkType::SmtSiblingSpin),
            "IpcVariance" => Some(WorkType::IpcVariance {
                hot_iters: defaults::IPC_VARIANCE_HOT_ITERS,
                cold_iters: defaults::IPC_VARIANCE_COLD_ITERS,
                period_iters: defaults::IPC_VARIANCE_PERIOD_ITERS,
            }),
            "TimerLatency" => Some(WorkType::TimerLatency {
                interval_us: defaults::TIMER_LATENCY_INTERVAL_US,
            }),
            "NetTraffic" => Some(WorkType::NetTraffic {
                interval_us: defaults::NET_TRAFFIC_INTERVAL_US,
                frame_bytes: defaults::NET_TRAFFIC_FRAME_BYTES,
            }),
            "IrqWake" => Some(WorkType::IrqWake {
                interval_us: defaults::IRQ_WAKE_INTERVAL_US,
                frame_bytes: defaults::IRQ_WAKE_FRAME_BYTES,
            }),
            // Sequence requires explicit phases; no default from_name.
            _ => None,
        }
    }

    /// Case-insensitive lookup that returns the canonical PascalCase
    /// entry from [`ALL_NAMES`](Self::ALL_NAMES) matching the input,
    /// or `None` when no entry matches.
    ///
    /// Distinct from [`from_name`](Self::from_name) in two ways:
    ///
    /// 1. It matches case-insensitively, so `"spinwait"` / `"SPINWAIT"`
    ///    / `"SpinWait"` all map to the same canonical `"SpinWait"`.
    /// 2. It returns the name string rather than a default-parameter
    ///    [`WorkType`] value, so callers can quote the canonical
    ///    spelling in error messages without also instantiating the
    ///    variant.
    ///
    /// Intended as a CLI / config-parser helper: when `from_name`
    /// returns `None` for the user's input, pass the same string
    /// here to recover the canonical spelling (if any) for a
    /// friendlier "did you mean `SpinWait`?" diagnostic. Includes
    /// `"Sequence"` and `"Custom"` in the match space even though
    /// `from_name` refuses to construct them — the point of
    /// [`suggest`](Self::suggest) is naming, not construction.
    ///
    /// Whitespace handling: the match uses `eq_ignore_ascii_case`
    /// without trimming, so surrounding whitespace in `s`
    /// (`" SpinWait"`, `"SpinWait\n"`) suppresses a match. Callers
    /// that accept user input with possible surrounding whitespace
    /// must `s.trim()` before calling — the same convention
    /// `from_name` follows. Keeping the predicate strict here
    /// avoids confusing "suggested canonical spelling" reports for
    /// inputs that were already nearly correct save for stray
    /// whitespace the caller should have already normalized.
    pub fn suggest(s: &str) -> Option<&'static str> {
        Self::ALL_NAMES
            .iter()
            .copied()
            .find(|n| n.eq_ignore_ascii_case(s))
    }

    /// Worker group size for this work type, or None if ungrouped.
    ///
    /// `num_workers` must be divisible by this value. Paired types return 2,
    /// fan-out returns fan_out + 1 (1 messenger + N receivers), and
    /// MutexContention returns `contenders`.
    pub fn worker_group_size(&self) -> Option<usize> {
        match self {
            WorkType::PipeIo { .. }
            | WorkType::FutexPingPong { .. }
            | WorkType::CachePipe { .. } => Some(2),
            WorkType::FutexFanOut { fan_out, .. } => Some(fan_out + 1),
            WorkType::FanOutCompute { fan_out, .. } => Some(fan_out + 1),
            WorkType::MutexContention { contenders, .. } => Some(*contenders),
            // ThunderingHerd uses a single global futex shared by
            // every worker — group size must equal num_workers so
            // the per-group futex allocator yields exactly one
            // shared region for the whole herd.
            WorkType::ThunderingHerd { waiters, .. } => Some(waiters + 1),
            // PriorityInversion: all 3 tiers share the same futex
            // word, so the group covers every worker.
            WorkType::PriorityInversion {
                high_count,
                medium_count,
                low_count,
                ..
            } => Some(high_count + medium_count + low_count),
            // ProducerConsumerImbalance: producers + consumers
            // share the queue mmap.
            WorkType::ProducerConsumerImbalance {
                producers,
                consumers,
                ..
            } => Some(producers + consumers),
            // RtStarvation: rt + cfs workers share the same
            // affinity-constrained scheduling domain.
            WorkType::RtStarvation {
                rt_workers,
                cfs_workers,
                ..
            } => Some(rt_workers + cfs_workers),
            // AsymmetricWaker: paired waker + wakee (group of 2),
            // matching FutexPingPong's shape.
            WorkType::AsymmetricWaker { .. } => Some(2),
            // WakeChain: each chain has `depth` workers. Group
            // size is the per-chain size; num_workers must be a
            // positive multiple of depth, and the spawn-side
            // derives the parallel-chain count from
            // `num_workers / depth`.
            WorkType::WakeChain { depth, .. } => Some(*depth),
            // SignalStorm: paired (waker / wakee), num_workers
            // must be even. Each pair shares the partner-tid
            // exchange region.
            WorkType::SignalStorm { .. } => Some(2),
            // PreemptStorm: 1 RT worker + cfs_workers CFS spinners
            // share the same affinity-constrained scheduling
            // domain so the RT preempts on the same CPU.
            WorkType::PreemptStorm { cfs_workers, .. } => Some(cfs_workers + 1),
            // EpollStorm: producers + consumers share the eventfd
            // / epoll fd handoff. One group per (producers,
            // consumers) tuple.
            WorkType::EpollStorm {
                producers,
                consumers,
                ..
            } => Some(producers + consumers),
            // SmtSiblingSpin: paired workers intended to be
            // pinned to the two SMT siblings of one physical
            // core. The variant doesn't allocate shared memory;
            // the group of 2 is what binds the AffinityIntent
            // resolution to a sibling pair. Pair via
            // [`AffinityIntent::SmtSiblingPair`] (auto-resolved
            // from host topology) or [`AffinityIntent::Exact`]
            // (caller-supplied CPU IDs).
            WorkType::SmtSiblingSpin => Some(2),
            // IrqWake: paired sender / receiver (group of 2, even count). pos==0
            // sends self-addressed frames, pos==1 blocks in recvfrom and records a
            // liveness sample (block-to-return duration) per delivered frame.
            WorkType::IrqWake { .. } => Some(2),
            _ => None,
        }
    }

    /// Whether this work type needs a pre-fork shared memory region (MAP_SHARED mmap).
    ///
    /// `RtStarvation` opts in even though its body never reads or
    /// writes the futex word: the spawn-side `(futex_ptr, pos)`
    /// tuple is the only mechanism that hands the worker its
    /// per-position index, which `RtStarvation` consumes to
    /// classify itself as RT or CFS. Allocating a single 4-byte
    /// MAP_SHARED region per group is the cheapest way to get
    /// `pos` plumbed through worker_main without a wider dispatch
    /// contract change. `IrqWake` opts in for the same reason:
    /// `pos == 0` is the frame sender, `pos == 1` the receiver that
    /// blocks in `recvfrom` — neither touches the futex word.
    pub fn needs_shared_mem(&self) -> bool {
        matches!(
            self,
            WorkType::FutexPingPong { .. }
                | WorkType::FutexFanOut { .. }
                | WorkType::FanOutCompute { .. }
                | WorkType::MutexContention { .. }
                | WorkType::ThunderingHerd { .. }
                | WorkType::PriorityInversion { .. }
                | WorkType::ProducerConsumerImbalance { .. }
                | WorkType::AsymmetricWaker { .. }
                | WorkType::WakeChain { .. }
                | WorkType::RtStarvation { .. }
                | WorkType::SignalStorm { .. }
                | WorkType::PreemptStorm { .. }
                | WorkType::EpollStorm { .. }
                | WorkType::IrqWake { .. }
        )
    }

    /// Number of pipes per chain that the spawn-side must allocate
    /// for this work type, or `None` when no per-stage pipe ring is
    /// needed. The returned `depth` matches the variant's `depth`
    /// field for `WakeChain { wake: WakeMechanism::Pipe, .. }`;
    /// every other variant (and `WakeChain` with
    /// `wake: WakeMechanism::Futex`) returns `None`.
    ///
    /// When this returns `Some(depth)`, the spawn-side allocates
    /// `depth` pipes per chain so stage `i` holds
    /// `pipe[i].write_end` (to wake stage `i + 1`) and
    /// `pipe[(i + depth - 1) % depth].read_end` (predecessor's
    /// wake). `WakeMechanism::Futex` keeps the existing futex-word
    /// ring and returns `None`.
    pub fn chain_pipe_depth(&self) -> Option<usize> {
        match self {
            WorkType::WakeChain {
                wake: WakeMechanism::Pipe,
                depth,
                ..
            } => Some(*depth),
            _ => None,
        }
    }

    /// Whether this work type allocates a per-worker cache buffer post-fork.
    pub fn needs_cache_buf(&self) -> bool {
        matches!(
            self,
            WorkType::CachePressure { .. }
                | WorkType::CacheYield { .. }
                | WorkType::CachePipe { .. }
                | WorkType::FanOutCompute { .. }
        )
    }

    /// Bursty work: CPU burst for `burst_duration`, sleep for
    /// `sleep_duration`, repeat.
    ///
    /// Validation fires at spawn time, not construction time; see
    /// [`WorkType::Bursty`] variant doc for preconditions.
    pub const fn bursty(burst_duration: Duration, sleep_duration: Duration) -> Self {
        WorkType::Bursty {
            burst_duration,
            sleep_duration,
        }
    }

    /// Paired pipe I/O with CPU burst between exchanges.
    ///
    /// Validation fires at spawn time, not construction time; see
    /// [`WorkType::PipeIo`] variant doc for preconditions.
    pub const fn pipe_io(burst_iters: u64) -> Self {
        WorkType::PipeIo { burst_iters }
    }

    /// Paired futex ping-pong with CPU spin between wakes.
    ///
    /// Validation fires at spawn time, not construction time; see
    /// [`WorkType::FutexPingPong`] variant doc for preconditions.
    pub const fn futex_ping_pong(spin_iters: u64) -> Self {
        WorkType::FutexPingPong { spin_iters }
    }

    /// Strided read-modify-write over a `size_kib` KiB buffer.
    ///
    /// Validation fires at spawn time, not construction time; see
    /// [`WorkType::CachePressure`] variant doc for preconditions.
    pub const fn cache_pressure(size_kib: usize, stride: usize) -> Self {
        WorkType::CachePressure { size_kib, stride }
    }

    /// Cache pressure burst followed by sched_yield().
    ///
    /// Validation fires at spawn time, not construction time; see
    /// [`WorkType::CacheYield`] variant doc for preconditions.
    pub const fn cache_yield(size_kib: usize, stride: usize) -> Self {
        WorkType::CacheYield { size_kib, stride }
    }

    /// Cache pressure burst then pipe exchange with a partner worker.
    ///
    /// Validation fires at spawn time, not construction time; see
    /// [`WorkType::CachePipe`] variant doc for preconditions.
    pub const fn cache_pipe(size_kib: usize, burst_iters: u64) -> Self {
        WorkType::CachePipe {
            size_kib,
            burst_iters,
        }
    }

    /// 1:N fan-out wake pattern with CPU spin between wakes.
    ///
    /// Validation fires at spawn time, not construction time; see
    /// [`WorkType::FutexFanOut`] variant doc for preconditions.
    pub const fn futex_fan_out(fan_out: usize, spin_iters: u64) -> Self {
        WorkType::FutexFanOut {
            fan_out,
            spin_iters,
        }
    }

    /// Rapid self-directed affinity changes with `spin_iters` CPU work between.
    ///
    /// Validation fires at spawn time, not construction time; see
    /// [`WorkType::AffinityChurn`] variant doc for preconditions.
    pub const fn affinity_churn(spin_iters: u64) -> Self {
        WorkType::AffinityChurn { spin_iters }
    }

    /// Rapid cross-task affinity churn: each worker rewrites its
    /// siblings' affinity. Must run in a dedicated cgroup; see the
    /// [`WorkType::CrossAffinityChurn`] variant doc for preconditions.
    pub const fn cross_affinity_churn(spin_iters: u64) -> Self {
        WorkType::CrossAffinityChurn { spin_iters }
    }

    /// Cycle scheduling policies with `spin_iters` CPU work between switches.
    ///
    /// Validation fires at spawn time, not construction time; see
    /// [`WorkType::PolicyChurn`] variant doc for preconditions.
    pub const fn policy_churn(spin_iters: u64) -> Self {
        WorkType::PolicyChurn { spin_iters }
    }

    /// Messenger/worker fan-out with compute work using the given parameters.
    ///
    /// `fan_out` is passed to `futex_wake(ptr, N)` where `N: i32` is
    /// the number of waiters to wake. Realistic values are tens of
    /// workers; sched-test topologies that need more than `i32::MAX`
    /// (~2.1B) receivers per messenger are not expressible.
    /// The worker's `futex_wake` call site clamps the cast via
    /// `clamp_futex_wake_n` (`worker/mod.rs`) so a pathological
    /// `usize` input wakes at most `i32::MAX` waiters instead of
    /// wrapping to a negative N (FUTEX_WAKE broadcasts when passed a
    /// negative N on some kernels, which would wake every waiter on
    /// the futex rather than just this messenger's receivers).
    ///
    /// Validation fires at spawn time, not construction time; see
    /// [`WorkType::FanOutCompute`] variant doc for preconditions.
    pub const fn fan_out_compute(
        fan_out: usize,
        cache_footprint_kib: usize,
        operations: usize,
        sleep_usec: u64,
    ) -> Self {
        WorkType::FanOutCompute {
            fan_out,
            cache_footprint_kib,
            operations,
            sleep_usec,
        }
    }

    /// The `schbench_rs` workload (schbench's default mode), configured by
    /// `config`. Build the config with [`SchbenchConfig::default`] plus its
    /// chainable setters, e.g.
    /// `WorkType::schbench(SchbenchConfig::default().message_threads(2))`. Use
    /// with a single ktstr worker; see the [`WorkType::Schbench`] variant doc.
    pub const fn schbench(config: SchbenchConfig) -> Self {
        WorkType::Schbench { config }
    }

    /// The `taobench_rs` workload (a bounded, evicting key-value cache),
    /// configured by `config`. Build the config with [`TaobenchConfig::default`]
    /// plus its chainable setters, e.g.
    /// `WorkType::taobench(TaobenchConfig::default().target_hit_pct(95))`. Use
    /// with a single ktstr worker; see the [`WorkType::Taobench`] variant doc.
    pub const fn taobench(config: TaobenchConfig) -> Self {
        WorkType::Taobench { config }
    }

    /// Rapid page fault cycling with `spin_iters` CPU work between cycles.
    ///
    /// Validation fires at spawn time, not construction time; see
    /// [`WorkType::PageFaultChurn`] variant doc for preconditions.
    pub const fn page_fault_churn(
        region_kib: usize,
        touches_per_cycle: usize,
        spin_iters: u64,
    ) -> Self {
        WorkType::PageFaultChurn {
            region_kib,
            touches_per_cycle,
            spin_iters,
        }
    }

    /// N-way futex mutex contention with `contenders` workers per group.
    ///
    /// Validation fires at spawn time, not construction time; see
    /// [`WorkType::MutexContention`] variant doc for preconditions.
    pub const fn mutex_contention(contenders: usize, hold_iters: u64, work_iters: u64) -> Self {
        WorkType::MutexContention {
            contenders,
            hold_iters,
            work_iters,
        }
    }

    /// One waker, N waiters on a single global futex; broadcasts via
    /// `FUTEX_WAKE` per batch. Pairs with
    /// [`WorkType::ThunderingHerd`].
    ///
    /// Validation fires at spawn time, not construction time; see
    /// [`WorkType::ThunderingHerd`] variant doc for preconditions.
    pub const fn thundering_herd(waiters: usize, batches: u64, inter_batch_ms: u64) -> Self {
        WorkType::ThunderingHerd {
            waiters,
            batches,
            inter_batch_ms,
        }
    }

    /// Three priority tiers contending for one shared lock. See
    /// [`WorkType::PriorityInversion`] for behavior; pass
    /// [`FutexLockMode::Pi`] to invoke `FUTEX_LOCK_PI` or
    /// [`FutexLockMode::Plain`] for a non-PI futex.
    ///
    /// Validation fires at spawn time, not construction time; see
    /// [`WorkType::PriorityInversion`] variant doc for preconditions.
    pub const fn priority_inversion(
        high_count: usize,
        medium_count: usize,
        low_count: usize,
        hold_iters: u64,
        work_iters: u64,
        pi_mode: FutexLockMode,
    ) -> Self {
        WorkType::PriorityInversion {
            high_count,
            medium_count,
            low_count,
            hold_iters,
            work_iters,
            pi_mode,
        }
    }

    /// Producer/consumer pipeline with deliberately unbalanced
    /// rates. See [`WorkType::ProducerConsumerImbalance`].
    ///
    /// Validation fires at spawn time, not construction time; see
    /// [`WorkType::ProducerConsumerImbalance`] variant doc for preconditions.
    pub const fn producer_consumer_imbalance(
        producers: usize,
        consumers: usize,
        produce_rate_hz: u64,
        consume_iters: u64,
        queue_depth_target: u64,
    ) -> Self {
        WorkType::ProducerConsumerImbalance {
            producers,
            consumers,
            produce_rate_hz,
            consume_iters,
            queue_depth_target,
        }
    }

    /// `rt_workers` SCHED_FIFO workers vs. `cfs_workers` SCHED_NORMAL
    /// workers competing on the same CPU set. See
    /// [`WorkType::RtStarvation`].
    ///
    /// Validation fires at spawn time, not construction time; see
    /// [`WorkType::RtStarvation`] variant doc for preconditions.
    pub const fn rt_starvation(
        rt_workers: usize,
        cfs_workers: usize,
        rt_priority: i32,
        burst_iters: u64,
    ) -> Self {
        WorkType::RtStarvation {
            rt_workers,
            cfs_workers,
            rt_priority,
            burst_iters,
        }
    }

    /// Paired workers in mismatched scheduling classes. See
    /// [`WorkType::AsymmetricWaker`].
    ///
    /// Validation fires at spawn time, not construction time; see
    /// [`WorkType::AsymmetricWaker`] variant doc for preconditions.
    pub const fn asymmetric_waker(
        waker_class: SchedClass,
        wakee_class: SchedClass,
        burst_iters: u64,
    ) -> Self {
        WorkType::AsymmetricWaker {
            waker_class,
            wakee_class,
            burst_iters,
        }
    }

    /// Pipeline of waker-wakee hops with optional `WF_SYNC`. See
    /// [`WorkType::WakeChain`].
    ///
    /// Validation fires at spawn time, not construction time; see
    /// [`WorkType::WakeChain`] variant doc for preconditions
    /// (`depth >= 2`, `num_workers` divisible by `depth`, etc.).
    pub const fn wake_chain(depth: usize, wake: WakeMechanism, work_per_hop: Duration) -> Self {
        WorkType::WakeChain {
            depth,
            wake,
            work_per_hop,
        }
    }

    /// NUMA working-set sweep with periodic `mbind` rotation. See
    /// [`WorkType::NumaWorkingSetSweep`]. `target_nodes` accepts
    /// any `IntoIterator<Item = usize>` for ergonomic call sites
    /// (`[0, 1, 2]`, `0..node_count`, `BTreeSet`, etc.).
    ///
    /// Validation fires at spawn time, not construction time; see
    /// [`WorkType::NumaWorkingSetSweep`] variant doc for preconditions.
    pub fn numa_working_set_sweep(
        region_kib: usize,
        sweep_period_ms: u64,
        target_nodes: impl IntoIterator<Item = usize>,
    ) -> Self {
        WorkType::NumaWorkingSetSweep {
            region_kib,
            sweep_period_ms,
            target_nodes: target_nodes.into_iter().collect(),
        }
    }

    /// Construct a [`WorkType::Sequence`] from a head phase and an
    /// iterator of follow-on phases.
    ///
    /// The `Sequence` variant cannot use [`from_name`](Self::from_name)
    /// because phases require explicit construction; this constructor
    /// is the only typed entry point. Accepts any `IntoIterator<Item =
    /// WorkPhase>` for `rest` so callers can pass arrays, `Vec`, or
    /// builder-style chains.
    ///
    /// Validation fires at spawn time, not construction time; see
    /// [`WorkType::Sequence`] variant doc for preconditions.
    pub fn sequence(first: WorkPhase, rest: impl IntoIterator<Item = WorkPhase>) -> Self {
        WorkType::Sequence {
            first,
            rest: rest.into_iter().collect(),
        }
    }

    /// Construct a [`WorkType::CgroupChurn`].
    ///
    /// Validation fires at spawn time, not construction time; see
    /// [`WorkType::CgroupChurn`] variant doc for preconditions.
    pub const fn cgroup_churn(groups: usize, cycle_ms: u64) -> Self {
        WorkType::CgroupChurn { groups, cycle_ms }
    }

    /// Construct a [`WorkType::CgroupAttachStorm`].
    ///
    /// `dest` is the sibling cgroup name each forked child is migrated
    /// into; it must already exist at run time (e.g. created via
    /// [`Op::add_cgroup`](crate::scenario::ops::Op::add_cgroup)). Non-`const`
    /// because `Into::<String>::into` allocates (mirrors
    /// [`custom`](Self::custom)). Validation fires at spawn time, not
    /// construction time; see the [`WorkType::CgroupAttachStorm`] variant
    /// doc for preconditions.
    pub fn cgroup_attach_storm(dest: impl Into<String>, reap: ReapMode) -> Self {
        WorkType::CgroupAttachStorm {
            dest: dest.into(),
            reap,
        }
    }

    /// Construct a [`WorkType::SignalStorm`].
    ///
    /// Validation fires at spawn time, not construction time; see
    /// [`WorkType::SignalStorm`] variant doc for preconditions.
    pub const fn signal_storm(signals_per_iter: u64, work_iters: u64) -> Self {
        WorkType::SignalStorm {
            signals_per_iter,
            work_iters,
        }
    }

    /// Construct a [`WorkType::PreemptStorm`].
    ///
    /// Validation fires at spawn time, not construction time; see
    /// [`WorkType::PreemptStorm`] variant doc for preconditions.
    pub const fn preempt_storm(cfs_workers: usize, rt_burst_iters: u64, rt_sleep_us: u64) -> Self {
        WorkType::PreemptStorm {
            cfs_workers,
            rt_burst_iters,
            rt_sleep_us,
        }
    }

    /// Construct a [`WorkType::EpollStorm`].
    ///
    /// Validation fires at spawn time, not construction time; see
    /// [`WorkType::EpollStorm`] variant doc for preconditions.
    pub const fn epoll_storm(producers: usize, consumers: usize, events_per_burst: u64) -> Self {
        WorkType::EpollStorm {
            producers,
            consumers,
            events_per_burst,
        }
    }

    /// Construct a [`WorkType::NumaMigrationChurn`].
    ///
    /// Validation fires at spawn time, not construction time; see
    /// [`WorkType::NumaMigrationChurn`] variant doc for preconditions.
    pub const fn numa_migration_churn(period_ms: u64) -> Self {
        WorkType::NumaMigrationChurn { period_ms }
    }

    /// Construct a [`WorkType::IdleChurn`] with the default
    /// `precise_timing = false`.
    ///
    /// # Spawn-time precondition
    ///
    /// `burst_duration` and `sleep_duration` must both be
    /// strictly greater than `Duration::ZERO`. The constructor
    /// itself accepts any value (no early validation); the
    /// rejection fires at `WorkloadHandle::spawn` time with an
    /// actionable bail message naming the offending field. See
    /// [`WorkType::IdleChurn`] variant doc for the rationale and
    /// the kernel-source citation.
    ///
    /// # `precise_timing`
    ///
    /// This constructor sets `precise_timing` to
    /// [`defaults::IDLE_CHURN_PRECISE_TIMING`] (`false`),
    /// preserving the inherited `current->timer_slack_ns`
    /// (~50µs default). To opt into 1ns timer slack, build the
    /// variant directly via the struct-literal form:
    /// `WorkType::IdleChurn { burst_duration, sleep_duration,
    /// precise_timing: true }`. See the variant's
    /// `precise_timing` field doc for the kernel-side
    /// mechanism.
    pub const fn idle_churn(burst_duration: Duration, sleep_duration: Duration) -> Self {
        WorkType::IdleChurn {
            burst_duration,
            sleep_duration,
            precise_timing: defaults::IDLE_CHURN_PRECISE_TIMING,
        }
    }

    /// Construct a [`WorkType::AluHot`] at the given execution
    /// width.
    ///
    /// `AluWidth::Widest` resolves to the widest data-path the
    /// host supports at worker entry. See [`AluWidth`] for the
    /// per-variant data-path width and the runtime resolution
    /// rules.
    ///
    /// Validation fires at spawn time, not construction time;
    /// see [`WorkType::AluHot`] variant doc for preconditions.
    pub const fn alu_hot(width: AluWidth) -> Self {
        WorkType::AluHot { width }
    }

    /// Construct a [`WorkType::TimerLatency`] with an inter-cycle interval
    /// (`interval_us`, the `cyclictest` `-i` analogue). `interval_us == 0` is
    /// rejected at spawn (`validate_workload_admission`); pass a non-zero
    /// period. See the [`WorkType::TimerLatency`] variant doc.
    pub const fn timer_latency(interval_us: u64) -> Self {
        WorkType::TimerLatency { interval_us }
    }

    /// Construct a [`WorkType::NetTraffic`] with an inter-frame interval
    /// (`interval_us`; `0` = continuous burst) and Ethernet `frame_bytes`.
    /// `frame_bytes` is validated to `[60, 1514]` at spawn, not here; see the
    /// [`WorkType::NetTraffic`] variant doc.
    pub const fn net_traffic(interval_us: u64, frame_bytes: u16) -> Self {
        WorkType::NetTraffic {
            interval_us,
            frame_bytes,
        }
    }

    /// Construct a [`WorkType::IrqWake`] — a paired sender/receiver where the
    /// receiver blocks in `recvfrom` and is woken from NET_RX softirq context.
    /// `interval_us` paces the sender (default 1000 µs gives the receiver a clean
    /// empty-queue block per frame; `0` maximizes softirq load into `ksoftirqd`
    /// but degenerates the wake reservoir); `frame_bytes` is validated to
    /// `[60, 1514]` at spawn. Spawn an even worker count (group size 2). See the
    /// [`WorkType::IrqWake`] variant doc.
    pub const fn irq_wake(interval_us: u64, frame_bytes: u16) -> Self {
        WorkType::IrqWake {
            interval_us,
            frame_bytes,
        }
    }

    /// Construct a [`WorkType::IpcVariance`] with explicit hot,
    /// cold, and period iteration counts.
    ///
    /// Returns [`WorkTypeValidationError::ZeroIpcVarianceParam`]
    /// when any of `hot_iters`, `cold_iters`, or `period_iters`
    /// is `0`. Construction-time validation matches the
    /// spawn-time check so callers get immediate feedback at
    /// the call site rather than discovering the rejection
    /// only at `WorkloadHandle::spawn` time.
    pub const fn ipc_variance(
        hot_iters: u64,
        cold_iters: u64,
        period_iters: u64,
    ) -> std::result::Result<Self, WorkTypeValidationError> {
        if hot_iters == 0 {
            return Err(WorkTypeValidationError::ZeroIpcVarianceParam {
                field: "hot_iters",
                group_idx: 0,
            });
        }
        if cold_iters == 0 {
            return Err(WorkTypeValidationError::ZeroIpcVarianceParam {
                field: "cold_iters",
                group_idx: 0,
            });
        }
        if period_iters == 0 {
            return Err(WorkTypeValidationError::ZeroIpcVarianceParam {
                field: "period_iters",
                group_idx: 0,
            });
        }
        Ok(WorkType::IpcVariance {
            hot_iters,
            cold_iters,
            period_iters,
        })
    }

    /// User-supplied work function with a display name.
    ///
    /// `run` receives a [`WorkerCtx`] exposing the worker's stop flag
    /// (flipped per-mode: the SIGUSR1 handler for `CloneMode::Fork`, a
    /// per-worker `AtomicBool` for `CloneMode::Thread`) plus its
    /// effective cpuset, cgroup-sibling pids, own cgroup-v2 dir, and a
    /// default-zero [`CustomCfg`] payload (use [`Self::custom_with`] to pass
    /// a non-default cfg), and must return a [`WorkerReport`] when the stop
    /// flag becomes `true`. The
    /// framework handles fork / thread spawn, cgroup placement,
    /// affinity, scheduling policy, and signal setup (Fork mode only);
    /// `run` owns only the work loop.
    ///
    /// The per-iteration built-in instrumentation (wake-latency samples,
    /// `iter_slot` publish, gap tracking) runs only for built-in variants
    /// and is bypassed for `Custom`. See the [`Custom`](Self::Custom)
    /// variant doc for the full telemetry contract and what `run` must
    /// populate on [`WorkerReport`] to keep downstream assertions honest.
    pub fn custom(name: impl Into<String>, run: fn(&WorkerCtx) -> WorkerReport) -> Self {
        WorkType::Custom {
            name: name.into(),
            run: super::work_type::CustomFn(run),
            cfg: CustomCfg::default(),
        }
    }

    /// Like [`Self::custom`] but carries a [`CustomCfg`] payload, surfaced
    /// to the closure via [`WorkerCtx::cfg`]. The payload is Copy POD,
    /// inherited byte-faithfully across `fork`, so a Custom worker reads its
    /// per-worker config from `ctx.cfg()` instead of a `static` / global —
    /// see `tests/preempt_regression.rs` for the shared-futex pattern.
    pub fn custom_with(
        name: impl Into<String>,
        run: fn(&WorkerCtx) -> WorkerReport,
        cfg: CustomCfg,
    ) -> Self {
        WorkType::Custom {
            name: name.into(),
            run: super::work_type::CustomFn(run),
            cfg,
        }
    }
}
