use crossterm::execute;
use crossterm::terminal::{
    EnterAlternateScreen, LeaveAlternateScreen, disable_raw_mode, enable_raw_mode,
};
use indicatif::{MultiProgress, ProgressBar, ProgressDrawTarget, ProgressStyle};
use log::{Level, LevelFilter, Log, Metadata, Record};
use ratatui::prelude::*;
use ratatui::text::{Line as TextLine, Span};
use ratatui::widgets::{Axis, Block, Borders, Chart, Dataset, GraphType, Paragraph, Wrap};
use std::io::{self, IsTerminal, Stdout, Write};
use std::sync::{Mutex, OnceLock};
use std::time::{Duration, Instant};

const INTERACTIVE_DRAW_INTERVAL: Duration = Duration::from_millis(40);
const DUMB_DRAW_INTERVAL: Duration = Duration::from_secs(1);
const MAX_HISTORY_POINTS: usize = 1200;
const MAX_DIAGNOSTIC_LINES: usize = 10;

static LOGGER: ProgressLogger = ProgressLogger;
static ACTIVE_MULTIPROGRESS: OnceLock<Mutex<Option<MultiProgress>>> = OnceLock::new();

fn active_multiprogress() -> &'static Mutex<Option<MultiProgress>> {
    ACTIVE_MULTIPROGRESS.get_or_init(|| Mutex::new(None))
}

struct ProgressLogger;

impl Log for ProgressLogger {
    fn enabled(&self, metadata: &Metadata<'_>) -> bool {
        metadata.level() <= log::max_level()
    }

    fn log(&self, record: &Record<'_>) {
        if !self.enabled(record.metadata()) {
            return;
        }
        let line = format_log_record(record);
        if let Ok(guard) = active_multiprogress().lock()
            && let Some(mp) = guard.as_ref()
        {
            mp.println(line).ok();
            return;
        }
        writeln!(io::stderr(), "{line}").ok();
    }

    fn flush(&self) {}
}

fn format_log_record(record: &Record<'_>) -> String {
    let tag = match record.level() {
        Level::Error => "ERROR",
        Level::Warn => "WARN",
        Level::Info => "INFO",
        Level::Debug => "DEBUG",
        Level::Trace => "TRACE",
    };
    format!("[{tag}] {}", record.args())
}

pub fn init_logging() {
    if log::set_logger(&LOGGER).is_ok() {
        log::set_max_level(LevelFilter::Info);
    }
}

fn install_multiprogress(mp: Option<MultiProgress>) {
    if let Ok(mut guard) = active_multiprogress().lock() {
        *guard = mp;
    }
}

#[derive(Clone, Debug, Default)]
struct LaneState {
    label: String,
    current: usize,
    total: Option<usize>,
    done: bool,
}

#[derive(Clone, Debug)]
struct VisualizerModel {
    history_cost_accepted: Vec<(f64, f64)>,
    history_cost_trial: Vec<(f64, f64)>,
    history_grad_log: Vec<(f64, f64)>,
    start_time: Instant,
    current_iter: f64,
    best_cost: f64,
    current_status: String,
    current_stage: String,
    current_detail: String,
    current_eval_state: String,
    current_cost: f64,
    current_grad: f64,
    primary_lane: LaneState,
    secondary_lane: LaneState,
    edf_terms: Vec<(String, f64, f64)>,
    diagnostics_lines: Vec<String>,
    diagnostics_condition: Option<f64>,
    diagnostics_step_size: Option<f64>,
    diagnostics_ridge: Option<f64>,
}

impl Default for VisualizerModel {
    fn default() -> Self {
        Self {
            history_cost_accepted: Vec::new(),
            history_cost_trial: Vec::new(),
            history_grad_log: Vec::new(),
            start_time: Instant::now(),
            current_iter: 0.0,
            best_cost: f64::INFINITY,
            current_status: "Initializing...".to_string(),
            current_stage: "init".to_string(),
            current_detail: String::new(),
            current_eval_state: String::new(),
            current_cost: f64::NAN,
            current_grad: f64::NAN,
            primary_lane: LaneState::default(),
            secondary_lane: LaneState::default(),
            edf_terms: Vec::new(),
            diagnostics_lines: Vec::new(),
            diagnostics_condition: None,
            diagnostics_step_size: None,
            diagnostics_ridge: None,
        }
    }
}

enum VisualizerState {
    Disabled,
    Interactive(InteractiveVisualizer),
    Dumb(DumbVisualizer),
}

pub struct VisualizerSession {
    state: VisualizerState,
    model: VisualizerModel,
}

struct InteractiveVisualizer {
    terminal: Terminal<CrosstermBackend<Stdout>>,
    last_draw: Instant,
}

struct DumbVisualizer {
    multi: Option<MultiProgress>,
    primary_bar: ProgressBar,
    secondary_bar: ProgressBar,
    last_draw: Instant,
    text_only: bool,
    last_lines: Vec<String>,
}

impl InteractiveVisualizer {
    fn new() -> io::Result<Self> {
        enable_raw_mode()?;
        let mut stdout = io::stdout();
        execute!(stdout, EnterAlternateScreen)?;
        let backend = CrosstermBackend::new(stdout);
        let terminal = Terminal::new(backend)?;
        Ok(Self {
            terminal,
            last_draw: Instant::now() - INTERACTIVE_DRAW_INTERVAL,
        })
    }

