//! Prometheus metrics for ML training binaries.
//!
//! Registers the 52 metrics expected by the Training Cockpit Grafana dashboard
//! and provides typed helper functions so callers never mis-spell metric names.
//!
//! # Usage
//!
//! ```rust,ignore
//! use common::metrics::training_metrics;
//! use common::metrics::server;
//!
//! training_metrics::init();
//! server::start_metrics_server(9094);
//! training_metrics::set_active_workers(1.0);
//! // … training loop …
//! training_metrics::set_epoch("kan", "0", 1.0);
//! training_metrics::set_epoch_loss("kan", "0", 0.0042);
//! ```

use super::{
    increment_counter_vec, observe_histogram_vec, register_counter_vec, register_gauge,
    register_gauge_vec, register_histogram_vec, set_gauge, set_gauge_vec,
};

use std::sync::OnceLock;

static VERBOSE: OnceLock<bool> = OnceLock::new();

/// Returns true if `FOXHUNT_VERBOSE_METRICS` env var is set.
pub fn verbose_enabled() -> bool {
    *VERBOSE.get_or_init(|| std::env::var("FOXHUNT_VERBOSE_METRICS").is_ok())
}

// ---------------------------------------------------------------------------
// Registration
// ---------------------------------------------------------------------------

/// Register all 52 training metrics with the global Prometheus registry.
///
/// Safe to call multiple times — the registry silently ignores duplicates.
pub fn init() {
    let mf: &[&str] = &["model", "fold"];
    let m: &[&str] = &["model"];

    // Gauges (model + fold)
    _ = register_gauge_vec("foxhunt_training_current_epoch", "Current epoch number", mf);
    _ = register_gauge_vec("foxhunt_training_epoch_loss", "Training loss at end of epoch", mf);
    _ = register_gauge_vec(
        "foxhunt_training_validation_loss",
        "Validation loss at end of epoch",
        mf,
    );
    _ = register_gauge_vec(
        "foxhunt_training_batches_per_second",
        "Training throughput in batches per second",
        mf,
    );
    _ = register_gauge_vec(
        "foxhunt_training_batches_processed",
        "Total batches processed this run",
        mf,
    );
    _ = register_gauge_vec(
        "foxhunt_training_iteration_seconds",
        "Last epoch wall time in seconds",
        mf,
    );
    _ = register_gauge_vec(
        "foxhunt_training_eval_accuracy",
        "Eval accuracy (supervised models)",
        mf,
    );
    _ = register_gauge_vec(
        "foxhunt_training_eval_precision",
        "Eval precision (supervised models)",
        mf,
    );
    _ = register_gauge_vec(
        "foxhunt_training_eval_recall",
        "Eval recall (supervised models)",
        mf,
    );
    _ = register_gauge_vec(
        "foxhunt_training_eval_f1",
        "Eval F1 score (supervised models)",
        mf,
    );
    _ = register_gauge_vec(
        "foxhunt_training_checkpoint_size_bytes",
        "Size of last checkpoint file in bytes",
        mf,
    );

    // Counters (model + fold)
    _ = register_counter_vec(
        "foxhunt_training_checkpoint_saves_total",
        "Successful checkpoint saves",
        mf,
    );
    _ = register_counter_vec(
        "foxhunt_training_checkpoint_failures_total",
        "Failed checkpoint saves",
        mf,
    );
    _ = register_counter_vec(
        "foxhunt_training_nan_detected_total",
        "NaN loss events detected",
        mf,
    );
    _ = register_counter_vec(
        "foxhunt_training_gradient_explosion_total",
        "Gradient explosion events",
        mf,
    );
    _ = register_counter_vec(
        "foxhunt_training_feature_errors_total",
        "Feature extraction failures",
        mf,
    );

    // Histograms (model only)
    _ = register_histogram_vec(
        "foxhunt_training_checkpoint_duration_seconds",
        "Checkpoint save latency",
        m,
        vec![0.1, 0.5, 1.0, 2.0, 5.0, 10.0, 30.0],
    );
    _ = register_histogram_vec(
        "foxhunt_training_data_load_seconds",
        "Data loading latency per fold",
        m,
        vec![0.5, 1.0, 5.0, 10.0, 30.0, 60.0, 120.0, 300.0],
    );

    // Special gauge (no labels)
    _ = register_gauge(
        "foxhunt_training_active_workers",
        "Number of active training workers",
    );

    // Step-level progress (updated every log_q_values / log_diagnostics call)
    _ = register_gauge_vec("foxhunt_training_step", "Current training step within epoch", mf);

    // RL diagnostic gauges (model + fold)
    _ = register_gauge_vec("foxhunt_training_q_value_mean", "Mean Q-value (updated per step)", mf);
    _ = register_gauge_vec("foxhunt_training_q_value_max", "Max Q-value (updated per step)", mf);
    _ = register_gauge_vec("foxhunt_training_q_overestimation_ratio", "Q-max / |Q-mean| ratio — spikes indicate overestimation (DQN)", mf);
    _ = register_gauge_vec("foxhunt_training_policy_entropy", "Policy entropy (PPO)", mf);
    _ = register_gauge_vec(
        "foxhunt_training_kl_divergence",
        "KL divergence old vs new policy (PPO)",
        mf,
    );
    _ = register_gauge_vec(
        "foxhunt_training_advantage_mean",
        "Mean advantage estimate (PPO)",
        mf,
    );
    _ = register_gauge_vec(
        "foxhunt_training_replay_buffer_size",
        "Replay buffer occupancy (DQN)",
        mf,
    );

    // Gradient & training health gauges (model + fold)
    _ = register_gauge_vec(
        "foxhunt_training_gradient_norm",
        "L2 gradient norm after backward",
        mf,
    );
    _ = register_gauge_vec("foxhunt_training_learning_rate", "Effective learning rate", mf);
    _ = register_gauge_vec(
        "foxhunt_training_epoch_duration_seconds",
        "Wall-clock time per epoch",
        mf,
    );

    // Exploration diagnostics (DQN action collapse monitoring)
    _ = register_gauge_vec(
        "foxhunt_training_action_entropy",
        "Normalized entropy of epoch action distribution (0=collapse, 1=uniform)",
        mf,
    );
    _ = register_gauge_vec(
        "foxhunt_training_action_diversity",
        "Fraction of 45 actions used above 0.5% threshold",
        mf,
    );

    // Epoch-level financial metrics (model + fold)
    _ = register_gauge_vec(
        "foxhunt_training_epoch_sharpe",
        "Epoch Sharpe ratio from validation backtest",
        mf,
    );
    _ = register_gauge_vec(
        "foxhunt_training_epoch_sortino",
        "Epoch Sortino ratio from validation backtest",
        mf,
    );
    _ = register_gauge_vec(
        "foxhunt_training_epoch_win_rate",
        "Epoch win rate 0-1",
        mf,
    );
    _ = register_gauge_vec(
        "foxhunt_training_epoch_max_drawdown",
        "Epoch max drawdown 0-1",
        mf,
    );
    _ = register_gauge_vec(
        "foxhunt_training_epoch_profit_factor",
        "Epoch profit factor (gross profit / gross loss)",
        mf,
    );
    _ = register_gauge_vec(
        "foxhunt_training_epoch_total_return",
        "Epoch total return (fractional)",
        mf,
    );
    _ = register_gauge_vec(
        "foxhunt_training_epoch_avg_return",
        "Epoch average return per trade",
        mf,
    );
    _ = register_gauge_vec(
        "foxhunt_training_epoch_total_trades",
        "Epoch total trade count",
        mf,
    );
    _ = register_gauge_vec(
        "foxhunt_training_epoch_action_buy_pct",
        "BUY action percentage 0-1",
        mf,
    );
    _ = register_gauge_vec(
        "foxhunt_training_epoch_action_sell_pct",
        "SELL action percentage 0-1",
        mf,
    );
    _ = register_gauge_vec(
        "foxhunt_training_epoch_action_hold_pct",
        "HOLD action percentage 0-1",
        mf,
    );

    // Hyperopt gauges (model only)
    _ = register_gauge_vec(
        "foxhunt_hyperopt_trial_current",
        "Current trial number (1-indexed)",
        m,
    );
    _ = register_gauge_vec(
        "foxhunt_hyperopt_trial_total",
        "Total trials configured",
        m,
    );
    _ = register_gauge_vec(
        "foxhunt_hyperopt_best_objective",
        "Best objective value so far (Sharpe)",
        m,
    );
    _ = register_gauge_vec(
        "foxhunt_hyperopt_mode",
        "1=hyperopt active, 0=training only",
        m,
    );

    // Hyperopt intra-trial gauges (model only)
    _ = register_gauge_vec(
        "foxhunt_hyperopt_trial_epoch",
        "Current epoch within active trial",
        m,
    );
    _ = register_gauge_vec(
        "foxhunt_hyperopt_trial_best_loss",
        "Best loss in current trial",
        m,
    );
    _ = register_gauge_vec(
        "foxhunt_hyperopt_elapsed_seconds",
        "Total wall-clock time since hyperopt start",
        m,
    );

    // Hyperopt counters (model only)
    _ = register_counter_vec(
        "foxhunt_hyperopt_trials_failed_total",
        "Failed trials count",
        m,
    );

    // Hyperopt histogram (model only)
    _ = register_histogram_vec(
        "foxhunt_hyperopt_trial_duration_seconds",
        "Duration of each trial",
        m,
        vec![5.0, 15.0, 30.0, 60.0, 120.0, 300.0, 600.0],
    );

    // Tier 2: Verbose metrics (only registered when FOXHUNT_VERBOSE_METRICS is set)
    if verbose_enabled() {
        let mfl: &[&str] = &["model", "fold", "layer"];
        _ = register_histogram_vec(
            "foxhunt_training_batch_loss",
            "Per-batch loss distribution",
            mf,
            vec![0.001, 0.01, 0.05, 0.1, 0.5, 1.0, 5.0, 10.0],
        );
        _ = register_histogram_vec(
            "foxhunt_training_gradient_norm_per_layer",
            "Per-layer gradient L2 norm",
            mfl,
            vec![0.0001, 0.001, 0.01, 0.1, 1.0, 10.0, 100.0],
        );
        _ = register_gauge_vec(
            "foxhunt_training_replay_priority_mean",
            "Mean PER priority",
            mf,
        );
        _ = register_gauge_vec(
            "foxhunt_training_advantage_std",
            "Advantage standard deviation (PPO)",
            mf,
        );
        _ = register_gauge_vec(
            "foxhunt_training_value_explained_variance",
            "Value function explained variance (PPO)",
            mf,
        );
    }
}

