#![allow(
    clippy::assertions_on_constants,
    clippy::assertions_on_result_states,
    clippy::clone_on_copy,
    clippy::decimal_literal_representation,
    clippy::doc_markdown,
    clippy::empty_line_after_doc_comments,
    clippy::field_reassign_with_default,
    clippy::get_unwrap,
    clippy::identity_op,
    clippy::inconsistent_digit_grouping,
    clippy::indexing_slicing,
    clippy::integer_division,
    clippy::len_zero,
    clippy::let_underscore_must_use,
    clippy::manual_div_ceil,
    clippy::manual_let_else,
    clippy::manual_range_contains,
    clippy::modulo_arithmetic,
    clippy::needless_range_loop,
    clippy::non_ascii_literal,
    clippy::redundant_clone,
    clippy::shadow_reuse,
    clippy::shadow_same,
    clippy::shadow_unrelated,
    clippy::single_match_else,
    clippy::str_to_string,
    clippy::string_slice,
    clippy::tests_outside_test_module,
    clippy::too_many_lines,
    clippy::unnecessary_wraps,
    clippy::unseparated_literal_suffix,
    clippy::use_debug,
    clippy::useless_vec,
    clippy::wildcard_enum_match_arm,
    clippy::else_if_without_else,
    clippy::expect_used,
    clippy::missing_const_for_fn,
    clippy::similar_names,
    clippy::type_complexity,
    clippy::collapsible_else_if,
    clippy::doc_lazy_continuation,
    clippy::items_after_test_module,
    clippy::map_clone,
    clippy::multiple_unsafe_ops_per_block,
    clippy::unwrap_or_default,
    clippy::assign_op_pattern,
    clippy::needless_borrow,
    clippy::println_empty_string,
    clippy::unnecessary_cast,
    clippy::used_underscore_binding,
    clippy::create_dir,
    clippy::implicit_saturating_sub,
    clippy::exit,
    clippy::expect_fun_call,
    clippy::too_many_arguments,
    clippy::unnecessary_map_or,
    clippy::unwrap_used,
    dead_code,
    unused_imports,
    unused_variables,
    clippy::cloned_ref_to_slice_refs,
    clippy::neg_multiply,
    clippy::while_let_loop,
    clippy::bool_assert_comparison,
    clippy::excessive_precision,
    clippy::trivially_copy_pass_by_ref,
    clippy::op_ref,
    clippy::redundant_closure,
    clippy::unnecessary_lazy_evaluations,
    clippy::if_then_some_else_none,
    clippy::unnecessary_to_owned,
    clippy::single_component_path_imports,
)]
//! Hyperopt Runner for Supervised Models on DBN Data
//!
//! Runs hyperparameter optimization using Particle Swarm Optimization (PSO) for
//! any of the 8 supervised models: TFT, Mamba2, Liquid, TGGN, TLOB, KAN, xLSTM,
//! and Diffusion. Uses the same --data-dir / --symbol interface as the RL hyperopt
//! and training binaries.
//!
//! ## Usage
//!
//! ```bash
//! # Run TFT hyperopt on ES futures
//! hyperopt_baseline_supervised --model tft --data-dir /data/ES.FUT --trials 20 --epochs 20
//!
//! # Run Mamba2 hyperopt
//! hyperopt_baseline_supervised --model mamba2 --data-dir /data/NQ.FUT --trials 20 --epochs 10
//!
//! # Run a specific model
//! hyperopt_baseline_supervised --model liquid --data-dir /data/ES.FUT
//!
//! # Run TFT + Mamba2 (backward-compatible alias)
//! hyperopt_baseline_supervised --model both --data-dir /data/ES.FUT
//!
//! # Run all 8 supervised models
//! hyperopt_baseline_supervised --model all --data-dir /data/ES.FUT
//! ```
//!
//! ## Output
//!
//! Results are written as JSON to `--output` (default: `ml/trained_models/hyperopt_results.json`).

#![allow(unused_crate_dependencies)]

use anyhow::{Context, Result};
use clap::Parser;
use serde_json::Value;
use std::path::PathBuf;
use std::time::Instant;
use tracing::{error, info, warn};

use common::metrics::{server as metrics_server, training_metrics};

