//! TGGN (Temporal Graph Gated Network) Hyperparameter Optimization Adapter
//!
//! Provides `TGGNParams` (implementing `ParameterSpace`), `TGGNMetrics`,
//! and `TGGNTrainer` (implementing `HyperparameterOptimizable`) for
//! hyperparameter optimization of the TGGN model via the unified framework.

use std::sync::Arc;
use ml_core::device::MlDevice;
use serde::{Deserialize, Serialize};
use std::path::PathBuf;
use tracing::{info, warn};

use crate::features::extract_ml_features;
use crate::features::extraction::OHLCVBar;
use crate::hyperopt::paths::TrainingPaths;
use crate::hyperopt::traits::{HardwareBudget, HyperparameterOptimizable, ParameterSpace};
use crate::tgnn::TGGNConfig;
use crate::tgnn::trainable_adapter::TGGNTrainableAdapter;
use crate::training::unified_trainer::UnifiedTrainable;
use crate::MLError;

/// TGGN graph network model overhead in MB (weights + optimizer state)
const MODEL_OVERHEAD_MB: f64 = 40.0;
/// TGGN per-sample memory in MB (graph edges + node features)
const MB_PER_SAMPLE: f64 = 0.01;

/// Hyperparameters for TGGN hyperopt tuning.
///
/// Controls the temporal graph neural network's architecture and training:
/// - Graph structure: `node_dim`, `hidden_dim`, `num_layers`, `message_passing_steps`
/// - Temporal dynamics: `temporal_decay`
/// - Regularization: `dropout`, `weight_decay`, `grad_clip`
/// - Training: `learning_rate`, `batch_size`
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
pub struct TGGNParams {
    /// Learning rate for AdamW optimizer (log-scale)
    pub learning_rate: f64,
    /// Hidden dimension for graph message passing
    pub hidden_dim: usize,
    /// Number of message passing layers
    pub num_layers: usize,
    /// Input node feature dimension
    pub node_dim: usize,
    /// Number of message passing steps per layer
    pub message_passing_steps: usize,
    /// Temporal decay factor for exponential edge weighting
    pub temporal_decay: f64,
    /// Dropout rate for regularization
    pub dropout: f64,
    /// Training batch size
    pub batch_size: usize,
    /// L2 weight decay (log-scale)
    pub weight_decay: f64,
    /// Maximum gradient norm for clipping (log-scale)
    pub grad_clip: f64,
    /// Signal high threshold in basis points (GPU backtest action mapping)
    pub signal_high_bps: f64,
    /// Signal low threshold in basis points (GPU backtest action mapping)
    pub signal_low_bps: f64,
}

impl Default for TGGNParams {
    fn default() -> Self {
        Self {
            learning_rate: 1e-3,
            hidden_dim: 64,
            num_layers: 3,
            node_dim: 32,
            message_passing_steps: 3,
            temporal_decay: 0.99,
            dropout: 0.1,
            batch_size: 32,
            weight_decay: 1e-4,
            grad_clip: 1.0,
            signal_high_bps: 10.0,
            signal_low_bps: 5.0,
        }
    }
}

/// Number of continuous parameters in the TGGN parameter space.
const NUM_PARAMS: usize = 12;

impl ParameterSpace for TGGNParams {
    fn continuous_bounds() -> Vec<(f64, f64)> {
        vec![
            (1e-5_f64.ln(), 1e-2_f64.ln()), // learning_rate (log)
            (16.0, 1024.0),                  // hidden_dim (linear)
            (1.0, 6.0),                      // num_layers (linear)
            (8.0, 256.0),                    // node_dim (linear)
            (1.0, 6.0),                      // message_passing_steps (linear)
            (0.9, 0.999),                    // temporal_decay (linear)
            (0.0, 0.5),                      // dropout (linear)
            (8.0, 512.0),                    // batch_size (linear)
            (1e-6_f64.ln(), 1e-2_f64.ln()),  // weight_decay (log)
            (0.5_f64.ln(), 5.0_f64.ln()),    // grad_clip (log)
            (1.0, 30.0),                     // signal_high_bps
            (0.5, 15.0),                     // signal_low_bps
        ]
    }

