#![allow(clippy::indexing_slicing)] // ratatui Layout::split indices match constraints
#![allow(clippy::integer_division)] // intentional truncation for time formatting
//! Training cockpit -- production ML training session dashboard.
//!
//! Sub-tab bar: **Live** | **History**.  Tab/Shift-Tab switches.
//! Live tab: selectable sessions table (j/k), expandable detail panel (Enter/Right),
//! resource gauges (CPU + GPU), epoch financial metrics, health indicators.
//! History tab: completed/failed runs from ListTrainingJobs.

use ratatui::Frame;
use ratatui::layout::{Constraint, Direction, Layout, Rect};
use ratatui::style::{Modifier, Style};
use ratatui::text::{Line, Span};
use ratatui::widgets::{Gauge, Paragraph, Row, Table};

use crate::tui::cockpit::Cockpit;
use crate::tui::state::{
    AppState, EpochSnapshot, TrainingHistoryEntry, TrainingSessionData, TrainingTab,
};
use crate::tui::theme;

pub struct TrainingCockpit;

impl Cockpit for TrainingCockpit {
    fn name(&self) -> &str {
        "Training"
    }

    fn render(&self, frame: &mut Frame, area: Rect, state: &AppState) {
        // Split: 1-line sub-tab bar + remaining body
        let outer = Layout::default()
            .direction(Direction::Vertical)
            .constraints([Constraint::Length(1), Constraint::Min(0)])
            .split(area);

        render_sub_tab_bar(frame, outer[0], state);

        match state.training_tab {
            TrainingTab::Live => render_live_tab(frame, outer[1], state),
            TrainingTab::History => render_history_tab(frame, outer[1], state),
        }
    }
}

// ---------------------------------------------------------------------------
// Sub-tab bar
// ---------------------------------------------------------------------------

fn render_sub_tab_bar(frame: &mut Frame, area: Rect, state: &AppState) {
    let live_style = if state.training_tab == TrainingTab::Live {
        Style::default()
            .fg(theme::HIGHLIGHT)
            .add_modifier(Modifier::BOLD)
    } else {
        theme::muted_style()
    };
    let history_style = if state.training_tab == TrainingTab::History {
        Style::default()
            .fg(theme::HIGHLIGHT)
            .add_modifier(Modifier::BOLD)
    } else {
        theme::muted_style()
    };

    let live_label = if state.training_tab == TrainingTab::Live {
        "[Live]"
    } else {
        " Live "
    };
    let hist_label = if state.training_tab == TrainingTab::History {
        "[History]"
    } else {
        " History "
    };

    let line = Line::from(vec![
        Span::styled(" ", theme::muted_style()),
        Span::styled(live_label, live_style),
        Span::styled("  ", theme::muted_style()),
        Span::styled(hist_label, history_style),
        Span::styled("    Tab: switch", theme::muted_style()),
    ]);
    frame.render_widget(Paragraph::new(line), area);
}

// ---------------------------------------------------------------------------
// Live tab (existing layout)
// ---------------------------------------------------------------------------

fn render_live_tab(frame: &mut Frame, area: Rect, state: &AppState) {
    if state.training_detail_expanded {
        // 3-row layout: sessions table | resource+metrics | detail panel
        let rows = Layout::default()
            .direction(Direction::Vertical)
            .constraints([
                Constraint::Percentage(30),
                Constraint::Percentage(30),
                Constraint::Percentage(40),
            ])
            .split(area);

        render_sessions_table(frame, rows[0], state);

        let mid = Layout::default()
            .direction(Direction::Horizontal)
            .constraints([
                Constraint::Length(24),
                Constraint::Length(24),
                Constraint::Min(30),
            ])
            .split(rows[1]);

        render_gpu_panel(frame, mid[0], state);
        render_cpu_panel(frame, mid[1], state);
        render_epoch_metrics(frame, mid[2], state);
        render_detail_panel(frame, rows[2], state);
    } else {
        // 2-row layout: sessions table | resource+metrics
        let rows = Layout::default()
            .direction(Direction::Vertical)
            .constraints([Constraint::Percentage(50), Constraint::Percentage(50)])
            .split(area);

        render_sessions_table(frame, rows[0], state);

        let bottom = Layout::default()
            .direction(Direction::Horizontal)
            .constraints([
                Constraint::Length(24),
                Constraint::Length(24),
                Constraint::Min(30),
            ])
            .split(rows[1]);

        render_gpu_panel(frame, bottom[0], state);
        render_cpu_panel(frame, bottom[1], state);
        render_epoch_metrics(frame, bottom[2], state);
    }
}