use ml::hyperopt::adapters::diffusion::DiffusionTrainer;
use ml::hyperopt::adapters::kan::KANTrainer;
use ml::hyperopt::adapters::liquid::LiquidTrainer;
use ml::hyperopt::adapters::mamba2::Mamba2Trainer;
use ml::hyperopt::adapters::tft::TFTTrainer;
use ml::hyperopt::adapters::tggn::TGGNTrainer;
use ml::hyperopt::adapters::tlob::TLOBTrainer;
use ml::hyperopt::adapters::xlstm::XLSTMTrainer;
use ml::hyperopt::paths::{generate_run_id, TrainingPaths};
use ml::hyperopt::ArgminOptimizer;

/// Hyperparameter optimization runner for supervised models
#[derive(Parser, Debug)]
#[command(name = "hyperopt-baseline-supervised")]
#[command(about = "Run hyperparameter optimization for supervised models (tft, mamba2, liquid, tggn, tlob, kan, xlstm, diffusion, both, all)")]
struct Args {
    /// Model to optimize: "tft", "mamba2", "liquid", "tggn", "tlob", "kan",
    /// "xlstm", "diffusion", "both" (tft+mamba2), or "all" (all 8 models)
    #[arg(long, default_value = "both")]
    model: String,

    /// Directory containing .dbn.zst OHLCV data files (env: FOXHUNT_DATA_DIR)
    #[arg(long, env = "FOXHUNT_DATA_DIR")]
    data_dir: PathBuf,

    /// Number of PSO trials per model
    #[arg(long, default_value = "20")]
    trials: usize,

    /// Number of initial LHS (Latin Hypercube Sampling) samples
    #[arg(long, default_value = "5")]
    n_initial: usize,

    /// Training epochs per trial
    #[arg(long, default_value = "20")]
    epochs: usize,

    /// Output path for JSON results
    #[arg(long, default_value = "ml/trained_models/hyperopt_results.json")]
    output: PathBuf,

    /// Random seed for reproducibility
    #[arg(long, default_value = "42")]
    seed: u64,

    /// Base directory for training run outputs (checkpoints, logs, metrics)
    #[arg(long, default_value = "/tmp/ml_training")]
    base_dir: String,

    /// Early stopping patience (epochs without improvement)
    #[arg(long, default_value = "10")]
    early_stopping_patience: usize,
}

fn build_model_result(
    best_objective: f64,
    best_params_json: Value,
    num_trials: usize,
    elapsed_secs: f64,
) -> Value {
    serde_json::json!({
        "best_objective": best_objective,
        "best_params": best_params_json,
        "trials": num_trials,
        "elapsed_secs": elapsed_secs,
    })
}

#[allow(clippy::cognitive_complexity)]
fn run_tft_hyperopt(args: &Args) -> Result<Value> {
    info!("========================================");
    info!("  TFT Hyperparameter Optimization");
    info!("========================================");

    let run_id = generate_run_id("hyperopt-tft");
    let training_paths = TrainingPaths::new(&args.base_dir, "tft", &run_id);

    info!("Run ID: {}", run_id);
    info!("Data dir: {}", args.data_dir.display());
    info!("Epochs per trial: {}", args.epochs);
    info!("Trials: {}", args.trials);

    let mut trainer = TFTTrainer::new(&args.data_dir, args.epochs)
        .context("Failed to create TFT trainer")?
        .with_early_stopping(args.early_stopping_patience)
        .with_training_paths(training_paths);

    if let Err(e) = trainer.preload_data() {
        warn!("TFT data preload failed ({}), trials will load from disk", e);
    } else {
        info!("Training data preloaded for all {} trials", args.trials);
    }

    let optimizer = ArgminOptimizer::builder()
        .max_trials(args.trials)
        .n_initial(args.n_initial)
        .seed(args.seed)
        .model_name("tft")
        .build();

    training_metrics::set_hyperopt_trial("tft", 0.0, args.trials as f64);

    let start = Instant::now();
    let result = optimizer
        .optimize(trainer)
        .context("TFT hyperopt optimization failed")?;
    let elapsed = start.elapsed().as_secs_f64();

    training_metrics::set_hyperopt_trial("tft", result.all_trials.len() as f64, args.trials as f64);
    training_metrics::set_hyperopt_best_objective("tft", result.best_objective);
    training_metrics::set_hyperopt_elapsed("tft", elapsed);

    info!("TFT hyperopt complete:");
    info!("  Best objective: {:.6}", result.best_objective);
    info!("  Total trials: {}", result.all_trials.len());
    info!("  Elapsed: {:.1}s", elapsed);

    let best_params_json = serde_json::to_value(&result.best_params)
        .ok()
        .unwrap_or(Value::Null);

    Ok(build_model_result(
        result.best_objective,
        best_params_json,
        result.all_trials.len(),
        elapsed,
    ))
}