    fn maybe_draw(&mut self, model: &VisualizerModel, force: bool) {
        if !force && self.last_draw.elapsed() < INTERACTIVE_DRAW_INTERVAL {
            return;
        }
        self.draw(model).ok();
        self.last_draw = Instant::now();
    }

    fn draw(&mut self, model: &VisualizerModel) -> io::Result<()> {
        let cost_accepted = model.history_cost_accepted.clone();
        let cost_trial = model.history_cost_trial.clone();
        let grad_data = model.history_grad_log.clone();
        let status = model.current_status.clone();
        let stage = model.current_stage.clone();
        let detail = model.current_detail.clone();
        let eval_state = model.current_eval_state.clone();
        let iter = model.current_iter;
        let best = model.best_cost;
        let elapsed = model.start_time.elapsed().as_secs();
        let current_cost = model.current_cost;
        let current_grad = model.current_grad;
        let primary_lane = model.primary_lane.clone();
        let secondary_lane = model.secondary_lane.clone();
        let edf_terms = model.edf_terms.clone();
        let diagnostics_lines = model.diagnostics_lines.clone();
        let diagnostics_condition = model.diagnostics_condition;
        let diagnostics_step_size = model.diagnostics_step_size;
        let diagnostics_ridge = model.diagnostics_ridge;
        let status_class = classify_model_status(model);
        let accent = status_color(status_class);
        let primary_class = classify_lane_status(&primary_lane, status_class);
        let secondary_class = classify_lane_status(&secondary_lane, status_class);

        self.terminal.draw(|f| {
            let area = f.area();
            let compact = area.height < 30 || area.width < 120;
            let chunks = Layout::default()
                .direction(Direction::Vertical)
                .constraints(if compact {
                    [Constraint::Percentage(68), Constraint::Percentage(32)]
                } else {
                    [Constraint::Percentage(72), Constraint::Percentage(28)]
                })
                .split(area);
            let top_chunks = Layout::default()
                .direction(Direction::Horizontal)
                .constraints(if compact {
                    [Constraint::Percentage(58), Constraint::Percentage(42)]
                } else {
                    [Constraint::Percentage(62), Constraint::Percentage(38)]
                })
                .split(chunks[0]);

            let (min_y, max_y) = if cost_accepted.is_empty() && cost_trial.is_empty() {
                (0.0, 1.0)
            } else {
                let minval = cost_accepted
                    .iter()
                    .chain(cost_trial.iter())
                    .map(|(_, y)| *y)
                    .fold(f64::INFINITY, f64::min);
                let maxval = cost_accepted
                    .iter()
                    .chain(cost_trial.iter())
                    .map(|(_, y)| *y)
                    .fold(f64::NEG_INFINITY, f64::max);
                (minval, maxval)
            };
            let window = (max_y - min_y).max(1.0);

            let datasets = vec![
                Dataset::default()
                    .name("Accepted")
                    .marker(symbols::Marker::Braille)
                    .graph_type(GraphType::Line)
                    .style(Style::default().fg(Color::Cyan))
                    .data(&cost_accepted),
                Dataset::default()
                    .name("Trial")
                    .marker(symbols::Marker::Dot)
                    .graph_type(GraphType::Scatter)
                    .style(Style::default().fg(Color::LightBlue))
                    .data(&cost_trial),
            ];

            let chart = Chart::new(datasets)
                .block(
                    Block::default()
                        .title("Objective (LAML/REML)")
                        .border_style(Style::default().fg(accent))
                        .borders(Borders::ALL),
                )
                .x_axis(
                    Axis::default()
                        .title("Outer Iteration")
                        .bounds([0.0, iter.max(10.0)])
                        .labels(vec![
                            Line::from("0"),
                            Line::from(format!("{:.0}", iter.max(10.0))),
                        ]),
                )
                .y_axis(
                    Axis::default()
                        .title("Objective Value")
                        .bounds([min_y - window * 0.1, max_y + window * 0.1])
                        .labels(vec![
                            Line::from(format!("{:.2}", min_y)),
                            Line::from(format!("{:.2}", max_y)),
                        ]),
                );
            f.render_widget(chart, top_chunks[0]);

            let right_chunks = Layout::default()
                .direction(Direction::Vertical)
                .constraints(if compact {
                    [Constraint::Percentage(36), Constraint::Percentage(64)]
                } else {
                    [Constraint::Percentage(34), Constraint::Percentage(66)]
                })
                .split(top_chunks[1]);

            let summary_lines = summary_panel_lines(
                &stage,
                &detail,
                &status,
                elapsed,
                &primary_lane,
                &secondary_lane,
                primary_class,
                secondary_class,
            );
            let summary_panel = Paragraph::new(summary_lines)
                .block(
                    Block::default()
                        .title("Session")
                        .border_style(Style::default().fg(accent))
                        .borders(Borders::ALL),
                )
                .wrap(Wrap { trim: true });
            f.render_widget(summary_panel, right_chunks[0]);

            let mut model_lines = Vec::<String>::new();
            if edf_terms.is_empty() {
                model_lines
                    .push("  Waiting for effective degrees of freedom updates...".to_string());
            } else {
                let maxrows = right_chunks[1]
                    .height
                    .saturating_sub(if compact { 3 } else { 4 })
                    as usize;
                let mut shown_terms = edf_terms.clone();
                shown_terms
                    .sort_by(|a, b| b.1.partial_cmp(&a.1).unwrap_or(std::cmp::Ordering::Equal));
                if shown_terms.len() > maxrows.saturating_sub(1) && maxrows > 1 {
                    shown_terms.truncate(maxrows - 1);
                    model_lines.push(format!("showing top {} terms by EDF", shown_terms.len()));
                }
                let namew = shown_terms
                    .iter()
                    .map(|(name, _, _)| name.len())
                    .max()
                    .unwrap_or(8)
                    .min(if compact { 14 } else { 20 })
                    .max(6);
                for (name, edf, ref_df) in shown_terms {
                    let ratio = if ref_df > 0.0 {
                        (edf / ref_df).clamp(0.0, 1.0)
                    } else {
                        0.0
                    };
                    let barwidth = if compact { 12 } else { 20 };
                    let bar = unicode_bar(ratio, barwidth);
                    let displayname = if name.len() > namew {
                        let keep = namew.saturating_sub(1);
                        format!("{}…", &name[..keep])
                    } else {
                        name
                    };
                    model_lines.push(format!(
                        "{:<namew$} effective {:>6.2} of {:>6.2} reference  {}",
                        displayname,
                        edf,
                        ref_df,
                        bar,
                        namew = namew
                    ));
                }
            }

            let model_panel = Paragraph::new(model_lines.join("\n"))
                .block(
                    Block::default()
                        .title("Effective Degrees of Freedom")
                        .border_style(Style::default().fg(Color::LightCyan))
                        .borders(Borders::ALL),
                )
                .style(Style::default().fg(Color::White))
                .wrap(Wrap { trim: true });
            f.render_widget(model_panel, right_chunks[1]);

            let mut diagnostics = Vec::<String>::new();
            diagnostics.push(format!("Stage: {stage}"));
            diagnostics.push(format!("Detail: {detail}"));
            diagnostics.push(format!("Evaluation: {:.0} {eval_state}", iter));
            diagnostics.push(format!("Status: {status}"));
            diagnostics.push(format!("Best Objective: {:.6}", best));
            diagnostics.push(format!("Current Objective: {:.6}", current_cost));
            diagnostics.push(format!("Gradient Norm: {:.3e}", current_grad));
            diagnostics.push(format!("Elapsed Time: {elapsed}s"));
            diagnostics.push(String::new());
            diagnostics.push(format!(
                "Condition Number: {}",
                diagnostics_condition
                    .map(|v| format!("{v:.3e}"))
                    .unwrap_or_else(|| "n/a".to_string())
            ));
            diagnostics.push(format!(
                "Step Size (norm delta rho): {}",
                diagnostics_step_size
                    .map(|v| format!("{v:.3e}"))
                    .unwrap_or_else(|| "n/a".to_string())
            ));
            diagnostics.push(format!(
                "Stabilization Ridge: {}",
                diagnostics_ridge
                    .map(|v| format!("{v:.3e}"))
                    .unwrap_or_else(|| "n/a".to_string())
            ));
            diagnostics.push(format!(
                "Convergence signal (base 10 log gradient norm): {:.3}",
                grad_data.last().map(|(_, y)| *y).unwrap_or(0.0)
            ));
            diagnostics.push("Recent Diagnostics and Warnings:".to_string());

            let base_reserved = diagnostics.len() + 1;
            let availrows = chunks[1].height.saturating_sub(2) as usize;
            let max_diag_lines = availrows.saturating_sub(base_reserved).max(1);
            if diagnostics_lines.is_empty() {
                diagnostics.push("  (no diagnostics yet)".to_string());
            } else {
                let start = diagnostics_lines.len().saturating_sub(max_diag_lines);
                for line in diagnostics_lines.into_iter().skip(start) {
                    diagnostics.push(format!("  {line}"));
                }
            }
            diagnostics.push("Press Ctrl+C to abort".to_string());

            let diagnostics_panel = Paragraph::new(diagnostics.join("\n"))
                .block(
                    Block::default()
                        .title("Diagnostics")
                        .border_style(Style::default().fg(accent))
                        .borders(Borders::ALL),
                )
                .style(Style::default().fg(Color::White))
                .wrap(Wrap { trim: true });
            f.render_widget(diagnostics_panel, chunks[1]);
        })?;
        Ok(())
    }