// ---------------------------------------------------------------------------
// Typed helpers
// ---------------------------------------------------------------------------

pub fn set_training_step(model: &str, fold: &str, step: f64) {
    set_gauge_vec("foxhunt_training_step", &[model, fold], step);
}

pub fn set_epoch(model: &str, fold: &str, epoch: f64) {
    set_gauge_vec("foxhunt_training_current_epoch", &[model, fold], epoch);
}

pub fn set_epoch_loss(model: &str, fold: &str, loss: f64) {
    set_gauge_vec("foxhunt_training_epoch_loss", &[model, fold], loss);
}

pub fn set_validation_loss(model: &str, fold: &str, loss: f64) {
    set_gauge_vec("foxhunt_training_validation_loss", &[model, fold], loss);
}

pub fn set_batches_per_second(model: &str, fold: &str, bps: f64) {
    set_gauge_vec("foxhunt_training_batches_per_second", &[model, fold], bps);
}

pub fn set_batches_processed(model: &str, fold: &str, total: f64) {
    set_gauge_vec("foxhunt_training_batches_processed", &[model, fold], total);
}

pub fn set_iteration_seconds(model: &str, fold: &str, secs: f64) {
    set_gauge_vec("foxhunt_training_iteration_seconds", &[model, fold], secs);
}

