//! #33 Adversarial Self-Play with Past Self (THE KING)
//!
//! GPU-native evolutionary saboteur that controls market microstructure
//! (spread, fill probability, slippage) to MAXIMIZE the trader's losses.
//!
//! All random number generation and parameter search happens on GPU:
//! - `saboteur_generate_params` kernel: per-episode params from GPU LCG RNG
//! - `saboteur_select_best` kernel: reduction to find worst-for-trader params
//! - Per-episode overrides in `experience_env_step` kernel via `saboteur_params` ptr
//!
//! This Rust struct only tracks epoch-level state (warmup, cycle phase).
//! Zero CPU-side RNG. Zero host↔device transfers for the evolutionary search.
//!
//! Self-play cycle:
//!   Phase 0 (epochs 0..warmup):     Normal training, no saboteur
//!   Phase 1 (odd epochs after warmup): Saboteur explores (GPU kernel generates params)
//!   Phase 2 (even epochs after warmup): Trader trains against best saboteur params

use tracing::info;

/// Epoch-level saboteur state. All computation is GPU-native.
pub(crate) struct AdversarialSaboteur {
    /// Whether self-play is enabled
    pub(crate) enabled: bool,
    /// Warmup epochs before self-play begins
    warmup_epochs: usize,
    /// How often to reset the saboteur's search (epochs)
    checkpoint_interval: usize,
    /// Current perturbation scale (decays over epochs)
    pub(crate) perturbation_scale: f32,
    /// Epoch counter for decay scheduling
    step_count: usize,
}

impl AdversarialSaboteur {
    pub(crate) fn new(enabled: bool, warmup_epochs: usize, checkpoint_interval: usize) -> Self {
        Self {
            enabled,
            warmup_epochs,
            checkpoint_interval,
            perturbation_scale: 0.3,
            step_count: 0,
        }
    }

    /// Check if self-play is active for this epoch.
    pub(crate) fn is_active(&self, epoch: usize) -> bool {
        self.enabled && epoch >= self.warmup_epochs
    }

    /// Check if the saboteur is exploring this epoch (odd epochs after warmup).
    pub(crate) fn is_saboteur_epoch(&self, epoch: usize) -> bool {
        self.is_active(epoch) && (epoch - self.warmup_epochs) % 2 == 1
    }

    /// Called at epoch end. Updates perturbation scale decay.
    /// The actual evolutionary search (param generation + selection) is done
    /// entirely in CUDA kernels inside the experience collector.
    pub(crate) fn update(&mut self, epoch: usize, _trader_sharpe: f32) {
        if !self.is_active(epoch) {
            return;
        }

        self.step_count += 1;

        // Decay perturbation scale (exploration → exploitation)
        self.perturbation_scale *= 0.995;
        self.perturbation_scale = self.perturbation_scale.max(0.05);

        // Reset search every checkpoint_interval epochs
        if self.checkpoint_interval > 0
            && self.step_count % self.checkpoint_interval == 0
        {
            self.perturbation_scale = 0.3; // Reset exploration
            info!(
                epoch, step_count = self.step_count,
                "SABOTEUR search reset (checkpoint interval)"
            );
        }

        if epoch % 10 == 0 {
            info!(
                epoch,
                perturbation_scale = %format!("{:.3}", self.perturbation_scale),
                is_exploring = self.is_saboteur_epoch(epoch),
                "Saboteur epoch state (GPU-native)"
            );
        }
    }
}