    fn teardown(&mut self, model: &VisualizerModel) {
        self.maybe_draw(model, true);
        disable_raw_mode().ok();
        execute!(io::stdout(), LeaveAlternateScreen).ok();
    }
}

#[derive(Clone, Copy, Debug, Eq, PartialEq)]
enum StatusClass {
    Running,
    Success,
    Warning,
    Error,
}

#[derive(Clone, Copy, Debug, Eq, PartialEq)]
enum LaneTone {
    Primary,
    Secondary,
}

impl DumbVisualizer {
    fn new(text_only: bool) -> Self {
        let (multi, primary_bar, secondary_bar) = if text_only {
            (None, ProgressBar::hidden(), ProgressBar::hidden())
        } else {
            let multi = MultiProgress::with_draw_target(ProgressDrawTarget::stderr());
            let primary_bar = multi.add(ProgressBar::hidden());
            let secondary_bar = multi.add(ProgressBar::hidden());
            primary_bar.set_style(primary_style());
            secondary_bar.set_style(secondary_style());
            install_multiprogress(Some(multi.clone()));
            (Some(multi), primary_bar, secondary_bar)
        };
        Self {
            multi,
            primary_bar,
            secondary_bar,
            last_draw: Instant::now() - DUMB_DRAW_INTERVAL,
            text_only,
            last_lines: Vec::new(),
        }
    }