// ---------------------------------------------------------------------------
// History tab
// ---------------------------------------------------------------------------

fn render_history_tab(frame: &mut Frame, area: Rect, state: &AppState) {
    // 2-row layout: history table | resource panels
    let rows = Layout::default()
        .direction(Direction::Vertical)
        .constraints([Constraint::Percentage(60), Constraint::Percentage(40)])
        .split(area);

    render_history_table(frame, rows[0], state);

    let bottom = Layout::default()
        .direction(Direction::Horizontal)
        .constraints([
            Constraint::Length(24),
            Constraint::Length(24),
            Constraint::Min(30),
        ])
        .split(rows[1]);

    render_gpu_panel(frame, bottom[0], state);
    render_cpu_panel(frame, bottom[1], state);
    render_history_detail(frame, bottom[2], state);
}

fn render_history_table(frame: &mut Frame, area: Rect, state: &AppState) {
    let header = Row::new(vec![
        " ", "Model", "Status", "Loss", "Best Val", "Created", "Duration",
    ])
    .style(theme::header_style());

    let selected = state.selected_history_entry;

    let rows: Vec<Row> = state
        .training_history
        .iter()
        .enumerate()
        .map(|(i, h)| {
            let is_selected = selected == Some(i);
            let cursor = if is_selected { "\u{25b6}" } else { " " };

            let base_style = if is_selected {
                Style::default()
                    .fg(theme::HIGHLIGHT)
                    .add_modifier(Modifier::BOLD)
            } else {
                Style::default().fg(theme::TEXT)
            };

            let status_style = match h.status.as_str() {
                "completed" => Style::default().fg(theme::SUCCESS),
                "failed" => Style::default().fg(theme::ERROR),
                "stopped" => Style::default().fg(theme::WARNING),
                "running" => Style::default().fg(theme::SECONDARY),
                _ => theme::muted_style(),
            };

            Row::new(vec![
                Span::styled(cursor.to_owned(), Style::default().fg(theme::SECONDARY)),
                Span::styled(h.model.clone(), base_style),
                Span::styled(h.status.clone(), status_style),
                Span::styled(format!("{:.4}", h.final_loss), base_style),
                Span::styled(format!("{:.4}", h.best_val), base_style),
                Span::styled(format_timestamp(h.created_at), theme::muted_style()),
                Span::styled(format_job_duration(h), theme::muted_style()),
            ])
        })
        .collect();

    let title = format!(
        " Training History \u{2014} {} run{} ",
        state.training_history.len(),
        if state.training_history.len() == 1 {
            ""
        } else {
            "s"
        }
    );

    let table = Table::new(
        rows,
        [
            Constraint::Length(2),  // cursor
            Constraint::Length(12), // model
            Constraint::Length(10), // status
            Constraint::Length(10), // loss
            Constraint::Length(10), // best val
            Constraint::Length(18), // created
            Constraint::Min(10),   // duration
        ],
    )
    .header(header)
    .block(theme::block(&title));

    frame.render_widget(table, area);
}

fn render_history_detail(frame: &mut Frame, area: Rect, state: &AppState) {
    let entry = state
        .selected_history_entry
        .and_then(|i| state.training_history.get(i));

    let Some(h) = entry else {
        let msg = Paragraph::new(Line::from(Span::styled(
            "Select a run (j/k) to see details",
            theme::muted_style(),
        )))
        .block(theme::block(" Run Detail "));
        frame.render_widget(msg, area);
        return;
    };

    let title = format!(" {} \u{2014} {} ", h.model, h.status);
    let block = theme::block(&title);
    let inner = block.inner(area);
    frame.render_widget(block, area);

    let mut lines = vec![
        Line::from(vec![
            Span::styled(" Job ID: ", theme::muted_style()),
            Span::styled(&h.job_id, Style::default().fg(theme::TEXT)),
        ]),
        Line::from(vec![
            Span::styled(" Loss:   ", theme::muted_style()),
            Span::styled(
                format!("{:.6}", h.final_loss),
                Style::default().fg(theme::TEXT),
            ),
        ]),
        Line::from(vec![
            Span::styled(" Val:    ", theme::muted_style()),
            Span::styled(
                format!("{:.6}", h.best_val),
                Style::default().fg(theme::HIGHLIGHT),
            ),
        ]),
        Line::from(vec![
            Span::styled(" Created:", theme::muted_style()),
            Span::styled(
                format!(" {}", format_timestamp(h.created_at)),
                Style::default().fg(theme::TEXT),
            ),
        ]),
        Line::from(vec![
            Span::styled(" Duration:", theme::muted_style()),
            Span::styled(
                format!(" {}", format_job_duration(h)),
                Style::default().fg(theme::TEXT),
            ),
        ]),
    ];

    if !h.description.is_empty() {
        lines.push(Line::from(vec![
            Span::styled(" Desc:   ", theme::muted_style()),
            Span::styled(&h.description, Style::default().fg(theme::TEXT)),
        ]));
    }

    frame.render_widget(Paragraph::new(lines), inner);
}