#[allow(clippy::cognitive_complexity)]
fn run_mamba2_hyperopt(args: &Args) -> Result<Value> {
    info!("========================================");
    info!("  Mamba2 Hyperparameter Optimization");
    info!("========================================");

    let run_id = generate_run_id("hyperopt-mamba2");
    let training_paths = TrainingPaths::new(&args.base_dir, "mamba2", &run_id);

    info!("Run ID: {}", run_id);
    info!("Data dir: {}", args.data_dir.display());
    info!("Epochs per trial: {}", args.epochs);
    info!("Trials: {}", args.trials);

    let mut trainer = Mamba2Trainer::new(&args.data_dir, args.epochs)
        .context("Failed to create Mamba2 trainer")?
        .with_training_paths(training_paths);

    if let Err(e) = trainer.preload_data() {
        warn!("Mamba2 data preload failed ({}), trials will load from disk", e);
    } else {
        info!("Training data preloaded for all {} trials", args.trials);
    }

    let optimizer = ArgminOptimizer::builder()
        .max_trials(args.trials)
        .n_initial(args.n_initial)
        .seed(args.seed)
        .model_name("mamba2")
        .build();

    training_metrics::set_hyperopt_trial("mamba2", 0.0, args.trials as f64);

    let start = Instant::now();
    let result = optimizer
        .optimize(trainer)
        .context("Mamba2 hyperopt optimization failed")?;
    let elapsed = start.elapsed().as_secs_f64();

    training_metrics::set_hyperopt_trial("mamba2", result.all_trials.len() as f64, args.trials as f64);
    training_metrics::set_hyperopt_best_objective("mamba2", result.best_objective);
    training_metrics::set_hyperopt_elapsed("mamba2", elapsed);

    info!("Mamba2 hyperopt complete:");
    info!("  Best objective: {:.6}", result.best_objective);
    info!("  Total trials: {}", result.all_trials.len());
    info!("  Elapsed: {:.1}s", elapsed);

    let best_params_json = serde_json::to_value(&result.best_params)
        .ok()
        .unwrap_or(Value::Null);

    Ok(build_model_result(
        result.best_objective,
        best_params_json,
        result.all_trials.len(),
        elapsed,
    ))
}

#[allow(clippy::cognitive_complexity)]
fn run_liquid_hyperopt(args: &Args) -> Result<Value> {
    info!("========================================");
    info!("  Liquid Hyperparameter Optimization");
    info!("========================================");

    let run_id = generate_run_id("hyperopt-liquid");
    let training_paths = TrainingPaths::new(&args.base_dir, "liquid", &run_id);

    info!("Run ID: {}", run_id);
    info!("Data dir: {}", args.data_dir.display());
    info!("Epochs per trial: {}", args.epochs);
    info!("Trials: {}", args.trials);

    let mut trainer = LiquidTrainer::new(&args.data_dir, args.epochs)
        .context("Failed to create Liquid trainer")?
        .with_early_stopping(args.early_stopping_patience)
        .with_training_paths(training_paths);

    if let Err(e) = trainer.preload_data() {
        warn!("Liquid data preload failed ({}), trials will load from disk", e);
    } else {
        info!("Training data preloaded for all {} trials", args.trials);
    }

    let optimizer = ArgminOptimizer::builder()
        .max_trials(args.trials)
        .n_initial(args.n_initial)
        .seed(args.seed)
        .model_name("liquid")
        .build();

    training_metrics::set_hyperopt_trial("liquid", 0.0, args.trials as f64);

    let start = Instant::now();
    let result = optimizer
        .optimize(trainer)
        .context("Liquid hyperopt optimization failed")?;
    let elapsed = start.elapsed().as_secs_f64();

    training_metrics::set_hyperopt_trial("liquid", result.all_trials.len() as f64, args.trials as f64);
    training_metrics::set_hyperopt_best_objective("liquid", result.best_objective);
    training_metrics::set_hyperopt_elapsed("liquid", elapsed);

    info!("Liquid hyperopt complete:");
    info!("  Best objective: {:.6}", result.best_objective);
    info!("  Total trials: {}", result.all_trials.len());
    info!("  Elapsed: {:.1}s", elapsed);

    let best_params_json = serde_json::to_value(&result.best_params)
        .ok()
        .unwrap_or(Value::Null);

    Ok(build_model_result(
        result.best_objective,
        best_params_json,
        result.all_trials.len(),
        elapsed,
    ))
}