    fn maybe_draw(&mut self, model: &VisualizerModel, force: bool) {
        if !self.text_only {
            self.sync_bars(model);
        }
        if !force && self.last_draw.elapsed() < DUMB_DRAW_INTERVAL {
            return;
        }
        self.draw(model, force);
        self.last_draw = Instant::now();
    }

    fn sync_bars(&self, model: &VisualizerModel) {
        sync_bar(&self.primary_bar, &model.primary_lane, &model);
        sync_bar(&self.secondary_bar, &model.secondary_lane, &model);
    }

    fn draw(&mut self, model: &VisualizerModel, force: bool) {
        let lines = dumb_render_lines(model);
        if !force && lines == self.last_lines {
            return;
        }
        self.last_lines = lines.clone();
        if let Some(multi) = &self.multi {
            for line in lines {
                multi.println(line).ok();
            }
        } else {
            for line in lines {
                writeln!(io::stderr(), "{line}").ok();
            }
        }
    }

    fn teardown(&mut self, model: &VisualizerModel) {
        self.maybe_draw(model, true);
        if let Some(multi) = &self.multi {
            multi.clear().ok();
        }
        install_multiprogress(None);
    }
}

fn primary_style() -> ProgressStyle {
    ProgressStyle::with_template(
        "{spinner:.cyan} [{elapsed_precise}] ▕{bar:40.cyan/blue}▏ {percent:>3}% | ETA: {eta_precise} | {msg}",
    )
    .unwrap_or_else(|_| ProgressStyle::default_bar())
    .progress_chars("█▉▊▋▌▍▎▏ ")
}

fn secondary_style() -> ProgressStyle {
    ProgressStyle::with_template(
        "{spinner:.magenta} [{elapsed_precise}] ▕{bar:40.magenta/blue}▏ {percent:>3}% | ETA: {eta_precise} | {msg}",
    )
    .unwrap_or_else(|_| ProgressStyle::default_bar())
    .progress_chars("█▉▊▋▌▍▎▏ ")
}

fn sync_bar(bar: &ProgressBar, lane: &LaneState, model: &VisualizerModel) {
    if lane.label.is_empty() {
        bar.set_draw_target(ProgressDrawTarget::hidden());
        return;
    }
    bar.set_draw_target(ProgressDrawTarget::stderr());
    match lane.total {
        Some(total) if total > 0 => {
            bar.set_length(total as u64);
            bar.set_position(lane.current.min(total) as u64);
        }
        _ => {
            bar.enable_steady_tick(Duration::from_millis(120));
        }
    }
    let mut msg = format!(
        "{} | LAML: {} | |grad|: {} | Ridge: {}",
        lane.label,
        format_metric(model.current_cost, "{:.4}"),
        format_metric(model.current_grad, "{:.3e}"),
        model
            .diagnostics_ridge
            .map(|v| format!("{v:.1e}"))
            .unwrap_or_else(|| "n/a".to_string()),
    );
    if lane.done {
        msg.push_str(" | converged");
    }
    bar.set_style(style_for_lane(
        classify_lane_status(lane, classify_model_status(model)),
        if lane.label.contains("Warmup")
            || lane.label.contains("Sample")
            || lane.label.contains("Inner")
        {
            LaneTone::Secondary
        } else {
            LaneTone::Primary
        },
    ));
    bar.set_message(msg);
    if lane.done {
        bar.finish_and_clear();
    }
}

impl Default for VisualizerSession {
    fn default() -> Self {
        Self {
            state: VisualizerState::Disabled,
            model: VisualizerModel::default(),
        }
    }
}

impl VisualizerSession {
    pub fn new(enabled: bool) -> Self {
        init_logging();
        if !enabled {
            return Self::default();
        }
        let notebook_or_noninteractive = should_use_text_only_progress();
        let interactive =
            !notebook_or_noninteractive && io::stdout().is_terminal() && io::stderr().is_terminal();
        let state = if interactive {
            match InteractiveVisualizer::new() {
                Ok(v) => VisualizerState::Interactive(v),
                Err(_) => VisualizerState::Dumb(DumbVisualizer::new(true)),
            }
        } else {
            VisualizerState::Dumb(DumbVisualizer::new(true))
        };
        Self {
            state,
            model: VisualizerModel::default(),
        }
    }

