//! DQN Trainer constructor -- `new_internal` and init helpers.

use std::collections::VecDeque;
use std::sync::Arc;
use std::time::Duration;

use anyhow::Result;
use ml_core::device::MlDevice;
use ml_core::fill_simulator::FillSimulator;
use risk::drawdown_monitor::DrawdownMonitor;
use risk::safety::position_limiter::HybridPositionLimiter;
use risk::safety::PositionLimiterConfig;
use rust_decimal::Decimal;
use tokio::sync::RwLock;
use tracing::info;

use crate::dqn::circuit_breaker::{CircuitBreaker, CircuitBreakerConfig};
use crate::dqn::curiosity::CuriosityModule;
use crate::dqn::dqn::DQNConfig;
use crate::dqn::logging::{LoggingConfig, MetricsAggregator};
use crate::dqn::portfolio_tracker::PortfolioTracker;
use crate::dqn::dqn::DQN;
use crate::dqn::reward::{RewardConfig, RewardFunction};
use crate::features::microstructure_features::*;
use crate::labeling::triple_barrier::TripleBarrierEngine;
use crate::TrainingMetrics;
use super::super::config::{DQNAgentType, DQNHyperparameters};
use super::DQNTrainer;

impl DQNTrainer {
    pub(crate) fn new_internal(mut hyperparams: DQNHyperparameters, debug_logging: bool, override_device: Option<MlDevice>) -> Result<Self> {
        // Validate batch_size > 0
        if hyperparams.batch_size == 0 {
            return Err(anyhow::anyhow!(
                "batch_size must be greater than 0"
            ));
        }

        // WAVE 26 P2.2: Validate gradient_accumulation_steps > 0
        if hyperparams.gradient_accumulation_steps == 0 {
            return Err(anyhow::anyhow!(
                "gradient_accumulation_steps must be greater than 0, got: {}",
                hyperparams.gradient_accumulation_steps
            ));
        }

        // Pre-compute hidden dims to get accurate model size for batch sizing.
        // Align input_dim to 8 so the log matches the actual model dimensions.
        // (device not yet created, so use the formula directly  -- CUDA always aligns)
        // 42 market + 14 portfolio (8 base + 6 plan) + 16 multi-timeframe + 20 OFI = 92, aligned (92+7)&!7=96
        let input_dim: usize = 96;
        let output_dim: usize = 5;
        let hidden_dims: Vec<usize> = match hyperparams.hidden_dim_base {
            Some(base) => {
                let b = crate::cuda_pipeline::align_to_tensor_cores(base);
                vec![b, b] // Constant-width: no tapering, no silently-discarded narrow layer
            }
            None => {
                let profile = ml_core::gpu::profile::GpuProfile::load();
                let base = profile.training.hidden_dim_base;
                let b = crate::cuda_pipeline::align_to_tensor_cores(base);
                vec![b, b] // Constant-width: no tapering
            }
        };

        // Compute accurate model size from actual network dimensions
        let full_dims: Vec<usize> = std::iter::once(input_dim)
            .chain(hidden_dims.iter().copied())
            .chain(std::iter::once(output_dim))
            .collect();
        let param_count = crate::gpu::memory_profile::network_param_count(&full_dims);
        // FP32 params + AdamW state (2x for momentum/variance) + target network copy = ~4x
        let model_overhead_mb = (param_count as f64 * 4.0 * 4.0) / (1024.0 * 1024.0);
        info!(
            "DQN network: {:?} -> {} params, {:.1} MB overhead",
            full_dims, param_count, model_overhead_mb
        );

        // batch_size is configured per GPU profile (h100.toml, rtx3050.toml).
        // No auto-sizing — the config is the source of truth.
        if hyperparams.batch_size == 0 {
            hyperparams.batch_size = 1024; // sensible default if somehow unset
        }
        info!("DQN batch_size: {} (from config)", hyperparams.batch_size);

        // Use override device if provided (hyperopt shares one CUDA context),
        // otherwise require CUDA GPU  -- no CPU fallback in CUDA builds.
        let device = if let Some(dev) = override_device {
            dev
        } else {
            match MlDevice::cuda(0) {
                Ok(dev) => {
                    dev
                }
                Err(e) => {
                    tracing::error!("CUDA device init failed: {e}");
                    return Err(anyhow::anyhow!(
                        "DQNTrainer requires CUDA GPU. CUDA init failed: {e}"
                    ));
                }
            }
        };

        // Set CUDA stack size ONCE for all GPU kernels.
        // cuCtxSetLimit reserves stack_bytes x max_threads_per_SM x SMs of VRAM.
        // RTX 3050 (20 SMs x 1536 threads): 64KB -> 1.88GB, 16KB -> 480MB.
        // Stack size is driven by the GPU TOML profile (config/gpu/*.toml).
        #[cfg(feature = "cuda")]
        if let MlDevice::Cuda { ref context, .. } = device {
            use cudarc::driver::sys::{cuCtxSetLimit, CUlimit, CUresult};
            context.bind_to_thread().map_err(|e| anyhow::anyhow!("bind: {e}"))?;
            let gpu_profile = ml_core::gpu::profile::GpuProfile::load();
            let stack_bytes: usize = gpu_profile.cuda.cuda_stack_bytes;
            let result = unsafe { cuCtxSetLimit(CUlimit::CU_LIMIT_STACK_SIZE, stack_bytes) };
            if result != CUresult::CUDA_SUCCESS {
                tracing::warn!("cuCtxSetLimit(STACK_SIZE, {stack_bytes}) failed: {result:?}  -- may cause kernel crashes");
            } else {
                tracing::info!("CUDA stack: {stack_bytes} bytes/thread (num_atoms={}, profile)", hyperparams.num_atoms);
            }
        }

        // Fork a dedicated CudaStream once. All GPU components (experience collector,
        // fused trainer, portfolio sim, monitoring) share this single stream via
        // Arc::clone. This eliminates:
        // 1. Candle's default stream (stream 0) from the hot path
        // 2. Dual-stream cudarc event tracking conflicts during CUDA Graph capture
        // 3. Implicit serialization points between default and forked streams
        let cuda_stream: Option<Arc<cudarc::driver::CudaStream>> =
            if let MlDevice::Cuda { ref stream, .. } = device {
                let stream = stream.fork()
                    .map_err(|e| anyhow::anyhow!("Failed to fork CUDA stream: {e}"))?;
                info!("Forked dedicated CudaStream for all GPU components");
                Some(stream)
            } else {
                None
            };

        // Dynamic replay buffer sizing: scale replay capacity to available VRAM.
        // Only activates when replay_buffer_vram_fraction > 0 and GPU is detected.
        // Also computes the PER memory budget from actual VRAM  -- no hardcoded caps.
        //
        // Guard: skip auto-sizing when buffer_size < 100K (the sizer's own minimum).
        // A small explicit buffer_size means the caller wants that exact size
        // (e.g., smoke tests with buffer_size=1024). Without this, H100's 75 GB VRAM
        // inflates small buffers -> 10M, causing empty-sample failures and multi-hour hangs.
        let original_buffer_size = hyperparams.buffer_size; // Save before AutoReplaySizer mutates it
        const AUTO_REPLAY_MIN_THRESHOLD: usize = 100_000;

        // Probe VRAM for PER budget  -- uses vram_fraction from hyperparams.
        let mut per_max_memory_bytes: usize = {
            use ml_core::memory_optimization::detect_gpu_hardware;
            match detect_gpu_hardware() {
                Ok(hw) => {
                    let frac = if hyperparams.replay_buffer_vram_fraction > 0.0 {
                        f64::from(hyperparams.replay_buffer_vram_fraction)
                    } else {
                        0.70_f64 // default fraction
                    };
                    (hw.free_memory_mb * frac / 3.0 * 1024.0 * 1024.0) as usize
                }
                Err(e) => {
                    return Err(anyhow::anyhow!(
                        "GPU PER requires VRAM probe  -- detect_gpu_hardware failed: {}", e
                    ));
                }
            }
        };

        // Dynamic replay buffer sizing: scale replay capacity to available VRAM.
        // Only activates when replay_buffer_vram_fraction > 0, GPU detected, and
        // buffer_size >= 100K (sizer's own minimum  -- smaller values are intentional).
        if hyperparams.replay_buffer_vram_fraction > 0.0
            && matches!(device, MlDevice::Cuda { .. })
            && (hyperparams.buffer_size == 0 || hyperparams.buffer_size >= AUTO_REPLAY_MIN_THRESHOLD)
        {
            use ml_core::memory_optimization::detect_gpu_hardware;
            if let Ok(hw) = detect_gpu_hardware() {
                let raw_sd = if !hyperparams.mbp10_data_dir.is_empty() { 74 } else { 66 };
                let aligned_sd = (raw_sd + 7) & !7;
                // Divide VRAM budget by 3 regime heads (each gets its own PER buffer)
                let num_regime_heads = 3;
                let per_head_fraction = f64::from(hyperparams.replay_buffer_vram_fraction) / num_regime_heads as f64;
                let replay_cfg = hw.optimal_replay_config(
                    aligned_sd,
                    per_head_fraction,
                );
                per_max_memory_bytes = replay_cfg.per_max_buffer_bytes;
                if replay_cfg.capacity != hyperparams.buffer_size {
                    info!(
                        "AutoReplaySizer: replay buffer {} -> {} (VRAM={:.0}MB, fraction={:.0}%, PER budget={:.0}MB)",
                        hyperparams.buffer_size,
                        replay_cfg.capacity,
                        hw.free_memory_mb,
                        hyperparams.replay_buffer_vram_fraction * 100.0,
                        per_max_memory_bytes as f64 / (1024.0 * 1024.0),
                    );
                    hyperparams.buffer_size = replay_cfg.capacity;
                }
            }
        }

        info!(
            "Initializing DQN trainer on device: {:?}, using 4-branch DQN (dir×mag×order×urgency = 4×3×3×3 = 108 actions)",
            "CUDA GPU",
        );

        // Create DQN configuration
        // State layout (GPU pipeline):
        //   [0..42)   42 market features (OHLCV, technical, patterns, volume, time, statistical)
        //   [42..56)  14 portfolio features (8 base + 6 plan)
        //   [56..72)  16 multi-timeframe features (4 windows x 4 features)
        //   [66..74)   8 raw OFI features (bid_ask_spread, depth_imbalance, ..., vpin, kyle_lambda)
        //   [74..82)   8 OFI temporal deltas (delta[bar] = ofi[bar] - ofi[bar-1])
        //   [82]       book_aggression (center-of-mass asymmetry from MBP-10 depth)
        //   [83]       log_bar_duration (ln(duration_secs) / 10.0)
        //   [84..86)   2 remaining microstructure (ofi_acceleration, toxicity_gradient)
        // Raw state_dim: 92 (42+14+16+20). OFI always enabled.
        // market_dim: always 42 (bottleneck compresses only base market features).
        // OFI features bypass bottleneck via portfolio_dim (fed directly to shared trunk).
        //
        // GpuTensor core alignment: state_dim is rounded up to the next multiple of 8
        // (92->96) so that cuBLAS dispatches TF32 HMMA instructions instead
        // of falling back to scalar FMA. The extra columns are zero-padded at the
        // data pipeline boundaries (GpuPreloadedData and train_batch CPU path).
        // state_dim is now the canonical STATE_DIM constant (96). The GpuDqnTrainer
        // handles bottleneck reduction internally via compute_param_sizes().

        // Resolve gradient clip norm ONCE  -- single source of truth (Bug #4 fix).
        let grad_norm = hyperparams.gradient_clip_norm.unwrap_or(10.0);

        let config = DQNConfig {
            num_actions: 4,                  // 4 direction levels (Short/Hold/Long/Flat)
            hidden_dims,
            learning_rate: hyperparams.learning_rate,
            // hyperparams fields below are f32 native (kernel-facing); DQNConfig stores some as f32
            // and some as f64 (ml-dqn crate not migrated). Cast once at the boundary.
            gamma: hyperparams.gamma,
            epsilon_start: hyperparams.epsilon_start,
            epsilon_end: hyperparams.epsilon_end,
            epsilon_decay: hyperparams.epsilon_decay,
            replay_buffer_capacity: hyperparams.buffer_size,
            collapse_warmup_capacity: original_buffer_size,
            batch_size: hyperparams.batch_size,
            min_replay_size: hyperparams.min_replay_size.min(hyperparams.buffer_size), // Cap at buffer_size to prevent deadlock
            target_update_freq: hyperparams.target_update_frequency, // Use hyperparameter instead of hardcoded 1000
            huber_delta: hyperparams.huber_delta,
            leaky_relu_alpha: 0.01, // Standard LeakyReLU alpha (prevents dead neurons)
            gradient_clip_norm: grad_norm, // Bug #4 fix: resolved once above

            // WAVE 16 (Agent 36): Target update configuration
            tau: f64::from(hyperparams.tau),
            tau_final: f64::from(hyperparams.tau) * 0.5,  // Anneal to 50% of base (was 10% — too frozen)
            tau_anneal_steps: 500_000,          // was 100K

            // Rainbow DQN warmup period
            warmup_steps: hyperparams.warmup_steps,

            // PER configuration
            initial_capital: f64::from(hyperparams.initial_capital),

            per_alpha: f64::from(hyperparams.per_alpha),
            per_beta_start: f64::from(hyperparams.per_beta_start),
            per_beta_max: 1.0,
            per_beta_annealing_steps: hyperparams.epochs * 500, // reach beta=1.0 by ~25% of training (was 2000 -> too slow)
            per_max_memory_bytes,

            // Wave 2.1: Dueling Networks (ENABLED BY DEFAULT - Wave 6.4)
            dueling_hidden_dim: hyperparams.dueling_hidden_dim,

            // Wave 2.2: Multi-Step Returns (N-step TD) (ENABLED BY DEFAULT - Wave 6.4)
            n_steps: hyperparams.n_steps,  // Default: 3 (Rainbow DQN standard)

            // Wave 2.3: Distributional RL (C51) (always enabled)
            num_atoms: hyperparams.num_atoms,                    // Rainbow DQN standard: 51 atoms
            v_min: hyperparams.v_min,                            // Minimum value for distribution support
            v_max: hyperparams.v_max,                            // Maximum value for distribution support

            // Wave 2.4: Noisy Networks for Exploration (always enabled)
            noisy_sigma_init: f64::from(hyperparams.noisy_sigma_init),      // Rainbow DQN standard: 0.5

            // BUG #37 FIX: Q-value clipping (prevents step-level explosions)
            q_value_clip_min: hyperparams.q_clip_min,
            q_value_clip_max: hyperparams.q_clip_max,

            // WAVE 23 P0 Fix #1: Adaptive gradient collapse threshold (from hyperparams)
            gradient_collapse_multiplier: f64::from(hyperparams.gradient_collapse_multiplier),
            gradient_collapse_patience: hyperparams.gradient_collapse_patience,

            cql_alpha: hyperparams.cql_alpha,
            // IQN runs unconditionally inside the cuda_pipeline dual-head
            // (`gpu_iqn_head` + `iqn_dual_head_kernel`) with a fixed 5-quantile
            // schedule (`FIXED_TAUS`) and kernel-side 1.0 Huber threshold.
            // `iqn_lambda` is the only IQN knob that still flows through here
            // (weight on the IQN aux loss in the C51+IQN dual head).
            iqn_lambda: hyperparams.iqn_lambda,
            branch_hidden_dim: hyperparams.branch_hidden_dim,
            cvar_alpha: hyperparams.cvar_alpha,

            #[allow(clippy::cast_possible_truncation)]
            minimum_profit_factor: hyperparams.minimum_profit_factor,
            // SP4 Layer B: weight_decay no longer flows from hyperparams;
            // ml-dqn `DQNConfig.weight_decay` retains its own default
            // (currently unread by any consumer post-migration; cleanup
            // deferred to a separate ml-dqn refactor pass).
            dropout_rate: f64::from(hyperparams.dropout_initial),
            entropy_coefficient: hyperparams.entropy_coefficient,
            noisy_epsilon_floor: hyperparams.noisy_epsilon_floor.unwrap_or(0.0), // C2: NoisyNet handles exploration
            count_bonus_coefficient: f64::from(hyperparams.count_bonus_coefficient.unwrap_or(0.0)),
            curiosity_market_dim: 42, // Always 42 raw market features  -- OFI features are separate
            ..DQNConfig::default()
        };

        // Extract curiosity dims before config is moved into the agent
        let curiosity_market_dim = config.curiosity_market_dim;
        let curiosity_hidden_dim = config.curiosity_hidden_dim;

        // Create DQN agent using the forked stream so ALL GPU components (trainer,
        // replay buffer, experience collector) share a single CUDA stream. This
        // eliminates cross-stream race conditions that caused sporadic NaN.
        let agent_device = if let Some(ref forked) = cuda_stream {
            MlDevice::Cuda {
                context: forked.context().clone(),
                stream: Arc::clone(forked),
            }
        } else {
            device.clone()
        };
        // Single DQN with MoE gate (Phase 3+). Regime conditioning via learned
        // MoE gate that reads ADX (idx 40) + CUSUM (idx 41) as part of the full
        // state vector — strictly subsuming the deleted 3-head threshold classifier.
        info!("Creating DQN with MoE gate (8 experts, learned regime conditioning)");
        let dqn = DQN::new_on_device(config, agent_device.clone())
            .map_err(|e| anyhow::anyhow!("Failed to create DQN: {}", e))?;
        let agent = DQNAgentType::new(dqn);
        

        // Initialize portfolio tracker with $100k starting capital and 1 basis point spread
        // Bug #2 fix: Portfolio features were hardcoded as [0.0, 0.0, 0.0] at line 1528
        let portfolio_tracker = PortfolioTracker::new(
            hyperparams.initial_capital, // P2-A: Configurable capital
            0.0001,    // 1 basis point spread (0.01%)
            f64::from(hyperparams.cash_reserve_percent), // Cash reserve requirement
        );

        // Initialize reward function with hyperparameter-driven configuration
        // Reward normalization and DSR now live in the GPU composite reward kernel.
        let reward_config = RewardConfig {
            pnl_weight: Decimal::ONE,
            risk_weight: Decimal::try_from(0.1).unwrap_or(Decimal::ZERO),
            cost_weight: Decimal::ONE, // Bug #2 fix: 100% transaction cost weight (was 0.05, 20x too low)
            movement_threshold: Decimal::try_from(f64::from(hyperparams.movement_threshold))
                .unwrap_or(Decimal::ZERO),
            diversity_weight: Decimal::try_from(-0.1).unwrap_or(Decimal::ZERO),
            circuit_breaker_config: CircuitBreakerConfig::default(),
            triple_barrier_profit_bonus: Decimal::try_from(0.5).unwrap_or(Decimal::ZERO),
            triple_barrier_stop_penalty: Decimal::try_from(0.5).unwrap_or(Decimal::ZERO),
            sharpe_weight: Decimal::ZERO, // WAVE 26 P1.3: Disabled by default
            sharpe_window: 20,            // WAVE 26 P1.3: Standard 20-period window
            // dsr_eta hardcoded — the DSR reward term was removed (see
            // 2026-04-21-phase1-reward-inventory). The RewardConfig field
            // still exists in ml-dqn/src/reward.rs; keeping the default
            // value here avoids a cross-crate change. Safe to drop when
            // ml-dqn's RewardConfig loses the field.
            dsr_eta: 0.01,
            initial_capital: f64::from(hyperparams.initial_capital),
        };
        let reward_fn = RewardFunction::new_with_debug(reward_config, debug_logging)?;

        // WAVE 1.1: Initialize triple barrier engine (max 1000 active trackers)
        let triple_barrier = Arc::new(RwLock::new(TripleBarrierEngine::new(1000)));
        info!("Triple barrier engine initialized with 1000 max trackers");

        // WAVE 16S: Initialize Kelly optimizer (always active)
        let kelly_optimizer = {
            use crate::risk::kelly_optimizer::{KellyCriterionOptimizer, KellyOptimizerConfig};
            let kelly_config = KellyOptimizerConfig {
                max_fraction: f64::from(hyperparams.kelly_max_fraction),
                min_fraction: 0.01,
                lookback_period: 252,
                confidence_threshold: 0.6,
                volatility_adjustment: true,
                drawdown_protection: true,
            };
            let optimizer = KellyCriterionOptimizer::new(kelly_config)
                .map_err(|e| anyhow::anyhow!("Failed to create Kelly optimizer: {}", e))?;
            info!("Kelly optimizer enabled (fractional={}, max={})",
                  hyperparams.kelly_fractional, hyperparams.kelly_max_fraction);
            Some(Arc::new(optimizer))
        };

        // Wave 16 Portfolio Features: action masking and entropy regularization always active
        let max_position: f64 = f64::from(hyperparams.max_position_absolute); // BLOCKER #2: Use hyperopt-tunable position limit

        // Entropy regularization: SAC-style computed directly on Q-values in DQN::compute_loss_internal
        {
            let coeff = hyperparams.entropy_coefficient;
            info!("Entropy regularization enabled (coefficient={coeff:.4}, applied to Q-value softmax in loss)");
        }

        // Multi-asset portfolio tracking (disabled -- single-asset is the current production mode)
        //
        // When expanding to multi-asset trading:
        //   1. Add `enable_multi_asset: bool` to DQNHyperparams (default false).
        //   2. Initialize MultiAssetPortfolioTracker here when the flag is set.
        //   3. Wire portfolio state into the DQN observation: expand state_dim to
        //      include per-asset position, PnL, and correlation features so the
        //      agent can learn cross-asset hedging and allocation.
        let multi_asset_portfolio: Option<Arc<crate::dqn::multi_asset::MultiAssetPortfolioTracker>> = None;

        // Stress testing for robustness validation
        // Initialized as None here; call init_stress_tester() after construction
        // to resolve the circular dependency (DQNStressTester needs a DQNTrainer).
        let stress_tester: Option<Arc<crate::dqn::stress_testing::DQNStressTester>> = None;

        info!(
            "Action masking enabled (max_position=+/-{:.1}, 30-50% filtering expected)",
            max_position
        );

        // Wave 16 Core Risk Features: Initialize drawdown monitor, position limiter, circuit breaker
        // These are ALWAYS enabled by default for production safety
        
        // 1. Drawdown Monitor (15% max drawdown, alerts at 10%, 12.5%, 15%)
        let drawdown_monitor = {
            // DrawdownMonitor will be configured in first training step
            // Config will be applied via async configure_alerts() in train_epoch
            info!("Drawdown monitor enabled (thresholds: 10%, 12.5%, 15%)");
            Some(Arc::new(DrawdownMonitor::new()))
        };

        // 2. Position Limiter (3-tier limits: +/-10.0 absolute, 1M notional, 10% concentration)
        let position_limiter = {
            let config = PositionLimiterConfig {
                enabled: true,
                cache_ttl: Duration::from_secs(60),
                rpc_check_threshold_percent: 0.8,
                max_position_per_symbol: 10.0,     // +/-10.0 absolute position limit
                max_order_value: 1_000_000.0,      // $1M notional limit
                max_daily_loss: 0.10,              // 10% concentration limit
            };
            let limiter = HybridPositionLimiter::new(config);
            info!("Position limiter enabled (abs=+/-10.0, notional=$1M, concentration=10%)");
            Some(Arc::new(limiter))
        };

        // 3. Circuit Breaker (5 consecutive failures, 60s cooldown)
        let circuit_breaker = {
            let config = CircuitBreakerConfig {
                failure_threshold: 5,
                success_threshold: 3,
                timeout_duration: Duration::from_secs(60),
                half_open_max_calls: 2,
            };
            let breaker = CircuitBreaker::new(config);
            info!("Circuit breaker enabled (threshold=5 failures, cooldown=60s)");
            Some(Arc::new(breaker))
        };

        // WAVE 24: Capture patience before hyperparams is moved
        let early_stopping_patience = hyperparams.gradient_collapse_patience;

        // WAVE 26 P1: Initialize advanced DQN features
        // P1.3: Sharpe ratio reward component
        let sharpe_weight: f64 = f64::from(hyperparams.sharpe_weight);
        let sharpe_window = hyperparams.sharpe_window;

        // P1.6: Adaptive dropout scheduler (always active)
        let dropout_scheduler = {
            use crate::dqn::network::DropoutScheduler;
            info!("Dropout scheduler enabled (initial={}, final={}, steps={})",
                  hyperparams.dropout_initial, hyperparams.dropout_final, hyperparams.dropout_anneal_steps);
            Some(DropoutScheduler::new(
                f64::from(hyperparams.dropout_initial),
                f64::from(hyperparams.dropout_final),
                hyperparams.dropout_anneal_steps,
            ))
        };

        // P1.9: Generalized Advantage Estimation (GAE)  -- always active
        let gae_calculator = {
            use crate::dqn::gae::GAECalculator;
            info!("GAE calculator enabled (lambda={}, gamma={})",
                  hyperparams.gae_lambda, hyperparams.gamma);
            Some(GAECalculator::new(f64::from(hyperparams.gae_lambda), f64::from(hyperparams.gamma)))
        };

        // P1.11: Noisy network sigma scheduling  -- always active
        let noisy_sigma_scheduler = {
            use crate::dqn::noisy_sigma_scheduler::NoisySigmaScheduler;
            info!("Noisy sigma scheduler enabled (initial={}, final={}, steps={})",
                  hyperparams.noisy_sigma_initial, hyperparams.noisy_sigma_final, hyperparams.noisy_sigma_anneal_steps);
            Some(NoisySigmaScheduler::new(
                f64::from(hyperparams.noisy_sigma_initial),
                f64::from(hyperparams.noisy_sigma_final),
                hyperparams.noisy_sigma_anneal_steps,
            ))
        };

        // WAVE 26 P1.8: Initialize curiosity module if curiosity_weight > 0
        // Must be created BEFORE hyperparams and device are moved
        let curiosity_module = if hyperparams.curiosity_weight > 0.0 {
            if let Some(ref s) = cuda_stream {
                Some(CuriosityModule::new(
                    std::sync::Arc::clone(s),
                    0.001,  // Forward model learning rate
                    2.0,    // Max curiosity reward (clip to prevent noise exploitation)
                    curiosity_market_dim,
                    curiosity_hidden_dim,
                    3,      // Action categories (Short/Flat/Long)
                ).map_err(|e| anyhow::anyhow!("Failed to create curiosity module: {}", e))?)
            } else {
                None
            }
        } else {
            None
        };

        // WAVE 26 P0.6: Initialize learning rate scheduler with warmup
        // Must be created BEFORE hyperparams is moved
        let lr_scheduler = {
            use super::super::lr_scheduler::LRScheduler;
            LRScheduler::new(
                hyperparams.learning_rate,
                hyperparams.warmup_steps,
                hyperparams.lr_decay_type,
            )
        };

        // Capture values before hyperparams is moved into Self
        let initial_batch_size = hyperparams.batch_size;
        let base_tau: f64 = f64::from(hyperparams.tau);
        let initial_epsilon = hyperparams.epsilon_start;
        // Plan C Phase 2 follow-up K (2026-04-29): cosine LR baseline for the
        // per-step warmup-factor consumer; captured here so the struct
        // literal below doesn't reborrow `hyperparams` after move.
        let initial_cosine_effective_lr_base: f32 = hyperparams.learning_rate as f32;

        // Multi-GPU: auto-detect if multiple CUDA devices are available
        let multi_gpu = crate::cuda_pipeline::multi_gpu::MultiGpuConfig::detect()
            .unwrap_or(None);
        if let Some(ref mg) = multi_gpu {
            info!("Multi-GPU: {} devices detected, data parallelism enabled", mg.world_size);
        }

        // #33 Extract saboteur config before hyperparams is moved (always active)
        let sab_enabled = true;
        let sab_warmup = hyperparams.adversarial_warmup_epochs;
        let sab_interval = hyperparams.adversarial_checkpoint_interval;

        Ok(Self {
            agent: Arc::new(RwLock::new(agent)),
            hyperparams,
            device,
            validation_stream: if let Some(ref s) = cuda_stream {
                match s.fork() {
                    Ok(vs) => {
                        info!("Forked dedicated validation CudaStream for async validation");
                        Some(vs)
                    }
                    Err(e) => { tracing::warn!("validation stream alloc failed: {e}"); None }
                }
            } else { None },
            pending_val_loss: None,
            cached_async_val_loss: None,
            cuda_stream,
            metrics: Arc::new(RwLock::new(TrainingMetrics::new())),
            loss_history: Vec::new(),
            q_value_history: Vec::new(),
            best_val_loss: f64::INFINITY, // Start with worst possible loss
            val_data: Vec::new(),
            val_loss_history: Vec::new(),
            sharpe_history: Vec::new(),
            best_sharpe: f64::NEG_INFINITY, // C4: Start with worst possible Sharpe
            best_epoch: 0,
            gradient_logging_step: 0,
            collapse_warmup_buffer_size: original_buffer_size,
            portfolio_tracker,
            feature_stats: None, // WAVE 3 FIX #2: Start with None, collect stats in epochs 0-10
            recent_actions: VecDeque::with_capacity(100),
            reward_fn,

            // WAVE 16S: Adaptive risk management
            kelly_optimizer,
            trade_history: VecDeque::with_capacity(500),
            volatility_returns: VecDeque::with_capacity(20), // Use default instead of moved hyperparams
            trade_stats_history: Vec::new(),
            training_sharpe_ema: 0.0,
            training_sharpe_ema_initialized: false,
            last_action_entropy: None,
            last_magnitude_dist: [0.0_f32; 3],
            // SP8 (Fix 36, 2026-05-03): `last_train_active_frac` host-cached
            // field deleted; the canary now lives at
            // `ISV[TRAIN_ACTIVE_FRAC_INDEX]` and is read via the accessor
            // `last_train_active_frac()` per `feedback_no_cpu_compute_strict.md`.
            explore_entropy_mag_history: Vec::new(),
            prev_controller_values: super::ControllerPrevValues::default(),
            controller_fire_counts: super::ControllerFireCounts::default(),
            controller_total_epochs: 0,
            grad_clip_kicked_this_epoch: false,
            last_eval_magnitude_dist: [0.0_f32; 3],
            last_eval_intent_magnitude_dist: [0.0_f32; 3],
            // Plan 5 Task 5 Phase A — populated after each `compute_validation_loss`
            // call; consumed by HEALTH_DIAG `val [...]` block emit.
            last_val_metrics: None,
            prev_reward_contrib_popart_var: 0.0,
            last_reward_contrib: [0.0_f32; 4],

            // Wave 16 Portfolio Features (action masking always active)
            max_position,
            current_epoch: 0,
            // SP14 L1+L2 follow-up (2026-05-05): fold_step_counter deleted
            // per feedback_isv_for_adaptive_bounds; variance-driven k_aux/k_q
            // provide warmup intrinsically.
            multi_asset_portfolio,
            stress_tester,

            // Wave 16 Core Risk Features
            drawdown_monitor,
            position_limiter,
            circuit_breaker,

            // WAVE 3.10: Microstructure feature calculators
            micro_high_low_spread: HighLowSpread::default(),
            micro_vw_spread: VolumeWeightedSpread::default(),
            micro_tick_count: TickCount::default(),
            micro_inter_arrival: InterArrivalTime::default(),
            micro_buy_sell_imbalance: BuySellImbalance::default(),
            micro_kyle_lambda: KyleLambda::default(),
            micro_price_impact: PriceImpact::default(),
            micro_variance_ratio: VarianceRatio::default(),
            last_timestamp_ns: 0,
            last_close: 0.0,

            // WAVE 1.1: Triple barrier integration
            triple_barrier,
            active_position_tracker: None,
            previous_simulated_position: 0.0,  // WAVE P3: Start with flat position

            // WAVE 1.2: Safety Infrastructure Integration (8 Systems)
            safety_loss_history: VecDeque::with_capacity(30),
            safety_loss_plateau_counter: 0,
            safety_action_counts: std::collections::HashMap::new(),
            safety_memory_manager: Arc::new(RwLock::new(
                crate::safety::memory_manager::SafeMemoryManager::new(
                    &crate::safety::MLSafetyConfig::default()
                )
            )),
            safety_level: crate::safety::SafetyLevel::Normal, // Default to Normal mode
            safety_step_counter: 0,

            feature_cache_dir: None,

            prev_epoch_q_mean: 0.0,
            adaptive_tau: base_tau,
            // Plan C Phase 2 follow-up H (2026-04-29): rolling window for the
            // Q-drift kill criterion's MAD-deviation test. Empty at construction
            // — the kill check skips the deviation branch until the window has
            // ≥ 2 entries (median/MAD undefined on a 1-element window). Capacity
            // matches `Q_DRIFT_WINDOW_SIZE` in `training_loop.rs`.
            q_mean_window: VecDeque::with_capacity(8),
            // Plan C Phase 2 follow-up K (2026-04-29): cosine LR baseline
            // updated by the cosine schedule block in `training_loop.rs`
            // and consumed per-step by the warmup-factor LR consumer.
            // Initialised to the configured base LR so the first epoch's
            // pre-cosine `set_lr` matches the constructor-injected
            // `config.lr` (no transient mismatch).
            cosine_effective_lr_base: initial_cosine_effective_lr_base,

            // Plan C T11 follow-up — F1 ep2 explosion diagnostic (2026-04-29).
            // Diagnostic-only fields; remove once root cause is identified.
            current_fold: 0,
            last_logged_grad_norm: None,
            explosion_diag_steps_remaining: 0,

            // WAVE 24 (Agent 17): Initialize patience-based early stopping
            // Use gradient_collapse_patience from hyperparams for consistency
            // Set min_delta to 0.001 (0.1% improvement threshold)
            early_stopping: super::super::early_stopping::EarlyStopping::new(
                early_stopping_patience, // Reuse patience parameter (default: 5)
                0.001, // 0.1% minimum improvement
            ),

            // WAVE 26 P0.6: Use pre-initialized learning rate scheduler
            lr_scheduler,

            // WAVE 26 P1.8: Use pre-initialized curiosity module
            curiosity_module,

            // WAVE 26 P1: Advanced DQN Features
            // P1.3: Sharpe ratio reward
            returns_history: VecDeque::with_capacity(sharpe_window),
            sharpe_weight,

            // P1.6: Adaptive dropout
            dropout_scheduler,

            // P1.9: Generalized Advantage Estimation
            gae_calculator,

            // P1.11: Noisy sigma scheduler
            noisy_sigma_scheduler,

            // WAVE 30: Structured logging integration
            logging_config: LoggingConfig::default(),
            metrics_aggregator: MetricsAggregator::new(),

            // OOM recovery: track effective batch size
            current_batch_size: initial_batch_size,

            // Q-value estimation: periodic (every 50 steps) instead of every step
            cached_avg_q: 0.0,
            q_estimation_counter: 0,
            low_dd_ratio: 0.0,
            adversarial_active: false,
            max_dd_ema: 0.0,
            saboteur: super::super::adversarial_self_play::AdversarialSaboteur::new(
                sab_enabled, sab_warmup, sab_interval,
            ),
            epoch_feature_mask: None,
            epoch_vol_normalizer: 0.0,
            epoch_q_min: f32::INFINITY,
            epoch_q_max: f32::NEG_INFINITY,
            epoch_q_variance: 0.0,
            epoch_q_gap: 0.0,
            epoch_atom_entropy: 0.0,
            epoch_atom_utilization: 0.0,
            observed_reward_std: 0.0,
            observed_reward_mean: 0.0,

            // GPU pipeline: pre-uploaded training data (initialized lazily at first epoch)
            gpu_data: None,
            targets_raw_cuda: None,
            features_raw_cuda: None,
            gpu_experience_collector: None,
            gpu_action_selector: None,
            training_guard: None,
            gpu_monitoring: None,
            reduction_kernels: None,

            // GPU walk-forward: initialized lazily when enable_gpu_walk_forward=true
            gpu_walk_forward: None,

            // Multi-GPU: auto-detected data parallelism
            multi_gpu,

            // OFI features: populated during data loading when MBP-10 data is available
            ofi_features: None,
            ofi_val_offset: 0,

            // SP15 Phase 1.6: Q8 dev / Q9 holdout slices populated lazily by
            // `train_walk_forward` BEFORE fold generation. None at construction.
            dev_features: None,
            dev_targets: None,
            holdout_features: None,
            holdout_targets: None,

            // SP15 Phase 1.7: per-fold test slice (re-populated each fold via
            // `set_test_data_from_slices`). None at construction; bars 0..0
            // until the first fold sets a real range.
            test_features: None,
            test_targets: None,
            test_start_bar: 0,
            test_end_bar: 0,
            test_data_observer: None,

            gpu_evaluator: None,

            // Phase C: Fill simulation and smart order routing
            fill_simulator: FillSimulator::default(),
            vol_ema: 0.01,     // Initial volatility estimate (1% daily)
            median_vol: 0.01,  // Slowly adapting baseline

            // Fused CUDA training: lazy-initialized on first training step
            fused_ctx: None,

            // Task 15: Per-fold adaptive regime replay decay (1.0 = no bias, standard PER)
            regime_replay_decay_override: 1.0,
            fold_dominant_regime: 1, // Default: Ranging
            experience_done_event: None,
            // In-flight async best-checkpoint workers — empty at trainer
            // construction; populated per epoch by
            // `handle_epoch_checkpoints_and_early_stopping`.
            pending_checkpoint_handles: Vec::new(),
            fold_train_start: 0,
            fold_train_end: 0,
            fold_val_start: 0,
            fold_val_end: 0,
            cached_n_episodes: None,
            backtracking: super::BacktrackingState::new(),
            cost_anneal_factor: 0.0,
            wf_window_sharpes: Vec::new(),
            wf_num_windows: 6,
            wf_purge_bars: 2000,  // ~1 trading day of imbalance bars. Was 100 (~1 hour, too short for ES regime persistence)
            wf_min_sharpe: f64::NEG_INFINITY,
            enrichment: super::enrichment::EnrichmentState::new(
                initial_epsilon,
            ),
            learning_health: crate::cuda_pipeline::learning_health::LearningHealth::new(),
            health_ema: Default::default(),
            last_q_gap: None,
            last_q_var: None,
            last_grad_norm: None,
            last_ens_disagreement: None,
            last_atom_util: None,
            last_cql_alpha_eff: None,
            last_iqn_budget_eff: None,
            last_cql_budget_eff: None,
            last_c51_budget_eff: None,
            last_c51_budget_per_branch: None,
            last_iqn_budget_per_branch: None,
            last_cql_budget_per_branch: None,
            last_tau_eff: None,
            last_sarsa_tau_factor: None,
            last_gamma_eff: None,
            last_anti_mult: 1.0,
            last_cf_ratio_eff: None,
            last_distill_active: None,
            last_barrier_loss: None,
            last_plasticity_ready: None,
            unhealthy_epoch_count: 0,
            last_ib_penalty: None,
            last_ensemble_collapse_score: None,
            last_contrarian_active: None,
            low_winrate_count: 0,
            contrarian_active: false,
            contrarian_remaining_epochs: 0,
            last_epoch_win_rate: 0.5,
            last_meta_q_pred: None,
            meta_q: crate::cuda_pipeline::meta_q_network::MetaQNetwork::new(),
            pending_meta_q_samples: std::collections::VecDeque::with_capacity(32),
            health_history: std::collections::VecDeque::with_capacity(2 * crate::cuda_pipeline::meta_q_network::META_Q_LOOKAHEAD),

            // Plan 5 Task 2 — A.4 Regression Detection Hard-Stop.
            // Auto-load `config/metric-bands.toml` if it exists alongside the
            // workspace Cargo.toml (same resolution rule the smoke tests use).
            // Falls back to an empty registry (no termination) when the file
            // is missing — keeps non-production callers (hyperopt sweeps,
            // bench harnesses) that don't ship the file functional.
            metric_bands: super::monitoring::MetricBandsRegistry::load_from_toml(
                std::path::Path::new("config/metric-bands.toml"),
            )
                .or_else(|_| {
                    // Try workspace-relative path so smoke tests run from
                    // crates/ml/ working directory still find it.
                    let candidates = [
                        std::path::PathBuf::from("../../config/metric-bands.toml"),
                        std::path::PathBuf::from("../config/metric-bands.toml"),
                    ];
                    for p in &candidates {
                        if p.is_file() {
                            return super::monitoring::MetricBandsRegistry::load_from_toml(p);
                        }
                    }
                    Err(anyhow::anyhow!("metric-bands.toml not found"))
                })
                .unwrap_or_else(|e| {
                    tracing::info!(
                        target: "dqn::regression",
                        "MetricBandsRegistry: no metric-bands.toml loaded ({e}); \
                         regression detection disabled for this trainer instance"
                    );
                    super::monitoring::MetricBandsRegistry::empty()
                }),
        })
    }

}