// ---------------------------------------------------------------------------
// Formatting helpers
// ---------------------------------------------------------------------------

fn format_duration(secs: f64) -> String {
    if secs <= 0.0 {
        return "\u{2014}".to_owned();
    }
    let total = secs as u64;
    let h = total / 3600;
    let m = (total % 3600) / 60;
    if h > 0 {
        format!("{h}h{m:02}m")
    } else {
        format!("{m}m")
    }
}

fn format_timestamp(unix_secs: i64) -> String {
    if unix_secs <= 0 {
        return "\u{2014}".to_owned();
    }
    // Cast to unsigned — timestamps are always positive here.
    let ts = unix_secs as u64;
    let secs_of_day = ts % 86400;
    let hh = secs_of_day / 3600;
    let mm = (secs_of_day % 3600) / 60;
    let days = ts / 86400;
    // Approximate date (good enough for display — no chrono dependency)
    let y = 1970 + days / 365;
    let doy = days % 365;
    let m = doy / 30 + 1;
    let d = doy % 30 + 1;
    format!("{y:04}-{m:02}-{d:02} {hh:02}:{mm:02}")
}

fn format_job_duration(h: &TrainingHistoryEntry) -> String {
    if h.completed_at == 0 || h.started_at == 0 {
        return "\u{2014}".to_owned();
    }
    let secs = h.completed_at.saturating_sub(h.started_at);
    if secs <= 0 {
        return "\u{2014}".to_owned();
    }
    let total = secs as u64;
    let hours = total / 3600;
    let mins = (total % 3600) / 60;
    let s = total % 60;
    if hours > 0 {
        format!("{hours}h{mins:02}m")
    } else if mins > 0 {
        format!("{mins}m{s:02}s")
    } else {
        format!("{s}s")
    }
}

fn health_indicator(s: &TrainingSessionData) -> (String, Style) {
    let issues = s.nan_detected + s.gradient_explosions + s.feature_errors;
    if issues == 0 {
        ("\u{25cf} OK".to_owned(), Style::default().fg(theme::SUCCESS))
    } else {
        let mut parts = Vec::new();
        if s.nan_detected > 0 {
            parts.push(format!("NaN({})", s.nan_detected));
        }
        if s.gradient_explosions > 0 {
            parts.push(format!("Grd({})", s.gradient_explosions));
        }
        if s.feature_errors > 0 {
            parts.push(format!("Feat({})", s.feature_errors));
        }
        let label = format!("\u{25b2} {}", parts.join(","));
        (label, Style::default().fg(theme::WARNING))
    }
}

// ---------------------------------------------------------------------------
// Sessions table (selectable)
// ---------------------------------------------------------------------------