    pub fn is_active(&self) -> bool {
        !matches!(self.state, VisualizerState::Disabled)
    }

    #[cfg(test)]
    pub(crate) fn is_interactive(&self) -> bool {
        matches!(self.state, VisualizerState::Interactive(_))
    }

    pub fn update(
        &mut self,
        cost: f64,
        grad_norm: f64,
        status_msg: &str,
        iter: f64,
        eval_state: &str,
        total_steps: Option<usize>,
    ) {
        self.model.current_iter = iter;
        self.model.current_cost = cost;
        self.model.current_grad = grad_norm;
        self.model.current_eval_state = eval_state.to_string();
        self.model.current_status = status_msg.to_string();

        if let Some(total) = total_steps {
            self.model.primary_lane.total = Some(total);
            self.model.primary_lane.current = iter.max(0.0).round() as usize;
        }

        if cost.is_finite() && cost.abs() < 1e15 {
            let target_series = if eval_state == "trial" {
                &mut self.model.history_cost_trial
            } else {
                &mut self.model.history_cost_accepted
            };
            push_sample(target_series, (iter, cost));
            if cost < self.model.best_cost {
                self.model.best_cost = cost;
            }
        }

        if grad_norm.is_finite() {
            let grad_log = grad_norm.max(1e-12).log10();
            push_sample(&mut self.model.history_grad_log, (iter, grad_log));
        }

        self.redraw(false);
    }

    pub fn set_stage(&mut self, stage: &str, detail: &str) {
        self.model.current_stage = stage.to_string();
        self.model.current_detail = detail.to_string();
        self.model.current_status = detail.to_string();
        self.redraw(true);
    }

    pub fn start_workflow(&mut self, label: &str, total: usize) {
        self.model.primary_lane = started_lane(label, total);
        self.redraw(true);
    }

    pub fn advance_workflow(&mut self, current: usize) {
        if self.model.primary_lane.label.is_empty() {
            return;
        }
        advance_lane(&mut self.model.primary_lane, current);
        self.redraw(false);
    }

    pub fn start_secondary_workflow(&mut self, label: &str, total: usize) {
        self.model.secondary_lane = started_lane(label, total);
        self.redraw(true);
    }

    pub fn advance_secondary_workflow(&mut self, current: usize) {
        if self.model.secondary_lane.label.is_empty() {
            return;
        }
        advance_lane(&mut self.model.secondary_lane, current);
        self.redraw(false);
    }

    pub fn finish_secondary_progress(&mut self, message: &str) {
        if self.model.secondary_lane.label.is_empty() {
            return;
        }
        if let Some(total) = self.model.secondary_lane.total {
            self.model.secondary_lane.current = total;
        }
        self.model.secondary_lane.done = true;
        self.push_diagnostic(message);
        self.redraw(true);
        self.model.secondary_lane = LaneState::default();
    }

    pub fn finish_progress(&mut self, message: &str) {
        if self.model.primary_lane.label.is_empty() {
            return;
        }
        if let Some(total) = self.model.primary_lane.total {
            self.model.primary_lane.current = total;
        }
        self.model.primary_lane.done = true;
        self.push_diagnostic(message);
        self.redraw(true);
    }

    pub fn push_diagnostic(&mut self, message: &str) {
        self.model.diagnostics_lines.push(message.to_string());
        if self.model.diagnostics_lines.len() > MAX_DIAGNOSTIC_LINES {
            let overflow = self.model.diagnostics_lines.len() - MAX_DIAGNOSTIC_LINES;
            self.model.diagnostics_lines.drain(0..overflow);
        }
        self.redraw(true);
    }

    pub fn teardown(&mut self) {
        match &mut self.state {
            VisualizerState::Disabled => {}
            VisualizerState::Interactive(vis) => vis.teardown(&self.model),
            VisualizerState::Dumb(vis) => vis.teardown(&self.model),
        }
        self.state = VisualizerState::Disabled;
    }

    fn redraw(&mut self, force: bool) {
        match &mut self.state {
            VisualizerState::Disabled => {}
            VisualizerState::Interactive(vis) => vis.maybe_draw(&self.model, force),
            VisualizerState::Dumb(vis) => vis.maybe_draw(&self.model, force),
        }
    }
}

impl Drop for VisualizerSession {
    fn drop(&mut self) {
        self.teardown();
    }
}

fn push_sample(series: &mut Vec<(f64, f64)>, sample: (f64, f64)) {
    series.push(sample);
    if series.len() > MAX_HISTORY_POINTS {
        let mut compacted = Vec::with_capacity(series.len() / 2 + 1);
        for (idx, point) in series.iter().enumerate() {
            if idx % 2 == 0 {
                compacted.push(*point);
            }
        }
        *series = compacted;
    }
}

fn classify_model_status(model: &VisualizerModel) -> StatusClass {
    let status = model.current_status.to_ascii_lowercase();
    let detail = model.current_detail.to_ascii_lowercase();
    let recent_diag = model
        .diagnostics_lines
        .last()
        .map(|s| s.to_ascii_lowercase())
        .unwrap_or_default();
    if contains_error_signal(&status)
        || contains_error_signal(&detail)
        || contains_error_signal(&recent_diag)
    {
        StatusClass::Error
    } else if contains_warning_signal(&status)
        || contains_warning_signal(&detail)
        || contains_warning_signal(&recent_diag)
    {
        StatusClass::Warning
    } else if contains_success_signal(&status) || contains_success_signal(&detail) {
        StatusClass::Success
    } else {
        StatusClass::Running
    }
}