    fn from_continuous(x: &[f64]) -> Result<Self, MLError> {
        if x.len() != NUM_PARAMS {
            return Err(MLError::ConfigError(format!("Expected {} continuous parameters, got {}", NUM_PARAMS, x.len())));
        }
        Ok(Self {
            learning_rate: x.first().copied().unwrap_or(0.0).exp(),
            hidden_dim: x.get(1).copied().unwrap_or(64.0).round().max(16.0) as usize,
            num_layers: x.get(2).copied().unwrap_or(3.0).round().max(1.0) as usize,
            node_dim: x.get(3).copied().unwrap_or(32.0).round().max(8.0) as usize,
            message_passing_steps: x.get(4).copied().unwrap_or(3.0).round().max(1.0) as usize,
            temporal_decay: x.get(5).copied().unwrap_or(0.99).clamp(0.9, 0.999),
            dropout: x.get(6).copied().unwrap_or(0.1).clamp(0.0, 0.5),
            batch_size: x.get(7).copied().unwrap_or(32.0).round().max(8.0) as usize,
            weight_decay: x.get(8).copied().unwrap_or(0.0).exp(),
            grad_clip: x.get(9).copied().unwrap_or(0.0).exp(),
            signal_high_bps: x.get(10).copied().unwrap_or(10.0).clamp(1.0, 30.0),
            signal_low_bps: x.get(11).copied().unwrap_or(5.0).clamp(0.5, 15.0),
        })
    }

    fn to_continuous(&self) -> Vec<f64> {
        vec![
            self.learning_rate.ln(),
            self.hidden_dim as f64,
            self.num_layers as f64,
            self.node_dim as f64,
            self.message_passing_steps as f64,
            self.temporal_decay,
            self.dropout,
            self.batch_size as f64,
            self.weight_decay.ln(),
            self.grad_clip.ln(),
            self.signal_high_bps,
            self.signal_low_bps,
        ]
    }

    fn param_names() -> Vec<&'static str> {
        vec![
            "learning_rate",
            "hidden_dim",
            "num_layers",
            "node_dim",
            "message_passing_steps",
            "temporal_decay",
            "dropout",
            "batch_size",
            "weight_decay",
            "grad_clip",
            "signal_high_bps",
            "signal_low_bps",
        ]
    }

    fn continuous_bounds_for(budget: &HardwareBudget) -> Vec<(f64, f64)> {
        let mut bounds = Self::continuous_bounds();
        if let Some(max_batch) = budget.max_batch_size(MODEL_OVERHEAD_MB, MB_PER_SAMPLE, 8.0, 2048.0) {
            if let Some(batch_bound) = bounds.get_mut(7) {
                batch_bound.1 = max_batch;
            }
        }
        // Cap hidden_dim and node_dim by VRAM
        if budget.gpu_memory_mb < 8000 {
            bounds[1] = (16.0, 128.0);    // hidden_dim cap
            bounds[3] = (8.0, 64.0);      // node_dim cap
        } else if budget.gpu_memory_mb < 16000 {
            bounds[1] = (16.0, 256.0);
            bounds[3] = (8.0, 128.0);
        } else if budget.gpu_memory_mb < 40000 {
            bounds[1] = (16.0, 512.0);
            bounds[3] = (8.0, 128.0);
        } else {
            // ≥40GB (L40S 46GB, H100 80GB): allow full range
        }
        bounds
    }
}

/// Metrics returned from TGGN hyperopt training.
#[derive(Debug, Clone, Default, Serialize, Deserialize)]
pub struct TGGNMetrics {
    /// Validation loss (primary objective for optimization)
    pub val_loss: f64,
    /// Training loss at end of run
    pub train_loss: f64,
    /// Directional accuracy on validation set
    pub directional_accuracy: f64,
    /// Number of epochs completed
    pub epochs_completed: usize,
    /// GPU walk-forward backtest Sharpe ratio
    pub backtest_sharpe: Option<f32>,
    /// GPU walk-forward backtest total trades
    pub backtest_trades: Option<u32>,
}

/// TGGN trainer for hyperparameter optimization.
///
/// Wraps the `TGGNTrainableAdapter` (which implements `UnifiedTrainable`)
/// and drives it through the `HyperparameterOptimizable` interface.
#[derive(Debug)]
pub struct TGGNTrainer {
    data_dir: PathBuf,
    epochs: usize,
    device: MlDevice,
    training_paths: TrainingPaths,
    early_stopping_patience: usize,
    trial_counter: usize,
    preloaded_bars: Option<Arc<[OHLCVBar]>>,
}