fn render_sessions_table(frame: &mut Frame, area: Rect, state: &AppState) {
    let header = Row::new(vec![
        " ", "Model", "Fold", "Epoch", "Loss", "Val Loss", "Batch/s", "Duration", "Health",
    ])
    .style(theme::header_style());

    let selected = state.selected_training_session;

    let rows: Vec<Row> = state
        .training_sessions
        .iter()
        .enumerate()
        .map(|(i, s)| {
            let is_selected = selected == Some(i);
            let cursor = if is_selected { "\u{25b6}" } else { " " };
            let (health_text, health_style) = health_indicator(s);

            let base_style = if is_selected {
                Style::default()
                    .fg(theme::HIGHLIGHT)
                    .add_modifier(Modifier::BOLD)
            } else {
                Style::default().fg(theme::TEXT)
            };

            let tag = if s.is_hyperopt { "HO" } else { "" };
            let model_label = if tag.is_empty() {
                s.model.clone()
            } else {
                format!("{} [{}]", s.model, tag)
            };

            Row::new(vec![
                Span::styled(cursor.to_owned(), Style::default().fg(theme::SECONDARY)),
                Span::styled(model_label, base_style),
                Span::styled(s.fold.clone(), base_style),
                Span::styled(s.epoch.to_string(), base_style),
                Span::styled(format!("{:.4}", s.loss), base_style),
                Span::styled(format!("{:.4}", s.val_loss), base_style),
                Span::styled(format!("{:.1}", s.batches_per_sec), base_style),
                Span::styled(
                    format_duration(s.epoch_duration_secs * f64::from(s.epoch)),
                    base_style,
                ),
                Span::styled(health_text, health_style),
            ])
        })
        .collect();

    let block_title = if state.active_jobs > 0 {
        format!(
            " Training Sessions \u{2014} {} K8s job{} ",
            state.active_jobs,
            if state.active_jobs == 1 { "" } else { "s" }
        )
    } else {
        " Training Sessions ".to_owned()
    };

    let table = Table::new(
        rows,
        [
            Constraint::Length(2),  // cursor
            Constraint::Length(14), // model
            Constraint::Length(8),  // fold
            Constraint::Length(7),  // epoch
            Constraint::Length(10), // loss
            Constraint::Length(10), // val loss
            Constraint::Length(9),  // batch/s
            Constraint::Length(10), // duration
            Constraint::Min(12),   // health
        ],
    )
    .header(header)
    .block(theme::block(&block_title));

    frame.render_widget(table, area);
}

// ---------------------------------------------------------------------------
// GPU panel
// ---------------------------------------------------------------------------

fn render_gpu_panel(frame: &mut Frame, area: Rect, state: &AppState) {
    let block = theme::block(" GPU ");
    let inner = block.inner(area);
    frame.render_widget(block, area);

    let chunks = Layout::default()
        .direction(Direction::Vertical)
        .constraints([
            Constraint::Length(1), // GPU name
            Constraint::Length(2), // utilization gauge
            Constraint::Length(2), // VRAM gauge
            Constraint::Length(1), // temp + power
            Constraint::Min(0),
        ])
        .split(inner);

    // GPU name
    let name_line = Line::from(vec![Span::styled(
        &state.gpu.name,
        Style::default()
            .fg(theme::HIGHLIGHT)
            .add_modifier(Modifier::BOLD),
    )]);
    frame.render_widget(Paragraph::new(name_line), chunks[0]);

    // GPU utilization
    let util_pct = state.gpu.utilization / 100.0;
    let gpu_gauge = Gauge::default()
        .gauge_style(Style::default().fg(theme::SECONDARY))
        .label(format!("{:.0}%", state.gpu.utilization))
        .ratio(util_pct.clamp(0.0, 1.0));
    frame.render_widget(gpu_gauge, chunks[1]);

    // VRAM
    let mem_pct = if state.gpu.memory_total_gb > 0.0 {
        state.gpu.memory_used_gb / state.gpu.memory_total_gb
    } else {
        0.0
    };
    let mem_gauge = Gauge::default()
        .gauge_style(Style::default().fg(theme::PRIMARY))
        .label(format!(
            "{:.1}/{:.0}G",
            state.gpu.memory_used_gb, state.gpu.memory_total_gb
        ))
        .ratio(mem_pct.clamp(0.0, 1.0));
    frame.render_widget(mem_gauge, chunks[2]);

    // Temp + power
    let temp_color = if state.gpu.temperature_c > 85 {
        theme::ERROR
    } else if state.gpu.temperature_c > 75 {
        theme::WARNING
    } else {
        theme::SUCCESS
    };
    let power_text = if state.gpu.power_limit_watts > 0 {
        format!(
            "{}W/{}W",
            state.gpu.power_watts, state.gpu.power_limit_watts
        )
    } else {
        format!("{}W", state.gpu.power_watts)
    };
    let temp_line = Line::from(vec![
        Span::styled(
            format!("{}\u{00b0}C", state.gpu.temperature_c),
            Style::default().fg(temp_color),
        ),
        Span::styled(" ", theme::muted_style()),
        Span::styled(power_text, theme::muted_style()),
    ]);
    frame.render_widget(Paragraph::new(temp_line), chunks[3]);
}