fn classify_lane_status(lane: &LaneState, model_class: StatusClass) -> StatusClass {
    if lane.done {
        StatusClass::Success
    } else {
        model_class
    }
}

fn contains_error_signal(text: &str) -> bool {
    ["error", "failed", "indefinite", "abort"]
        .iter()
        .any(|needle| text.contains(needle))
}

fn contains_warning_signal(text: &str) -> bool {
    ["warning", "stalled", "max iterations", "non-finite"]
        .iter()
        .any(|needle| text.contains(needle))
}

fn contains_success_signal(text: &str) -> bool {
    ["complete", "converged", "finished", "done"]
        .iter()
        .any(|needle| text.contains(needle))
}

fn status_color(class: StatusClass) -> Color {
    match class {
        StatusClass::Running => Color::Cyan,
        StatusClass::Success => Color::Green,
        StatusClass::Warning => Color::Yellow,
        StatusClass::Error => Color::Red,
    }
}

fn status_label(class: StatusClass) -> &'static str {
    match class {
        StatusClass::Running => "RUNNING",
        StatusClass::Success => "CONVERGED",
        StatusClass::Warning => "WARNING",
        StatusClass::Error => "ERROR",
    }
}

fn style_for_lane(class: StatusClass, tone: LaneTone) -> ProgressStyle {
    let template = match (tone, class) {
        (LaneTone::Primary, StatusClass::Running) => {
            "{spinner:.cyan} [{elapsed_precise}] ▕{bar:40.cyan/blue}▏ {percent:>3}% | ETA: {eta_precise} | {msg}"
        }
        (LaneTone::Primary, StatusClass::Success) => {
            "{spinner:.green} [{elapsed_precise}] ▕{bar:40.green/blue}▏ {percent:>3}% | ETA: {eta_precise} | {msg}"
        }
        (LaneTone::Primary, StatusClass::Warning) => {
            "{spinner:.yellow} [{elapsed_precise}] ▕{bar:40.yellow/red}▏ {percent:>3}% | ETA: {eta_precise} | {msg}"
        }
        (LaneTone::Primary, StatusClass::Error) => {
            "{spinner:.red} [{elapsed_precise}] ▕{bar:40.red/yellow}▏ {percent:>3}% | ETA: {eta_precise} | {msg}"
        }
        (LaneTone::Secondary, StatusClass::Running) => {
            "{spinner:.magenta} [{elapsed_precise}] ▕{bar:40.magenta/blue}▏ {percent:>3}% | ETA: {eta_precise} | {msg}"
        }
        (LaneTone::Secondary, StatusClass::Success) => {
            "{spinner:.green} [{elapsed_precise}] ▕{bar:40.green/magenta}▏ {percent:>3}% | ETA: {eta_precise} | {msg}"
        }
        (LaneTone::Secondary, StatusClass::Warning) => {
            "{spinner:.yellow} [{elapsed_precise}] ▕{bar:40.yellow/magenta}▏ {percent:>3}% | ETA: {eta_precise} | {msg}"
        }
        (LaneTone::Secondary, StatusClass::Error) => {
            "{spinner:.red} [{elapsed_precise}] ▕{bar:40.red/magenta}▏ {percent:>3}% | ETA: {eta_precise} | {msg}"
        }
    };
    ProgressStyle::with_template(template)
        .unwrap_or_else(|_| ProgressStyle::default_bar())
        .progress_chars("█▉▊▋▌▍▎▏ ")
}

fn summary_panel_lines(
    stage: &str,
    detail: &str,
    status: &str,
    elapsed: u64,
    primary_lane: &LaneState,
    secondary_lane: &LaneState,
    primary_class: StatusClass,
    secondary_class: StatusClass,
) -> Vec<TextLine<'static>> {
    let model_class =
        if secondary_class == StatusClass::Error || primary_class == StatusClass::Error {
            StatusClass::Error
        } else if secondary_class == StatusClass::Warning || primary_class == StatusClass::Warning {
            StatusClass::Warning
        } else if primary_class == StatusClass::Success
            && (secondary_lane.label.is_empty() || secondary_class == StatusClass::Success)
        {
            StatusClass::Success
        } else {
            StatusClass::Running
        };
    let mut lines = vec![
        TextLine::from(vec![
            Span::styled(
                format!(" {} ", status_label(model_class)),
                Style::default()
                    .fg(Color::Black)
                    .bg(status_color(model_class))
                    .add_modifier(Modifier::BOLD),
            ),
            Span::raw("  "),
            Span::styled(
                stage.to_ascii_uppercase(),
                Style::default()
                    .fg(Color::White)
                    .add_modifier(Modifier::BOLD),
            ),
        ]),
        TextLine::from(Span::styled(
            detail.to_string(),
            Style::default().fg(Color::Gray),
        )),
        TextLine::from(Span::styled(
            format!("Elapsed {}s", elapsed),
            Style::default().fg(Color::DarkGray),
        )),
        lane_line("Outer", primary_lane, primary_class),
    ];
    if !secondary_lane.label.is_empty() {
        lines.push(lane_line("Inner", secondary_lane, secondary_class));
    }
    lines.push(TextLine::from(Span::styled(
        status.to_string(),
        Style::default().fg(status_color(model_class)),
    )));
    lines
}