impl TGGNTrainer {
    /// Create a new TGGN trainer.
    ///
    /// # Arguments
    /// * `data_dir` - Directory containing .dbn/.dbn.zst files
    /// * `epochs` - Number of training epochs per trial
    pub fn new<P: Into<PathBuf>>(data_dir: P, epochs: usize) -> Result<Self, MLError> {
        let data_dir = data_dir.into();
        if !data_dir.exists() {
            return Err(MLError::ConfigError(format!("Data directory not found: {}", data_dir.display())));
        }

        let device = MlDevice::cuda(0)
            .map_err(|e| MLError::ConfigError(format!("CUDA GPU required for TGGN hyperopt: {}", e)))?;

        info!(
            "TGGN Trainer initialized: Device={:?}, Data={}, Epochs={}",
            device, data_dir.display(), epochs
        );

        Ok(Self {
            data_dir,
            epochs,
            device,
            training_paths: TrainingPaths::new("/tmp/ml_training", "tggn", "default"),
            early_stopping_patience: 10,
            trial_counter: 0,
            preloaded_bars: None,
        })
    }

    /// Set training paths configuration.
    pub fn with_training_paths(mut self, paths: TrainingPaths) -> Self {
        self.training_paths = paths;
        self
    }

    /// Configure early stopping patience.
    pub fn with_early_stopping(mut self, patience: usize) -> Self {
        self.early_stopping_patience = patience;
        self
    }

    /// Preload DBN data once for reuse across all hyperopt trials.
    pub fn preload_data(&mut self) -> Result<(), MLError> {
        if self.preloaded_bars.is_some() {
            return Ok(());
        }
        let bars = super::dbn_loader::load_bars_from_dbn_dir(&self.data_dir)
            .map_err(|e| MLError::ModelError(format!("Failed to preload DBN data: {}", e)))?;
        info!("Preloaded {} OHLCV bars for reuse across trials", bars.len());
        self.preloaded_bars = Some(Arc::from(bars));
        Ok(())
    }

    /// Check if data has been preloaded.
    pub fn has_preloaded_data(&self) -> bool {
        self.preloaded_bars.is_some()
    }
}

impl HyperparameterOptimizable for TGGNTrainer {
    type Params = TGGNParams;
    type Metrics = TGGNMetrics;