pub fn set_eval_metrics(
    model: &str,
    fold: &str,
    accuracy: f64,
    precision: f64,
    recall: f64,
    f1: f64,
) {
    set_gauge_vec("foxhunt_training_eval_accuracy", &[model, fold], accuracy);
    set_gauge_vec("foxhunt_training_eval_precision", &[model, fold], precision);
    set_gauge_vec("foxhunt_training_eval_recall", &[model, fold], recall);
    set_gauge_vec("foxhunt_training_eval_f1", &[model, fold], f1);
}

pub fn record_checkpoint_save(model: &str, fold: &str, duration_secs: f64, size_bytes: f64) {
    increment_counter_vec(
        "foxhunt_training_checkpoint_saves_total",
        &[model, fold],
        1.0,
    );
    set_gauge_vec(
        "foxhunt_training_checkpoint_size_bytes",
        &[model, fold],
        size_bytes,
    );
    observe_histogram_vec(
        "foxhunt_training_checkpoint_duration_seconds",
        &[model],
        duration_secs,
    );
}

pub fn record_checkpoint_failure(model: &str, fold: &str) {
    increment_counter_vec(
        "foxhunt_training_checkpoint_failures_total",
        &[model, fold],
        1.0,
    );
}

pub fn record_nan_detected(model: &str, fold: &str) {
    increment_counter_vec(
        "foxhunt_training_nan_detected_total",
        &[model, fold],
        1.0,
    );
}