fn lane_line(name: &str, lane: &LaneState, class: StatusClass) -> TextLine<'static> {
    let color = status_color(class);
    let label = if lane.label.is_empty() {
        format!("{name}: idle")
    } else {
        match lane.total {
            Some(total) if total > 0 => {
                let ratio = lane.current.min(total) as f64 / total as f64;
                format!(
                    "{name}: {} ▕{}▏ {}/{}",
                    lane.label,
                    unicode_bar(ratio, 12),
                    lane.current.min(total),
                    total
                )
            }
            _ => format!("{name}: {} step {}", lane.label, lane.current),
        }
    };
    TextLine::from(Span::styled(label, Style::default().fg(color)))
}

fn dumb_render_lines(model: &VisualizerModel) -> Vec<String> {
    if model.current_stage == "init"
        && model.current_detail.is_empty()
        && model.diagnostics_lines.is_empty()
        && !model.current_cost.is_finite()
        && !model.current_grad.is_finite()
    {
        return Vec::new();
    }
    let mut lines = Vec::new();
    let elapsed = model.start_time.elapsed().as_secs();
    let status_class = classify_model_status(model);
    let detail = if model.current_detail.is_empty() {
        model.current_status.as_str()
    } else {
        model.current_detail.as_str()
    };
    lines.push(format!(
        "[{}] {} | {} | elapsed={}s",
        status_label(status_class),
        model.current_stage,
        detail,
        elapsed,
    ));
    if !model.primary_lane.label.is_empty() {
        lines.push(render_dumb_lane("Workflow", &model.primary_lane, model));
    }
    if !model.secondary_lane.label.is_empty() {
        lines.push(render_dumb_lane("Inner", &model.secondary_lane, model));
    }
    if let Some(last) = model.diagnostics_lines.last() {
        lines.push(format!("Note: {last}"));
    }
    lines
}

fn render_dumb_lane(prefix: &str, lane: &LaneState, model: &VisualizerModel) -> String {
    match lane.total {
        Some(total) if total > 0 => {
            let ratio = lane.current.min(total) as f64 / total as f64;
            let mut parts = vec![
                format!("{prefix}: {}", lane.label),
                format!(
                    "{:>3}% ▕{}▏",
                    (ratio * 100.0).round() as usize,
                    unicode_bar(ratio, 16)
                ),
            ];
            if let Some(eta) = estimate_eta(model, lane) {
                parts.push(format!("ETA: {eta}"));
            }
            if model.current_cost.is_finite() {
                parts.push(format!(
                    "objective={}",
                    format_metric(model.current_cost, "{:.4}")
                ));
            }
            if model.current_grad.is_finite() {
                parts.push(format!(
                    "|grad|={}",
                    format_metric(model.current_grad, "{:.3e}")
                ));
            }
            parts.join(" | ")
        }
        _ => {
            let mut parts = vec![
                format!("{prefix}: {}", lane.label),
                format!("step={}", lane.current),
            ];
            if model.current_cost.is_finite() {
                parts.push(format!(
                    "objective={}",
                    format_metric(model.current_cost, "{:.4}")
                ));
            }
            if model.current_grad.is_finite() {
                parts.push(format!(
                    "|grad|={}",
                    format_metric(model.current_grad, "{:.3e}")
                ));
            }
            parts.join(" | ")
        }
    }
}

fn estimate_eta(model: &VisualizerModel, lane: &LaneState) -> Option<String> {
    let Some(total) = lane.total else {
        return None;
    };
    if lane.current == 0 || lane.current >= total {
        return None;
    }
    let elapsed = model.start_time.elapsed().as_secs_f64();
    if elapsed < 1.0 {
        return None;
    }
    let rate = lane.current as f64 / elapsed.max(1e-6);
    if rate <= 0.0 {
        return None;
    }
    let remaining = total.saturating_sub(lane.current) as f64 / rate.max(1e-6);
    Some(format!("{:.0}s", remaining.max(0.0)))
}

fn format_metric(value: f64, fmt: &str) -> String {
    if value.is_finite() {
        if fmt == "{:.3e}" {
            format!("{value:.3e}")
        } else {
            format!("{value:.4}")
        }
    } else {
        "n/a".to_string()
    }
}

fn unicode_bar(ratio: f64, width: usize) -> String {
    const STEPS: [char; 9] = [' ', '▏', '▎', '▍', '▌', '▋', '▊', '▉', '█'];
    let clamped = ratio.clamp(0.0, 1.0);
    let filled = clamped * width as f64;
    let full = filled.floor() as usize;
    let partial = ((filled - full as f64) * 8.0).round() as usize;
    let mut out = String::with_capacity(width);
    for idx in 0..width {
        if idx < full {
            out.push('█');
        } else if idx == full && partial > 0 {
            out.push(STEPS[partial.min(8)]);
        } else {
            out.push(' ');
        }
    }
    out
}