#[allow(clippy::cognitive_complexity)]
fn run_tggn_hyperopt(args: &Args) -> Result<Value> {
    info!("========================================");
    info!("  TGGN Hyperparameter Optimization");
    info!("========================================");

    let run_id = generate_run_id("hyperopt-tggn");
    let training_paths = TrainingPaths::new(&args.base_dir, "tggn", &run_id);

    info!("Run ID: {}", run_id);
    info!("Data dir: {}", args.data_dir.display());
    info!("Epochs per trial: {}", args.epochs);
    info!("Trials: {}", args.trials);

    let mut trainer = TGGNTrainer::new(&args.data_dir, args.epochs)
        .context("Failed to create TGGN trainer")?
        .with_early_stopping(args.early_stopping_patience)
        .with_training_paths(training_paths);

    if let Err(e) = trainer.preload_data() {
        warn!("TGGN data preload failed ({}), trials will load from disk", e);
    } else {
        info!("Training data preloaded for all {} trials", args.trials);
    }

    let optimizer = ArgminOptimizer::builder()
        .max_trials(args.trials)
        .n_initial(args.n_initial)
        .seed(args.seed)
        .model_name("tggn")
        .build();

    training_metrics::set_hyperopt_trial("tggn", 0.0, args.trials as f64);

    let start = Instant::now();
    let result = optimizer
        .optimize(trainer)
        .context("TGGN hyperopt optimization failed")?;
    let elapsed = start.elapsed().as_secs_f64();

    training_metrics::set_hyperopt_trial("tggn", result.all_trials.len() as f64, args.trials as f64);
    training_metrics::set_hyperopt_best_objective("tggn", result.best_objective);
    training_metrics::set_hyperopt_elapsed("tggn", elapsed);

    info!("TGGN hyperopt complete:");
    info!("  Best objective: {:.6}", result.best_objective);
    info!("  Total trials: {}", result.all_trials.len());
    info!("  Elapsed: {:.1}s", elapsed);

    let best_params_json = serde_json::to_value(&result.best_params)
        .ok()
        .unwrap_or(Value::Null);

    Ok(build_model_result(
        result.best_objective,
        best_params_json,
        result.all_trials.len(),
        elapsed,
    ))
}