// ---------------------------------------------------------------------------
// CPU / RAM panel
// ---------------------------------------------------------------------------

fn render_cpu_panel(frame: &mut Frame, area: Rect, state: &AppState) {
    let block = theme::block(" CPU ");
    let inner = block.inner(area);
    frame.render_widget(block, area);

    let chunks = Layout::default()
        .direction(Direction::Vertical)
        .constraints([
            Constraint::Length(1), // label
            Constraint::Length(2), // CPU gauge
            Constraint::Length(2), // RAM gauge
            Constraint::Min(0),
        ])
        .split(inner);

    // Label
    let label = Line::from(Span::styled("System", theme::muted_style()));
    frame.render_widget(Paragraph::new(label), chunks[0]);

    // CPU gauge
    let cpu_pct = state.system_resources.cpu_usage_pct / 100.0;
    let cpu_gauge = Gauge::default()
        .gauge_style(Style::default().fg(theme::SUCCESS))
        .label(format!("CPU {:.0}%", state.system_resources.cpu_usage_pct))
        .ratio(cpu_pct.clamp(0.0, 1.0));
    frame.render_widget(cpu_gauge, chunks[1]);

    // RAM gauge
    let ram_pct = if state.system_resources.ram_total_gb > 0.0 {
        state.system_resources.ram_used_gb / state.system_resources.ram_total_gb
    } else {
        0.0
    };
    let ram_gauge = Gauge::default()
        .gauge_style(Style::default().fg(theme::WARNING))
        .label(format!(
            "{:.0}/{:.0}G",
            state.system_resources.ram_used_gb, state.system_resources.ram_total_gb
        ))
        .ratio(ram_pct.clamp(0.0, 1.0));
    frame.render_widget(ram_gauge, chunks[2]);
}

// ---------------------------------------------------------------------------
// Epoch financial metrics + action distribution
// ---------------------------------------------------------------------------

fn render_epoch_metrics(frame: &mut Frame, area: Rect, state: &AppState) {
    let block = theme::block(" Epoch Financial Metrics ");
    let inner = block.inner(area);
    frame.render_widget(block, area);

    let chunks = Layout::default()
        .direction(Direction::Vertical)
        .constraints([Constraint::Min(0), Constraint::Length(1)])
        .split(inner);

    // Metrics table
    let header =
        Row::new(vec!["Model", "Sharpe", "Sortino", "Win%", "MaxDD", "PF", "Return", "Trades"])
            .style(theme::header_style());

    let selected = state.selected_training_session;

    let rows: Vec<Row> = state
        .training_sessions
        .iter()
        .enumerate()
        .map(|(i, s)| {
            let is_sel = selected == Some(i);
            let base = if is_sel {
                Style::default()
                    .fg(theme::HIGHLIGHT)
                    .add_modifier(Modifier::BOLD)
            } else {
                Style::default().fg(theme::TEXT)
            };

            let dd_style = if s.max_dd > 0.10 {
                Style::default().fg(theme::ERROR)
            } else if s.max_dd > 0.05 {
                Style::default().fg(theme::WARNING)
            } else {
                Style::default().fg(theme::SUCCESS)
            };

            Row::new(vec![
                Span::styled(s.model.clone(), base),
                Span::styled(format!("{:.2}", s.sharpe), Style::default().fg(theme::HIGHLIGHT)),
                Span::styled(
                    format!("{:.2}", s.sortino),
                    Style::default().fg(theme::SECONDARY),
                ),
                Span::styled(format!("{:.1}%", s.win_rate * 100.0), base),
                Span::styled(format!("{:.1}%", s.max_dd * 100.0), dd_style),
                Span::styled(format!("{:.2}", s.profit_factor), base),
                Span::styled(format!("{:+.1}%", s.total_return * 100.0), base),
                Span::styled(s.total_trades.to_string(), theme::muted_style()),
            ])
        })
        .collect();

    let table = Table::new(
        rows,
        [
            Constraint::Length(10),
            Constraint::Length(8),
            Constraint::Length(8),
            Constraint::Length(7),
            Constraint::Length(7),
            Constraint::Length(6),
            Constraint::Length(9),
            Constraint::Min(6),
        ],
    )
    .header(header);

    frame.render_widget(table, chunks[0]);

    // Action distribution from selected session
    let action_line = if let Some(sel) = selected.and_then(|i| state.training_sessions.get(i)) {
        Line::from(vec![
            Span::styled("Actions: ", theme::muted_style()),
            Span::styled(
                format!("Buy {:.0}%", sel.action_buy_pct * 100.0),
                Style::default().fg(theme::SUCCESS),
            ),
            Span::styled(" | ", theme::muted_style()),
            Span::styled(
                format!("Sell {:.0}%", sel.action_sell_pct * 100.0),
                Style::default().fg(theme::ERROR),
            ),
            Span::styled(" | ", theme::muted_style()),
            Span::styled(
                format!("Hold {:.0}%", sel.action_hold_pct * 100.0),
                Style::default().fg(theme::SECONDARY),
            ),
        ])
    } else {
        Line::from(Span::styled(
            "Select a session (j/k) to see actions",
            theme::muted_style(),
        ))
    };
    frame.render_widget(Paragraph::new(action_line), chunks[1]);
}