fn normalize_total(total: usize) -> usize {
    total.max(1)
}

fn should_use_text_only_progress() -> bool {
    !io::stdout().is_terminal() || !io::stderr().is_terminal()
}

fn started_lane(label: &str, total: usize) -> LaneState {
    LaneState {
        label: label.to_string(),
        current: 0,
        total: Some(normalize_total(total)),
        done: false,
    }
}

fn advance_lane(lane: &mut LaneState, current: usize) {
    let next = match lane.total {
        Some(total) => current.min(total),
        None => current,
    };
    lane.current = lane.current.max(next);
    lane.done = false;
}

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

    #[test]
    fn unicode_bar_uses_partial_blocks() {
        let bar = unicode_bar(0.53, 8);
        assert!(bar.contains('█'));
        assert!(bar.contains('▎') || bar.contains('▍') || bar.contains('▌') || bar.contains('▋'));
    }

    #[test]
    fn dumb_renderer_throttles_to_seconds_and_formats_eta() {
        let mut session = VisualizerSession::new(false);
        session.model.start_time = Instant::now() - Duration::from_secs(4);
        session.set_stage("fit", "optimizing");
        session.start_workflow("REML", 10);
        session.advance_workflow(4);
        session.update(42.0, 1e-3, "optimizing", 4.0, "accepted", Some(10));
        let lines = dumb_render_lines(&session.model);
        assert!(lines.iter().any(|line| line.contains("Workflow: REML")));
        assert!(lines.iter().any(|line| line.contains("ETA:")));
    }

    #[test]
    fn dumb_renderer_suppresses_uninitialized_frames() {
        let session = VisualizerSession::new(false);
        let lines = dumb_render_lines(&session.model);
        assert!(lines.is_empty());
    }

    #[test]
    fn dumb_renderer_omits_placeholder_metrics() {
        let mut session = VisualizerSession::new(false);
        session.set_stage("fit", "loading survival data");
        session.start_workflow("Survival Fit", 5);
        session.advance_workflow(1);
        let lines = dumb_render_lines(&session.model);
        assert!(lines.iter().all(|line| !line.contains("n/a")));
        assert!(
            lines
                .iter()
                .any(|line| line.contains("Workflow: Survival Fit"))
        );
    }

    #[test]
    fn diagnostics_ring_buffer_drops_old_messages() {
        let mut session = VisualizerSession::new(false);
        for idx in 0..20 {
            session.push_diagnostic(&format!("line {idx}"));
        }
        assert_eq!(session.model.diagnostics_lines.len(), MAX_DIAGNOSTIC_LINES);
        assert_eq!(
            session.model.diagnostics_lines.first().map(String::as_str),
            Some("line 10")
        );
    }

    #[test]
    fn finishing_secondary_progress_clears_inner_lane() {
        let mut session = VisualizerSession::new(false);
        session.start_secondary_workflow("inner", 5);
        session.advance_secondary_workflow(2);
        assert_eq!(session.model.secondary_lane.label, "inner");
        session.finish_secondary_progress("done");
        assert!(session.model.secondary_lane.label.is_empty());
        assert_eq!(
            session.model.diagnostics_lines.last().map(String::as_str),
            Some("done")
        );
    }

    #[test]
    fn enabled_session_uses_dumb_mode_when_not_attached_to_tty() {
        let session = VisualizerSession::new(true);
        assert!(session.is_active());
        assert!(!session.is_interactive());
    }

    #[test]
    fn finish_progress_keeps_completion_diagnostic() {
        let mut session = VisualizerSession::new(false);
        session.start_workflow("outer", 10);
        session.advance_workflow(5);
        session.finish_progress("outer complete");
        assert_eq!(
            session.model.diagnostics_lines.last().map(String::as_str),
            Some("outer complete")
        );
        assert!(session.model.primary_lane.done);
        assert_eq!(session.model.primary_lane.current, 10);
    }

    #[test]
    fn workflow_normalizes_zero_total_and_never_goes_backwards() {
        let mut session = VisualizerSession::new(false);
        session.start_workflow("outer", 0);
        assert_eq!(session.model.primary_lane.total, Some(1));
        session.advance_workflow(1);
        session.advance_workflow(0);
        assert_eq!(session.model.primary_lane.current, 1);
    }

    #[test]
    fn secondary_workflow_ignores_advances_before_start() {
        let mut session = VisualizerSession::new(false);
        session.advance_secondary_workflow(3);
        assert!(session.model.secondary_lane.label.is_empty());
        session.start_secondary_workflow("inner", 2);
        session.advance_secondary_workflow(5);
        assert_eq!(session.model.secondary_lane.current, 2);
    }

    #[test]
    fn warning_status_is_reflected_in_dumb_render() {
        let mut session = VisualizerSession::new(false);
        session.set_stage("fit", "optimizing");
        session.push_diagnostic("warning: matrix ill-conditioned");
        let lines = dumb_render_lines(&session.model);
        assert!(lines.first().is_some_and(|line| line.contains("[WARNING]")));
    }
}