#[allow(clippy::cognitive_complexity)]
fn run_tlob_hyperopt(args: &Args) -> Result<Value> {
    info!("========================================");
    info!("  TLOB Hyperparameter Optimization");
    info!("========================================");

    let run_id = generate_run_id("hyperopt-tlob");
    let training_paths = TrainingPaths::new(&args.base_dir, "tlob", &run_id);

    info!("Run ID: {}", run_id);
    info!("Data dir: {}", args.data_dir.display());
    info!("Epochs per trial: {}", args.epochs);
    info!("Trials: {}", args.trials);

    let mut trainer = TLOBTrainer::new(&args.data_dir, args.epochs)
        .context("Failed to create TLOB trainer")?
        .with_early_stopping(args.early_stopping_patience)
        .with_training_paths(training_paths);

    if let Err(e) = trainer.preload_data() {
        warn!("TLOB data preload failed ({}), trials will load from disk", e);
    } else {
        info!("Training data preloaded for all {} trials", args.trials);
    }

    let optimizer = ArgminOptimizer::builder()
        .max_trials(args.trials)
        .n_initial(args.n_initial)
        .seed(args.seed)
        .model_name("tlob")
        .build();

    training_metrics::set_hyperopt_trial("tlob", 0.0, args.trials as f64);

    let start = Instant::now();
    let result = optimizer
        .optimize(trainer)
        .context("TLOB hyperopt optimization failed")?;
    let elapsed = start.elapsed().as_secs_f64();

    training_metrics::set_hyperopt_trial("tlob", result.all_trials.len() as f64, args.trials as f64);
    training_metrics::set_hyperopt_best_objective("tlob", result.best_objective);
    training_metrics::set_hyperopt_elapsed("tlob", elapsed);

    info!("TLOB hyperopt complete:");
    info!("  Best objective: {:.6}", result.best_objective);
    info!("  Total trials: {}", result.all_trials.len());
    info!("  Elapsed: {:.1}s", elapsed);

    let best_params_json = serde_json::to_value(&result.best_params)
        .ok()
        .unwrap_or(Value::Null);

    Ok(build_model_result(
        result.best_objective,
        best_params_json,
        result.all_trials.len(),
        elapsed,
    ))
}

#[allow(clippy::cognitive_complexity)]
fn run_kan_hyperopt(args: &Args) -> Result<Value> {
    info!("========================================");
    info!("  KAN Hyperparameter Optimization");
    info!("========================================");

    let run_id = generate_run_id("hyperopt-kan");
    let training_paths = TrainingPaths::new(&args.base_dir, "kan", &run_id);

    info!("Run ID: {}", run_id);
    info!("Data dir: {}", args.data_dir.display());
    info!("Epochs per trial: {}", args.epochs);
    info!("Trials: {}", args.trials);

    let mut trainer = KANTrainer::new(&args.data_dir, args.epochs)
        .context("Failed to create KAN trainer")?
        .with_early_stopping(args.early_stopping_patience)
        .with_training_paths(training_paths);

    if let Err(e) = trainer.preload_data() {
        warn!("KAN data preload failed ({}), trials will load from disk", e);
    } else {
        info!("Training data preloaded for all {} trials", args.trials);
    }

    let optimizer = ArgminOptimizer::builder()
        .max_trials(args.trials)
        .n_initial(args.n_initial)
        .seed(args.seed)
        .model_name("kan")
        .build();

    training_metrics::set_hyperopt_trial("kan", 0.0, args.trials as f64);

    let start = Instant::now();
    let result = optimizer
        .optimize(trainer)
        .context("KAN hyperopt optimization failed")?;
    let elapsed = start.elapsed().as_secs_f64();

    training_metrics::set_hyperopt_trial("kan", result.all_trials.len() as f64, args.trials as f64);
    training_metrics::set_hyperopt_best_objective("kan", result.best_objective);
    training_metrics::set_hyperopt_elapsed("kan", elapsed);

    info!("KAN hyperopt complete:");
    info!("  Best objective: {:.6}", result.best_objective);
    info!("  Total trials: {}", result.all_trials.len());
    info!("  Elapsed: {:.1}s", elapsed);

    let best_params_json = serde_json::to_value(&result.best_params)
        .ok()
        .unwrap_or(Value::Null);

    Ok(build_model_result(
        result.best_objective,
        best_params_json,
        result.all_trials.len(),
        elapsed,
    ))
}