// ---------------------------------------------------------------------------
// Detail panel (RL diagnostics, gradient health, hyperopt)
// ---------------------------------------------------------------------------

fn render_detail_panel(frame: &mut Frame, area: Rect, state: &AppState) {
    let session = state
        .selected_training_session
        .and_then(|i| state.training_sessions.get(i));

    let Some(s) = session else {
        let msg = Paragraph::new(Line::from(Span::styled(
            "No session selected",
            theme::muted_style(),
        )))
        .block(theme::block(" Session Detail "));
        frame.render_widget(msg, area);
        return;
    };

    let title = format!(" {} {} \u{2014} Epoch {} ", s.model, s.fold, s.epoch);
    let block = theme::block(&title);
    let inner = block.inner(area);
    frame.render_widget(block, area);

    // Split: diagnostics summary on top, epoch history table below
    let sections = Layout::default()
        .direction(Direction::Vertical)
        .constraints([Constraint::Length(7), Constraint::Min(3)])
        .split(inner);

    // --- Top: 3-column diagnostics (compact) ---
    render_diagnostics_row(frame, sections[0], s);

    // --- Bottom: per-epoch history table ---
    render_epoch_history(frame, sections[1], s, state.epoch_scroll_offset);
}

fn render_diagnostics_row(frame: &mut Frame, area: Rect, s: &TrainingSessionData) {
    let cols = Layout::default()
        .direction(Direction::Horizontal)
        .constraints([
            Constraint::Percentage(35),
            Constraint::Percentage(35),
            Constraint::Percentage(30),
        ])
        .split(area);

    // Col 1: RL diagnostics
    render_detail_col(
        frame,
        cols[0],
        "RL Diagnostics",
        &[
            ("Q-mean", format!("{:.4}", s.q_value_mean)),
            ("Q-max", format!("{:.4}", s.q_value_max)),
            ("Entropy", format!("{:.4}", s.policy_entropy)),
            ("KL-div", format!("{:.6}", s.kl_divergence)),
            ("Advantage", format!("{:.4}", s.advantage_mean)),
            ("Replay buf", format_replay_size(s.replay_buffer_size)),
        ],
    );

    // Col 2: Gradient & health
    render_detail_col(
        frame,
        cols[1],
        "Gradient & Health",
        &[
            ("Grad norm", format!("{:.4}", s.gradient_norm)),
            ("Learn rate", format!("{:.2e}", s.learning_rate)),
            ("NaN count", s.nan_detected.to_string()),
            ("Grad explode", s.gradient_explosions.to_string()),
            ("Feature err", s.feature_errors.to_string()),
            (
                "Checkpoints",
                format!("{} ok, {}", s.checkpoint_saves, s.checkpoint_failures),
            ),
        ],
    );

    // Col 3: Hyperopt or training timing
    if s.is_hyperopt {
        render_detail_col(
            frame,
            cols[2],
            "Hyperopt",
            &[
                (
                    "Trial",
                    format!("{}/{}", s.hyperopt_trial_current, s.hyperopt_trial_total),
                ),
                ("Best obj", format!("{:.6}", s.hyperopt_best_objective)),
                ("Failed", s.hyperopt_trials_failed.to_string()),
                ("Trial epoch", s.hyperopt_trial_epoch.to_string()),
                ("Elapsed", format_duration(s.hyperopt_elapsed_secs)),
            ],
        );
    } else {
        render_detail_col(
            frame,
            cols[2],
            "Training",
            &[
                ("Iter time", format!("{:.2}s", s.iteration_secs)),
                ("Epoch time", format_duration(s.epoch_duration_secs)),
                (
                    "Total time",
                    format_duration(s.epoch_duration_secs * f64::from(s.epoch)),
                ),
                ("Profit factor", format!("{:.2}", s.profit_factor)),
                ("Total return", format!("{:+.2}%", s.total_return * 100.0)),
            ],
        );
    }
}