pub fn record_gradient_explosion(model: &str, fold: &str) {
    increment_counter_vec(
        "foxhunt_training_gradient_explosion_total",
        &[model, fold],
        1.0,
    );
}

pub fn record_feature_error(model: &str, fold: &str) {
    increment_counter_vec(
        "foxhunt_training_feature_errors_total",
        &[model, fold],
        1.0,
    );
}

pub fn record_data_load(model: &str, duration_secs: f64) {
    observe_histogram_vec(
        "foxhunt_training_data_load_seconds",
        &[model],
        duration_secs,
    );
}

pub fn set_active_workers(value: f64) {
    set_gauge("foxhunt_training_active_workers", value);
}

pub fn set_hyperopt_trial(model: &str, current: f64, total: f64) {
    set_gauge_vec("foxhunt_hyperopt_trial_current", &[model], current);
    set_gauge_vec("foxhunt_hyperopt_trial_total", &[model], total);
}

pub fn set_hyperopt_best_objective(model: &str, value: f64) {
    set_gauge_vec("foxhunt_hyperopt_best_objective", &[model], value);
}

pub fn record_hyperopt_trial_duration(model: &str, secs: f64) {
    observe_histogram_vec("foxhunt_hyperopt_trial_duration_seconds", &[model], secs);
}

pub fn record_hyperopt_trial_failure(model: &str) {
    increment_counter_vec("foxhunt_hyperopt_trials_failed_total", &[model], 1.0);
}

pub fn set_hyperopt_mode(model: &str, active: bool) {
    set_gauge_vec(
        "foxhunt_hyperopt_mode",
        &[model],
        if active { 1.0 } else { 0.0 },
    );
}

// ---------------------------------------------------------------------------
// Tier 1: RL diagnostics
// ---------------------------------------------------------------------------

pub fn set_q_value_stats(model: &str, fold: &str, mean: f64, max: f64) {
    set_gauge_vec("foxhunt_training_q_value_mean", &[model, fold], mean);
    set_gauge_vec("foxhunt_training_q_value_max", &[model, fold], max);
    // Q-value overestimation ratio: q_max / |q_mean| — spikes indicate DQN overestimation
    let ratio = if mean.abs() > 1e-8 { max / mean.abs() } else { 0.0 };
    set_gauge_vec("foxhunt_training_q_overestimation_ratio", &[model, fold], ratio);
    if ratio > 10.0 || mean > 5.0 {
        tracing::warn!(
            "Q-value overestimation alert: model={model} q_mean={mean:.3} q_max={max:.3} ratio={ratio:.1}"
        );
    }
}

pub fn set_policy_entropy(model: &str, fold: &str, entropy: f64) {
    set_gauge_vec("foxhunt_training_policy_entropy", &[model, fold], entropy);
}

pub fn set_kl_divergence(model: &str, fold: &str, kl: f64) {
    set_gauge_vec("foxhunt_training_kl_divergence", &[model, fold], kl);
}

pub fn set_advantage_mean(model: &str, fold: &str, mean: f64) {
    set_gauge_vec("foxhunt_training_advantage_mean", &[model, fold], mean);
}

pub fn set_replay_buffer_size(model: &str, fold: &str, size: f64) {
    set_gauge_vec("foxhunt_training_replay_buffer_size", &[model, fold], size);
}

// ---------------------------------------------------------------------------
// Tier 1: Gradient & training health
// ---------------------------------------------------------------------------