#[allow(clippy::cognitive_complexity)]
fn run_xlstm_hyperopt(args: &Args) -> Result<Value> {
    info!("========================================");
    info!("  xLSTM Hyperparameter Optimization");
    info!("========================================");

    let run_id = generate_run_id("hyperopt-xlstm");
    let training_paths = TrainingPaths::new(&args.base_dir, "xlstm", &run_id);

    info!("Run ID: {}", run_id);
    info!("Data dir: {}", args.data_dir.display());
    info!("Epochs per trial: {}", args.epochs);
    info!("Trials: {}", args.trials);

    let mut trainer = XLSTMTrainer::new(&args.data_dir, args.epochs)
        .context("Failed to create xLSTM trainer")?
        .with_early_stopping(args.early_stopping_patience)
        .with_training_paths(training_paths);

    if let Err(e) = trainer.preload_data() {
        warn!("xLSTM data preload failed ({}), trials will load from disk", e);
    } else {
        info!("Training data preloaded for all {} trials", args.trials);
    }

    let optimizer = ArgminOptimizer::builder()
        .max_trials(args.trials)
        .n_initial(args.n_initial)
        .seed(args.seed)
        .model_name("xlstm")
        .build();

    training_metrics::set_hyperopt_trial("xlstm", 0.0, args.trials as f64);

    let start = Instant::now();
    let result = optimizer
        .optimize(trainer)
        .context("xLSTM hyperopt optimization failed")?;
    let elapsed = start.elapsed().as_secs_f64();

    training_metrics::set_hyperopt_trial("xlstm", result.all_trials.len() as f64, args.trials as f64);
    training_metrics::set_hyperopt_best_objective("xlstm", result.best_objective);
    training_metrics::set_hyperopt_elapsed("xlstm", elapsed);

    info!("xLSTM hyperopt complete:");
    info!("  Best objective: {:.6}", result.best_objective);
    info!("  Total trials: {}", result.all_trials.len());
    info!("  Elapsed: {:.1}s", elapsed);

    let best_params_json = serde_json::to_value(&result.best_params)
        .ok()
        .unwrap_or(Value::Null);

    Ok(build_model_result(
        result.best_objective,
        best_params_json,
        result.all_trials.len(),
        elapsed,
    ))
}

#[allow(clippy::cognitive_complexity)]
fn run_diffusion_hyperopt(args: &Args) -> Result<Value> {
    info!("========================================");
    info!("  Diffusion Hyperparameter Optimization");
    info!("========================================");

    let run_id = generate_run_id("hyperopt-diffusion");
    let training_paths = TrainingPaths::new(&args.base_dir, "diffusion", &run_id);

    info!("Run ID: {}", run_id);
    info!("Data dir: {}", args.data_dir.display());
    info!("Epochs per trial: {}", args.epochs);
    info!("Trials: {}", args.trials);

    let mut trainer = DiffusionTrainer::new(&args.data_dir, args.epochs)
        .context("Failed to create Diffusion trainer")?
        .with_early_stopping(args.early_stopping_patience)
        .with_training_paths(training_paths);

    if let Err(e) = trainer.preload_data() {
        warn!("Diffusion data preload failed ({}), trials will load from disk", e);
    } else {
        info!("Training data preloaded for all {} trials", args.trials);
    }

    let optimizer = ArgminOptimizer::builder()
        .max_trials(args.trials)
        .n_initial(args.n_initial)
        .seed(args.seed)
        .model_name("diffusion")
        .build();

    training_metrics::set_hyperopt_trial("diffusion", 0.0, args.trials as f64);

    let start = Instant::now();
    let result = optimizer
        .optimize(trainer)
        .context("Diffusion hyperopt optimization failed")?;
    let elapsed = start.elapsed().as_secs_f64();

    training_metrics::set_hyperopt_trial("diffusion", result.all_trials.len() as f64, args.trials as f64);
    training_metrics::set_hyperopt_best_objective("diffusion", result.best_objective);
    training_metrics::set_hyperopt_elapsed("diffusion", elapsed);

    info!("Diffusion hyperopt complete:");
    info!("  Best objective: {:.6}", result.best_objective);
    info!("  Total trials: {}", result.all_trials.len());
    info!("  Elapsed: {:.1}s", elapsed);

    let best_params_json = serde_json::to_value(&result.best_params)
        .ok()
        .unwrap_or(Value::Null);

    Ok(build_model_result(
        result.best_objective,
        best_params_json,
        result.all_trials.len(),
        elapsed,
    ))
}

const VALID_MODELS: &[&str] = &[
    "tft", "mamba2", "liquid", "tggn", "tlob", "kan", "xlstm", "diffusion",
];