    fn train_with_params(&mut self, params: Self::Params) -> Result<Self::Metrics, MLError> {
        let trial_start = std::time::Instant::now();
        let current_trial = self.trial_counter;
        self.trial_counter += 1;

        info!(
            "Training TGGN: lr={:.6}, hidden={}, layers={}, node_dim={}, batch={}",
            params.learning_rate, params.hidden_dim, params.num_layers,
            params.node_dim, params.batch_size
        );

        self.training_paths.create_all().map_err(|e| {
            MLError::ModelError(format!("Failed to create training directories: {}", e))
        })?;

        // Load bars and extract features (use preloaded cache if available)
        let bars = if let Some(ref preloaded) = self.preloaded_bars {
            preloaded.to_vec() // cpu-side Arc<[OHLCVBar]> clone
        } else {
            super::dbn_loader::load_bars_from_dbn_dir(&self.data_dir)
                .map_err(|e| MLError::ModelError(format!("Failed to load DBN data: {}", e)))?
        };

        let features = extract_ml_features(&bars)
            .map_err(|e| MLError::ModelError(format!("Failed to extract features: {}", e)))?;

        if features.len() < 2 {
            return Err(MLError::ModelError("Insufficient features extracted".to_owned()));
        }

        let feature_dim = 51;

        // TGGN uses flat (non-sequence) input: node_dim is configurable
        // We project 51-dim features into node_dim through the model's input projection
        let pairs = crate::hyperopt::shared_data::build_flat_pairs(
            &features, &bars, feature_dim, &self.device,
        )?;

        let split = (pairs.len() as f64 * 0.8) as usize;
        let train_data = &pairs[..split];
        let val_data = &pairs[split..];

        if val_data.len() < 2 {
            return Err(MLError::ModelError("Validation set too small".to_owned()));
        }

        let config = TGGNConfig {
            max_nodes: 64,
            max_edges: 256,
            node_dim: feature_dim, // Use raw feature dim as node_dim for the adapter
            edge_dim: 4,
            hidden_dim: params.hidden_dim,
            num_layers: params.num_layers,
            temporal_decay: params.temporal_decay,
            update_frequency_ns: 1_000_000,
            use_simd: false,
        };

        let training_result = std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| -> Result<TGGNMetrics, MLError> {
            let mut model = TGGNTrainableAdapter::new(config)?;

            let mut best_val_loss = f64::MAX;
            let mut patience_counter = 0_usize;
            let mut last_train_loss = 0.0_f64;

            for epoch in 0..self.epochs {
                let mut loss_accum = 0.0_f64;
                let mut batch_count = 0_usize;

                let stream = self.device.cuda_stream()
                    .map_err(|e| MLError::DeviceError(format!("CUDA stream: {e}")))?;
                for (input, target) in train_data {
                    let input_host = input.to_host(stream)?;
                    let target_host = target.to_host(stream)?;
                    let loss_val = model.forward_loss(&input_host, &target_host)?;

                    if batch_count == 0 && (loss_val.is_nan() || loss_val.is_infinite()) {
                        return Ok(TGGNMetrics {
                            val_loss: 1000.0,
                            train_loss: 1000.0,
                            directional_accuracy: 0.0,
                            epochs_completed: epoch,
                            backtest_sharpe: None,
                            backtest_trades: None,
                        });
                    }

                    model.backward(loss_val)?;
                    model.optimizer_step()?;
                    loss_accum += loss_val;
                    batch_count += 1;
                }

                last_train_loss = if batch_count > 0 {
                    loss_accum / batch_count as f64
                } else {
                    0.0
                };

                // Validation: average forward_loss over val data
                let mut vl_accum = 0.0_f64;
                let mut vl_count = 0_usize;
                for (vi, vt) in val_data {
                    let vi_h = vi.to_host(stream)?;
                    let vt_h = vt.to_host(stream)?;
                    if let Ok(vl) = model.forward_loss(&vi_h, &vt_h) {
                        vl_accum += vl;
                        vl_count += 1;
                    }
                }
                let val_loss = if vl_count > 0 { vl_accum / vl_count as f64 } else { 1000.0 };

                if val_loss < best_val_loss {
                    best_val_loss = val_loss;
                    patience_counter = 0;
                } else {
                    patience_counter += 1;
                }

                if epoch % 10 == 0 {
                    info!(
                        "  Epoch {}/{}: train_loss={:.6}, val_loss={:.6}",
                        epoch + 1, self.epochs, last_train_loss, val_loss
                    );
                }

                if patience_counter >= self.early_stopping_patience {
                    info!("Early stopping at epoch {}", epoch + 1);
                    return Ok(TGGNMetrics {
                        val_loss: best_val_loss,
                        train_loss: last_train_loss,
                        directional_accuracy: 0.0,
                        epochs_completed: epoch + 1,
                        backtest_sharpe: None,
                        backtest_trades: None,
                    });
                }
            }

            Ok(TGGNMetrics {
                val_loss: best_val_loss,
                train_loss: last_train_loss,
                directional_accuracy: 0.0,
                epochs_completed: self.epochs,
                backtest_sharpe: None,
                backtest_trades: None,
            })
        }));

        let metrics = match training_result {
            Ok(Ok(m)) => m,
            Ok(Err(e)) => {
                warn!("TGGN training failed: {}", e);
                TGGNMetrics { val_loss: 1000.0, train_loss: 1000.0, directional_accuracy: 0.0, epochs_completed: 0, backtest_sharpe: None, backtest_trades: None }
            }
            Err(_) => {
                warn!("TGGN training panicked (likely OOM)");
                TGGNMetrics { val_loss: 1000.0, train_loss: 1000.0, directional_accuracy: 0.0, epochs_completed: 0, backtest_sharpe: None, backtest_trades: None }
            }
        };

        info!(
            "TGGN training completed: val_loss={:.6}, train_loss={:.6}, epochs={}",
            metrics.val_loss, metrics.train_loss, metrics.epochs_completed
        );

        let duration_secs = trial_start.elapsed().as_secs_f64();
        let trial_result = crate::hyperopt::traits::TrialResult {
            trial_num: current_trial,
            params,
            objective: Self::extract_objective(&metrics),
            duration_secs,
            metrics: None,
        };
        std::fs::create_dir_all(self.training_paths.hyperopt_dir()).ok();
        crate::hyperopt::shared_data::write_trial_result_json(
            &self.training_paths.hyperopt_dir(),
            &trial_result,
        ).ok();

        if self.device.is_cuda() {
            std::thread::sleep(std::time::Duration::from_millis(100));
        }

        Ok(metrics)
    }

    fn extract_objective(metrics: &Self::Metrics) -> f64 {
        if let (Some(sharpe), Some(trades)) = (metrics.backtest_sharpe, metrics.backtest_trades) {
            return crate::cuda_pipeline::signal_adapter::backtest_fitness(sharpe, trades, 30, 0.0);
        }
        metrics.val_loss
    }
}