pub fn set_gradient_norm(model: &str, fold: &str, norm: f64) {
    set_gauge_vec("foxhunt_training_gradient_norm", &[model, fold], norm);
}

pub fn set_learning_rate(model: &str, fold: &str, lr: f64) {
    set_gauge_vec("foxhunt_training_learning_rate", &[model, fold], lr);
}

pub fn set_epoch_duration(model: &str, fold: &str, secs: f64) {
    set_gauge_vec("foxhunt_training_epoch_duration_seconds", &[model, fold], secs);
}

pub fn set_action_entropy(model: &str, fold: &str, entropy: f64) {
    set_gauge_vec("foxhunt_training_action_entropy", &[model, fold], entropy);
}

pub fn set_action_diversity(model: &str, fold: &str, diversity: f64) {
    set_gauge_vec("foxhunt_training_action_diversity", &[model, fold], diversity);
}

// ---------------------------------------------------------------------------
// Tier 1: Epoch financial metrics
// ---------------------------------------------------------------------------

/// Push a full set of epoch-level financial metrics from a backtest evaluation.
#[allow(clippy::too_many_arguments)]
pub fn set_epoch_financial_metrics(
    model: &str,
    fold: &str,
    sharpe: f64,
    sortino: f64,
    win_rate: f64,
    max_drawdown: f64,
    profit_factor: f64,
    total_return: f64,
    avg_return: f64,
    total_trades: f64,
) {
    set_gauge_vec("foxhunt_training_epoch_sharpe", &[model, fold], sharpe);
    set_gauge_vec("foxhunt_training_epoch_sortino", &[model, fold], sortino);
    set_gauge_vec("foxhunt_training_epoch_win_rate", &[model, fold], win_rate);
    set_gauge_vec("foxhunt_training_epoch_max_drawdown", &[model, fold], max_drawdown);
    set_gauge_vec("foxhunt_training_epoch_profit_factor", &[model, fold], profit_factor);
    set_gauge_vec("foxhunt_training_epoch_total_return", &[model, fold], total_return);
    set_gauge_vec("foxhunt_training_epoch_avg_return", &[model, fold], avg_return);
    set_gauge_vec("foxhunt_training_epoch_total_trades", &[model, fold], total_trades);
}

/// Push epoch action distribution percentages (all 0-1).
pub fn set_epoch_action_distribution(
    model: &str,
    fold: &str,
    buy_pct: f64,
    sell_pct: f64,
    hold_pct: f64,
) {
    set_gauge_vec("foxhunt_training_epoch_action_buy_pct", &[model, fold], buy_pct);
    set_gauge_vec("foxhunt_training_epoch_action_sell_pct", &[model, fold], sell_pct);
    set_gauge_vec("foxhunt_training_epoch_action_hold_pct", &[model, fold], hold_pct);
}

// ---------------------------------------------------------------------------
// Tier 1: Hyperopt intra-trial
// ---------------------------------------------------------------------------

pub fn set_hyperopt_trial_epoch(model: &str, epoch: f64) {
    set_gauge_vec("foxhunt_hyperopt_trial_epoch", &[model], epoch);
}

pub fn set_hyperopt_trial_best_loss(model: &str, loss: f64) {
    set_gauge_vec("foxhunt_hyperopt_trial_best_loss", &[model], loss);
}

pub fn set_hyperopt_elapsed(model: &str, secs: f64) {
    set_gauge_vec("foxhunt_hyperopt_elapsed_seconds", &[model], secs);
}

/// Push per-trial backtest metrics to Prometheus (Rust-side only, no CUDA).
#[allow(clippy::too_many_arguments)]
pub fn set_hyperopt_backtest_metrics(
    model: &str,
    sharpe: f64,
    sortino: f64,
    max_drawdown_pct: f64,
    win_rate: f64,
    total_trades: f64,
    total_return_pct: f64,
    calmar: f64,
    omega: f64,
) {
    set_gauge_vec("foxhunt_hyperopt_best_sharpe", &[model], sharpe);
    set_gauge_vec("foxhunt_hyperopt_best_sortino", &[model], sortino);
    set_gauge_vec("foxhunt_hyperopt_best_max_drawdown_pct", &[model], max_drawdown_pct);
    set_gauge_vec("foxhunt_hyperopt_best_win_rate", &[model], win_rate);
    set_gauge_vec("foxhunt_hyperopt_best_total_trades", &[model], total_trades);
    set_gauge_vec("foxhunt_hyperopt_best_total_return_pct", &[model], total_return_pct);
    set_gauge_vec("foxhunt_hyperopt_best_calmar", &[model], calmar);
    set_gauge_vec("foxhunt_hyperopt_best_omega", &[model], omega);
}