#[allow(clippy::cognitive_complexity)]
fn main() -> Result<()> {
    // Initialize tracing with optional OTLP export to Tempo
    let otlp_endpoint = std::env::var("OTEL_EXPORTER_OTLP_ENDPOINT").ok();
    if let Err(e) = common::observability::init_observability(
        "hyperopt_baseline_supervised",
        otlp_endpoint.as_deref(),
    ) {
        eprintln!("Observability init failed (non-fatal): {e}");
    }

    training_metrics::init();
    metrics_server::start_metrics_server(9094);
    training_metrics::set_active_workers(1.0);

    let mut args = Args::parse();

    // Signal hyperopt mode active — will be cleared at exit
    let hyperopt_model_label = args.model.clone();
    training_metrics::set_hyperopt_mode(&hyperopt_model_label, true);

    info!("========================================");
    info!("  Hyperopt Baseline Supervised Runner");
    info!("========================================");
    info!("Model: {}", args.model);
    info!("Data dir: {}", args.data_dir.display());
    info!("Trials: {}", args.trials);
    info!("Initial LHS samples: {}", args.n_initial);
    info!("Epochs per trial: {}", args.epochs);
    info!("Output: {}", args.output.display());
    info!("Seed: {}", args.seed);

    if !args.data_dir.exists() {
        anyhow::bail!(
            "Data directory not found: {}. Provide a valid path via --data-dir.",
            args.data_dir.display()
        );
    }

    // Resolve model selection: "both" = tft+mamba2 (backward compat), "all" = all 8 models
    let models: Vec<&str> = if args.model == "both" {
        vec!["tft", "mamba2"]
    } else if args.model == "all" {
        VALID_MODELS.to_vec()
    } else if VALID_MODELS.contains(&args.model.as_str()) {
        vec![args.model.as_str()]
    } else {
        anyhow::bail!(
            "Invalid --model value '{}'. Must be one of: {}, 'both', or 'all'.",
            args.model,
            VALID_MODELS.join(", ")
        );
    };

    info!("Models to optimize: {:?}", models);

    // Enforce minimum 5 trials AND trials > n_initial
    // (ArgminOptimizer needs at least n_initial LHS rounds + 1 TPE-guided trial)
    const MIN_TRIALS: usize = 5;
    let effective_min = MIN_TRIALS.max(args.n_initial + 1);
    if args.trials < effective_min {
        info!(
            "Trials {} below minimum {} (floor={}, n_initial {}+1={}) — clamping",
            args.trials, effective_min, MIN_TRIALS, args.n_initial, args.n_initial + 1
        );
        args.trials = effective_min;
    }

    if let Some(parent) = args.output.parent() {
        std::fs::create_dir_all(parent)
            .with_context(|| format!("Failed to create output directory: {}", parent.display()))?;
    }

    let mut results = serde_json::Map::new();

    for model_name in &models {
        let result = match *model_name {
            "tft" => run_tft_hyperopt(&args),
            "mamba2" => run_mamba2_hyperopt(&args),
            "liquid" => run_liquid_hyperopt(&args),
            "tggn" => run_tggn_hyperopt(&args),
            "tlob" => run_tlob_hyperopt(&args),
            "kan" => run_kan_hyperopt(&args),
            "xlstm" => run_xlstm_hyperopt(&args),
            "diffusion" => run_diffusion_hyperopt(&args),
            other => {
                error!("Unknown model: {}", other);
                continue;
            }
        };

        match result {
            Ok(model_result) => {
                results.insert(model_name.to_string(), model_result);
            }
            Err(e) => {
                error!("{} hyperopt failed: {:#}", model_name, e);
                warn!("Continuing with remaining models...");
                results.insert(
                    model_name.to_string(),
                    serde_json::json!({ "error": format!("{:#}", e) }),
                );
            }
        }
    }

    let output_json = Value::Object(results);
    let output_str =
        serde_json::to_string_pretty(&output_json).context("Failed to serialize results")?;

    std::fs::write(&args.output, &output_str)
        .with_context(|| format!("Failed to write results to {}", args.output.display()))?;

    info!("========================================");
    info!("  Results saved to: {}", args.output.display());
    info!("========================================");
    info!("{}", output_str);

    training_metrics::set_hyperopt_mode(&hyperopt_model_label, false);
    training_metrics::set_active_workers(0.0);

    // Push final metrics to pushgateway so they persist after pod termination
    if let Err(e) = metrics_server::push_to_gateway(None, "hyperopt_baseline_supervised") {
        warn!("Failed to push metrics to gateway (non-fatal): {e}");
    }

    Ok(())
}