#[cfg(test)]
mod tests {
    use super::*;

    #[test]
    fn test_bounds_count_matches_param_names() {
        let bounds = TGGNParams::continuous_bounds();
        let names = TGGNParams::param_names();
        assert_eq!(bounds.len(), names.len());
        assert_eq!(bounds.len(), NUM_PARAMS);
    }

    #[test]
    fn test_roundtrip_continuous() {
        let params = TGGNParams::default();
        let continuous = params.to_continuous();
        let restored = TGGNParams::from_continuous(&continuous).unwrap();
        assert!(
            (params.learning_rate - restored.learning_rate).abs() < 1e-6,
            "lr mismatch: {} vs {}",
            params.learning_rate,
            restored.learning_rate
        );
        assert_eq!(params.num_layers, restored.num_layers);
        assert_eq!(params.hidden_dim, restored.hidden_dim);
        assert_eq!(params.node_dim, restored.node_dim);
        assert_eq!(params.message_passing_steps, restored.message_passing_steps);
        assert!(
            (params.temporal_decay - restored.temporal_decay).abs() < 1e-6,
            "temporal_decay mismatch"
        );
        assert!((params.dropout - restored.dropout).abs() < 1e-6, "dropout mismatch");
        assert_eq!(params.batch_size, restored.batch_size);
        assert!(
            (params.weight_decay - restored.weight_decay).abs() / params.weight_decay < 1e-6,
            "weight_decay mismatch"
        );
        assert!(
            (params.grad_clip - restored.grad_clip).abs() < 1e-6,
            "grad_clip mismatch"
        );
    }

    #[test]
    fn test_from_continuous_wrong_length_errors() {
        let result = TGGNParams::from_continuous(&[0.1, 0.2]);
        assert!(result.is_err());
        let err_msg = format!("{}", result.unwrap_err());
        assert!(err_msg.contains("Expected 12"), "Error should mention expected count: {}", err_msg);
    }

    #[test]
    fn test_bounds_are_valid() {
        for (i, (min, max)) in TGGNParams::continuous_bounds().iter().enumerate() {
            assert!(
                min < max,
                "Invalid bounds for param {} ({}): {} >= {}",
                i,
                TGGNParams::param_names().get(i).copied().unwrap_or("?"),
                min,
                max
            );
        }
    }

    #[test]
    fn test_default_within_bounds() {
        let params = TGGNParams::default();
        let continuous = params.to_continuous();
        let bounds = TGGNParams::continuous_bounds();
        let names = TGGNParams::param_names();
        for (i, (val, (min, max))) in continuous.iter().zip(bounds.iter()).enumerate() {
            assert!(
                *val >= *min && *val <= *max,
                "Param {} ({}) = {} outside [{}, {}]",
                i,
                names.get(i).copied().unwrap_or("?"),
                val,
                min,
                max
            );
        }
    }

    #[test]
    fn test_metrics_default() {
        let metrics = TGGNMetrics::default();
        assert_eq!(metrics.val_loss, 0.0);
        assert_eq!(metrics.train_loss, 0.0);
        assert_eq!(metrics.directional_accuracy, 0.0);
        assert_eq!(metrics.epochs_completed, 0);
    }

    #[test]
    fn test_continuous_length() {
        let params = TGGNParams::default();
        let continuous = params.to_continuous();
        assert_eq!(continuous.len(), NUM_PARAMS);
    }
}