fn render_epoch_history(
    frame: &mut Frame,
    area: Rect,
    s: &TrainingSessionData,
    scroll_offset: usize,
) {
    let header = Row::new(vec![
        "Ep", "Loss", "Val", "Sharpe", "Sortino", "Win%", "MaxDD", "PF", "Return", "Trades",
        "Q-mean", "GradNorm", "Buy%", "Sell%", "Hold%",
    ])
    .style(theme::header_style());

    // Build rows: epoch history + current epoch as the last row
    let total_epochs = s.epoch_history.len() + 1; // +1 for current
    let visible = area.height.saturating_sub(2) as usize; // header + border
    let max_offset = total_epochs.saturating_sub(visible);
    let offset = scroll_offset.min(max_offset);

    let mut all_rows: Vec<Row> = s
        .epoch_history
        .iter()
        .map(|e| epoch_snapshot_row(e))
        .collect();

    // Current epoch as the newest row
    all_rows.push(current_epoch_row(s));

    // Apply scroll offset
    let visible_rows: Vec<Row> = all_rows.into_iter().skip(offset).take(visible).collect();

    let epoch_count = total_epochs;
    let title = format!(
        " Epoch History \u{2014} {} epoch{} ",
        epoch_count,
        if epoch_count == 1 { "" } else { "s" }
    );

    let table = Table::new(
        visible_rows,
        [
            Constraint::Length(4),  // epoch
            Constraint::Length(7),  // loss
            Constraint::Length(7),  // val
            Constraint::Length(7),  // sharpe
            Constraint::Length(8),  // sortino
            Constraint::Length(6),  // win%
            Constraint::Length(6),  // maxdd
            Constraint::Length(5),  // pf
            Constraint::Length(8),  // return
            Constraint::Length(6),  // trades
            Constraint::Length(8),  // q-mean
            Constraint::Length(8),  // grad
            Constraint::Length(5),  // buy%
            Constraint::Length(5),  // sell%
            Constraint::Min(5),    // hold%
        ],
    )
    .header(header)
    .block(theme::block(&title));

    frame.render_widget(table, area);
}

fn epoch_snapshot_row(e: &EpochSnapshot) -> Row<'static> {
    let dd_style = if e.max_dd > 0.10 {
        Style::default().fg(theme::ERROR)
    } else if e.max_dd > 0.05 {
        Style::default().fg(theme::WARNING)
    } else {
        Style::default().fg(theme::SUCCESS)
    };

    Row::new(vec![
        Span::styled(e.epoch.to_string(), Style::default().fg(theme::TEXT)),
        Span::styled(format!("{:.4}", e.loss), Style::default().fg(theme::TEXT)),
        Span::styled(
            format!("{:.4}", e.val_loss),
            Style::default().fg(theme::SECONDARY),
        ),
        Span::styled(
            format!("{:.2}", e.sharpe),
            Style::default().fg(theme::HIGHLIGHT),
        ),
        Span::styled(
            format!("{:.2}", e.sortino),
            Style::default().fg(theme::SECONDARY),
        ),
        Span::styled(
            format!("{:.1}%", e.win_rate * 100.0),
            Style::default().fg(theme::TEXT),
        ),
        Span::styled(format!("{:.1}%", e.max_dd * 100.0), dd_style),
        Span::styled(
            format!("{:.2}", e.profit_factor),
            Style::default().fg(theme::TEXT),
        ),
        Span::styled(
            format!("{:+.1}%", e.total_return * 100.0),
            Style::default().fg(theme::TEXT),
        ),
        Span::styled(
            e.total_trades.to_string(),
            theme::muted_style(),
        ),
        Span::styled(
            format!("{:.3}", e.q_value_mean),
            theme::muted_style(),
        ),
        Span::styled(
            format!("{:.4}", e.gradient_norm),
            theme::muted_style(),
        ),
        Span::styled(
            format!("{:.0}", e.action_buy_pct * 100.0),
            Style::default().fg(theme::SUCCESS),
        ),
        Span::styled(
            format!("{:.0}", e.action_sell_pct * 100.0),
            Style::default().fg(theme::ERROR),
        ),
        Span::styled(
            format!("{:.0}", e.action_hold_pct * 100.0),
            Style::default().fg(theme::SECONDARY),
        ),
    ])
}