// ---------------------------------------------------------------------------
// Tier 2: Verbose metrics (no-op when FOXHUNT_VERBOSE_METRICS not set)
// ---------------------------------------------------------------------------

pub fn observe_batch_loss(model: &str, fold: &str, loss: f64) {
    if verbose_enabled() {
        observe_histogram_vec("foxhunt_training_batch_loss", &[model, fold], loss);
    }
}

pub fn observe_gradient_norm_per_layer(model: &str, fold: &str, layer: &str, norm: f64) {
    if verbose_enabled() {
        observe_histogram_vec(
            "foxhunt_training_gradient_norm_per_layer",
            &[model, fold, layer],
            norm,
        );
    }
}

pub fn set_replay_priority_mean(model: &str, fold: &str, mean: f64) {
    if verbose_enabled() {
        set_gauge_vec("foxhunt_training_replay_priority_mean", &[model, fold], mean);
    }
}

pub fn set_advantage_std(model: &str, fold: &str, std: f64) {
    if verbose_enabled() {
        set_gauge_vec("foxhunt_training_advantage_std", &[model, fold], std);
    }
}

pub fn set_value_explained_variance(model: &str, fold: &str, ev: f64) {
    if verbose_enabled() {
        set_gauge_vec(
            "foxhunt_training_value_explained_variance",
            &[model, fold],
            ev,
        );
    }
}

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

    #[test]
    fn test_init_is_idempotent() {
        init();
        init(); // calling twice must not panic
    }

    #[test]
    fn test_set_hyperopt_trial() {
        init();
        set_hyperopt_trial("dqn", 5.0, 20.0);
        // No panic = success (gauge_vec values not directly readable without label matching)
    }

    #[test]
    fn test_set_hyperopt_mode() {
        init();
        set_hyperopt_mode("dqn", true);
        set_hyperopt_mode("dqn", false);
    }

    #[test]
    fn test_record_hyperopt_trial_failure() {
        init();
        record_hyperopt_trial_failure("ppo");
    }

    #[test]
    fn test_set_hyperopt_best_objective() {
        init();
        set_hyperopt_best_objective("tft", 2.34);
    }

    #[test]
    fn test_record_hyperopt_trial_duration() {
        init();
        record_hyperopt_trial_duration("dqn", 45.3);
    }

    #[test]
    fn test_verbose_disabled_by_default() {
        init();
        observe_batch_loss("dqn", "0", 0.05);
        observe_gradient_norm_per_layer("dqn", "0", "fc1", 0.01);
        set_replay_priority_mean("dqn", "0", 1.5);
        set_advantage_std("ppo", "0", 0.8);
        set_value_explained_variance("ppo", "0", 0.7);
    }

    #[test]
    fn test_tier1_helpers_no_panic() {
        init();
        set_q_value_stats("dqn", "0", 12.3, 45.6);
        set_policy_entropy("ppo", "0", 1.23);
        set_kl_divergence("ppo", "0", 0.008);
        set_advantage_mean("ppo", "0", 0.001);
        set_replay_buffer_size("dqn", "0", 50000.0);
        set_gradient_norm("dqn", "0", 1.23);
        set_learning_rate("dqn", "0", 0.001);
        set_epoch_duration("dqn", "0", 12.5);
        set_hyperopt_trial_epoch("dqn", 5.0);
        set_hyperopt_trial_best_loss("dqn", 0.042);
        set_hyperopt_elapsed("dqn", 123.4);
    }

    #[test]
    fn test_epoch_financial_metrics_no_panic() {
        init();
        set_epoch_financial_metrics("dqn", "0", 2.31, 3.12, 0.55, 0.08, 1.84, 0.124, 0.003, 142.0);
        set_epoch_action_distribution("dqn", "0", 0.35, 0.25, 0.40);
    }
}