fn current_epoch_row(s: &TrainingSessionData) -> Row<'static> {
    let dd_style = if s.max_dd > 0.10 {
        Style::default().fg(theme::ERROR)
    } else if s.max_dd > 0.05 {
        Style::default().fg(theme::WARNING)
    } else {
        Style::default().fg(theme::SUCCESS)
    };

    Row::new(vec![
        Span::styled(
            format!("{}\u{25c0}", s.epoch),
            Style::default()
                .fg(theme::HIGHLIGHT)
                .add_modifier(Modifier::BOLD),
        ),
        Span::styled(
            format!("{:.4}", s.loss),
            Style::default()
                .fg(theme::TEXT)
                .add_modifier(Modifier::BOLD),
        ),
        Span::styled(
            format!("{:.4}", s.val_loss),
            Style::default()
                .fg(theme::SECONDARY)
                .add_modifier(Modifier::BOLD),
        ),
        Span::styled(
            format!("{:.2}", s.sharpe),
            Style::default()
                .fg(theme::HIGHLIGHT)
                .add_modifier(Modifier::BOLD),
        ),
        Span::styled(
            format!("{:.2}", s.sortino),
            Style::default()
                .fg(theme::SECONDARY)
                .add_modifier(Modifier::BOLD),
        ),
        Span::styled(
            format!("{:.1}%", s.win_rate * 100.0),
            Style::default()
                .fg(theme::TEXT)
                .add_modifier(Modifier::BOLD),
        ),
        Span::styled(
            format!("{:.1}%", s.max_dd * 100.0),
            dd_style.add_modifier(Modifier::BOLD),
        ),
        Span::styled(
            format!("{:.2}", s.profit_factor),
            Style::default()
                .fg(theme::TEXT)
                .add_modifier(Modifier::BOLD),
        ),
        Span::styled(
            format!("{:+.1}%", s.total_return * 100.0),
            Style::default()
                .fg(theme::TEXT)
                .add_modifier(Modifier::BOLD),
        ),
        Span::styled(
            s.total_trades.to_string(),
            Style::default()
                .fg(theme::TEXT)
                .add_modifier(Modifier::BOLD),
        ),
        Span::styled(
            format!("{:.3}", s.q_value_mean),
            Style::default().fg(theme::TEXT).add_modifier(Modifier::BOLD),
        ),
        Span::styled(
            format!("{:.4}", s.gradient_norm),
            Style::default().fg(theme::TEXT).add_modifier(Modifier::BOLD),
        ),
        Span::styled(
            format!("{:.0}", s.action_buy_pct * 100.0),
            Style::default()
                .fg(theme::SUCCESS)
                .add_modifier(Modifier::BOLD),
        ),
        Span::styled(
            format!("{:.0}", s.action_sell_pct * 100.0),
            Style::default()
                .fg(theme::ERROR)
                .add_modifier(Modifier::BOLD),
        ),
        Span::styled(
            format!("{:.0}", s.action_hold_pct * 100.0),
            Style::default()
                .fg(theme::SECONDARY)
                .add_modifier(Modifier::BOLD),
        ),
    ])
}

fn render_detail_col(frame: &mut Frame, area: Rect, title: &str, items: &[(&str, String)]) {
    let mut lines = vec![Line::from(Span::styled(
        title,
        Style::default()
            .fg(theme::SECONDARY)
            .add_modifier(Modifier::BOLD),
    ))];

    for (label, value) in items {
        lines.push(Line::from(vec![
            Span::styled(format!(" {label}: "), theme::muted_style()),
            Span::styled(value.clone(), Style::default().fg(theme::TEXT)),
        ]));
    }

    frame.render_widget(Paragraph::new(lines), area);
}

fn format_replay_size(size: u32) -> String {
    if size >= 1_000_000 {
        format!("{:.1}M", f64::from(size) / 1_000_000.0)
    } else if size >= 1_000 {
        format!("{:.1}K", f64::from(size) / 1_000.0)
    } else {
        size.to_string()
    }
}
