use std::cmp::Ordering;
use std::fs::File;
use std::io::{self, Write};
use std::path::PathBuf;
use std::time::{Duration, Instant};

use chrono::{DateTime, Local};
use crossterm::event::{self, Event, KeyCode, KeyEventKind};
use crossterm::execute;
use crossterm::terminal::{
    disable_raw_mode, enable_raw_mode, EnterAlternateScreen, LeaveAlternateScreen,
};
use ratatui::backend::CrosstermBackend;
use ratatui::layout::{Alignment, Constraint, Direction, Layout, Margin, Rect};
use ratatui::prelude::{Color, Line, Modifier, Span, Style};
use ratatui::symbols;
use ratatui::widgets::{
    Block, Borders, Cell, Clear, List, ListItem, Padding, Paragraph, Row, Table, Wrap,
};
use ratatui::Terminal;
use lobe_core::engine::history::{compare_ttfb, RegressionSummary};
use lobe_core::engine::proxy::SimpleProbe;
use lobe_core::error::Result;
use lobe_core::models::{ProbeResult, ProbeStatus, TlsStatus};
use lobe_core::storage::SqliteStore;

const MAX_RECENT_RUNS: usize = 8;
const MAX_STATS_RUNS: usize = 50;

pub async fn run_watch_tui(
    store: SqliteStore,
    url: String,
    interval_secs: u64,
    threshold_percent: f64,
    count: Option<usize>,
) -> Result<()> {
    let mut stdout = io::stdout();
    enable_raw_mode()?;
    execute!(stdout, EnterAlternateScreen)?;
    let backend = CrosstermBackend::new(stdout);
    let mut terminal = Terminal::new(backend)?;
    terminal.clear()?;
    let _guard = TerminalGuard;

    let mut app = WatchApp::new(store, url, interval_secs, threshold_percent, count)?;

    while !app.should_quit {
        if app.needs_full_redraw {
            terminal.clear()?;
            app.needs_full_redraw = false;
        }

        terminal.draw(|frame| draw(frame, &app))?;

        if app.should_probe() {
            app.run_probe().await?;
        }

        if event::poll(Duration::from_millis(100))? {
            match event::read()? {
                Event::Key(key) => {
                    if key.kind == KeyEventKind::Press {
                        handle_key_event(&mut app, key.code).await?;
                    }
                }
                Event::Resize(_, _) | Event::FocusGained => {
                    app.needs_full_redraw = true;
                }
                _ => {}
            }
        }

        app.on_tick();
    }

    Ok(())
}

struct TerminalGuard;

impl Drop for TerminalGuard {
    fn drop(&mut self) {
        let _ = disable_raw_mode();
        let _ = execute!(io::stdout(), LeaveAlternateScreen);
    }
}

#[derive(Debug)]
struct WatchApp {
    store: SqliteStore,
    probe: SimpleProbe,
    target: String,
    interval_secs: u64,
    threshold_percent: f64,
    remaining_count: Option<usize>,
    paused: bool,
    should_quit: bool,
    force_probe: bool,
    history_expanded: bool,
    tls_detail_expanded: bool,
    help_visible: bool,
    clear_confirm_visible: bool,
    selected_history: usize,
    session_started_at: Instant,
    last_probe_at: Option<Instant>,
    pulse_on: bool,
    last_pulse_flip: Instant,
    current_result: Option<ProbeResult>,
    recent_runs: Vec<ProbeResult>,
    stats_runs: Vec<ProbeResult>,
    target_run_count: usize,
    last_comparison: Option<RegressionSummary>,
    comparison_note: Option<String>,
    session_results: Vec<ProbeResult>,
    flash_message: Option<String>,
    last_export_path: Option<PathBuf>,
    needs_full_redraw: bool,
}

impl WatchApp {
    fn new(
        store: SqliteStore,
        target: String,
        interval_secs: u64,
        threshold_percent: f64,
        remaining_count: Option<usize>,
    ) -> Result<Self> {
        let recent_runs = store.recent_results_for_target(&target, MAX_RECENT_RUNS)?;
        let stats_runs = store.recent_results_for_target(&target, MAX_STATS_RUNS)?;
        let target_run_count = store.count_results_for_target(&target)?;
        let current_result = recent_runs.first().cloned();
        let selected_history = 0;
        let last_comparison = if recent_runs.len() >= 2 {
            compare_ttfb(&recent_runs[1], &recent_runs[0], threshold_percent)
        } else {
            None
        };
        let comparison_note = if recent_runs.is_empty() {
            Some("waiting-for-first-run".to_string())
        } else if recent_runs.len() == 1 {
            Some("first-run".to_string())
        } else if last_comparison.is_none() {
            Some("previous-ttfb-zero".to_string())
        } else {
            None
        };

        Ok(Self {
            store,
            probe: SimpleProbe::new(),
            target,
            interval_secs,
            threshold_percent,
            remaining_count,
            paused: false,
            should_quit: false,
            force_probe: true,
            history_expanded: false,
            tls_detail_expanded: false,
            help_visible: false,
            clear_confirm_visible: false,
            selected_history,
            session_started_at: Instant::now(),
            last_probe_at: None,
            pulse_on: true,
            last_pulse_flip: Instant::now(),
            current_result,
            recent_runs,
            stats_runs,
            target_run_count,
            last_comparison,
            comparison_note,
            session_results: Vec::new(),
            flash_message: None,
            last_export_path: None,
            needs_full_redraw: false,
        })
    }

    fn should_probe(&self) -> bool {
        if self.paused {
            return self.force_probe;
        }

        if self.force_probe {
            return true;
        }

        match self.last_probe_at {
            Some(last_probe_at) => last_probe_at.elapsed() >= Duration::from_secs(self.interval_secs),
            None => true,
        }
    }

    async fn run_probe(&mut self) -> Result<()> {
        let result = self
            .probe
            .probe_and_store(&self.store, &self.target)
            .await?;

        self.last_probe_at = Some(Instant::now());
        self.force_probe = false;
        self.current_result = Some(result.clone());
        self.session_results.push(result);

        if let Some(remaining) = self.remaining_count.as_mut() {
            *remaining -= 1;
            if *remaining == 0 {
                self.should_quit = true;
            }
        }

        self.refresh_history()?;

        Ok(())
    }

    fn refresh_history(&mut self) -> Result<()> {
        self.recent_runs = self
            .store
            .recent_results_for_target(&self.target, MAX_RECENT_RUNS)?;
        self.stats_runs = self
            .store
            .recent_results_for_target(&self.target, MAX_STATS_RUNS)?;
        self.target_run_count = self.store.count_results_for_target(&self.target)?;

        if self.selected_history >= self.recent_runs.len() && !self.recent_runs.is_empty() {
            self.selected_history = self.recent_runs.len() - 1;
        }

        self.current_result = self.recent_runs.first().cloned();
        self.last_comparison = if self.recent_runs.len() >= 2 {
            compare_ttfb(&self.recent_runs[1], &self.recent_runs[0], self.threshold_percent)
        } else {
            None
        };
        self.comparison_note = if self.recent_runs.is_empty() {
            Some("waiting-for-first-run".to_string())
        } else if self.recent_runs.len() == 1 {
            Some("first-run".to_string())
        } else if self.last_comparison.is_none() {
            Some("previous-ttfb-zero".to_string())
        } else {
            None
        };

        Ok(())
    }

    fn on_tick(&mut self) {
        if self.last_pulse_flip.elapsed() >= Duration::from_millis(900) {
            self.pulse_on = !self.pulse_on;
            self.last_pulse_flip = Instant::now();
        }
    }

    fn move_selection_up(&mut self) {
        if self.selected_history > 0 {
            self.selected_history -= 1;
        }
    }

    fn move_selection_down(&mut self) {
        if self.selected_history + 1 < self.recent_runs.len() {
            self.selected_history += 1;
        }
    }

    fn success_failure_counts(&self) -> (usize, usize) {
        self.session_results.iter().fold((0, 0), |(ok, fail), result| {
            if matches!(result.status, ProbeStatus::Succeeded) {
                (ok + 1, fail)
            } else {
                (ok, fail + 1)
            }
        })
    }

    fn export_session_csv(&mut self) -> Result<()> {
        let path = PathBuf::from(format!(
            "/tmp/lobe-session-{}.csv",
            Local::now().format("%Y%m%d-%H%M%S")
        ));
        let mut file = File::create(&path)?;
        writeln!(
            file,
            "id,target,status,dns_ms,tcp_ms,tls_ms,ttfb_ms,total_ms,created_at_ms,error_message"
        )?;

        for result in &self.session_results {
            let error_message = result.error_message.as_deref().unwrap_or("").replace(',', ";");
            writeln!(
                file,
                "{},{},{},{},{},{},{},{},{},{}",
                result.id,
                result.target,
                format_status(result.status),
                result.report.dns_ms,
                result.report.tcp_ms,
                result.report.tls_ms,
                result.report.ttfb_ms,
                result.report.total_ms,
                result.created_at_ms,
                error_message
            )?;
        }

        self.last_export_path = Some(path.clone());
        self.flash_message = Some(format!("exported session to {}", path.display()));

        Ok(())
    }

    fn clear_target_history(&mut self) -> Result<()> {
        let deleted = self.store.delete_results_for_target(&self.target)?;
        self.current_result = None;
        self.recent_runs.clear();
        self.stats_runs.clear();
        self.target_run_count = 0;
        self.last_comparison = None;
        self.comparison_note = Some("history-cleared".to_string());
        self.selected_history = 0;
        self.session_results.clear();
        self.flash_message = Some(format!("cleared {deleted} saved runs for {}", self.target));
        self.clear_confirm_visible = false;

        Ok(())
    }

    fn display_run_number(&self, result: &ProbeResult) -> Option<usize> {
        self.display_run_number_in_slice(result, &self.stats_runs)
            .or_else(|| self.display_run_number_in_slice(result, &self.recent_runs))
    }

    fn display_run_number_in_slice(
        &self,
        result: &ProbeResult,
        runs: &[ProbeResult],
    ) -> Option<usize> {
        if self.target_run_count == 0 {
            return None;
        }

        runs.iter()
            .position(|run| run.id == result.id)
            .map(|index| self.target_run_count.saturating_sub(index))
            .filter(|display_number| *display_number > 0)
    }

    fn display_run_label(&self, result: &ProbeResult) -> String {
        self.display_run_number(result)
            .map(|display_number| format!("run #{display_number}"))
            .unwrap_or_else(|| "run #--".to_string())
    }
}

async fn handle_key_event(app: &mut WatchApp, key: KeyCode) -> Result<()> {
    if app.clear_confirm_visible {
        match key {
            KeyCode::Char('y') | KeyCode::Char('Y') => {
                app.clear_target_history()?;
            }
            KeyCode::Char('n') | KeyCode::Char('N') | KeyCode::Esc => {
                app.clear_confirm_visible = false;
                app.flash_message = Some("clear cancelled".to_string());
            }
            _ => {}
        }

        return Ok(());
    }

    match key {
        KeyCode::Char('q') => app.should_quit = true,
        KeyCode::Char('p') => {
            app.paused = !app.paused;
            app.flash_message = Some(if app.paused {
                "watch paused".to_string()
            } else {
                "watch resumed".to_string()
            });
        }
        KeyCode::Char('r') => app.force_probe = true,
        KeyCode::Char('h') => app.history_expanded = !app.history_expanded,
        KeyCode::Char('t') => app.tls_detail_expanded = !app.tls_detail_expanded,
        KeyCode::Char('e') => {
            app.export_session_csv()?;
        }
        KeyCode::Char('c') => {
            app.clear_confirm_visible = true;
            app.flash_message = Some("confirm clear with y/n".to_string());
        }
        KeyCode::Char('?') => app.help_visible = !app.help_visible,
        KeyCode::Up => app.move_selection_up(),
        KeyCode::Down => app.move_selection_down(),
        _ => {}
    }

    Ok(())
}

fn draw(frame: &mut ratatui::Frame<'_>, app: &WatchApp) {
    let areas = Layout::default()
        .direction(Direction::Vertical)
        .constraints([
            Constraint::Length(3),
            Constraint::Min(10),
            Constraint::Length(2),
        ])
        .split(frame.area());

    draw_header(frame, areas[0], app);
    draw_body(frame, areas[1], app);
    draw_footer(frame, areas[2], app);

    if app.help_visible {
        draw_help_overlay(frame);
    }

    if app.clear_confirm_visible {
        draw_clear_confirm_overlay(frame, app);
    }
}

fn draw_header(frame: &mut ratatui::Frame<'_>, area: Rect, app: &WatchApp) {
    let block = Block::default()
        .borders(Borders::BOTTOM)
        .border_style(Style::default().fg(Color::Rgb(33, 38, 45)));
    frame.render_widget(block, area);

    let inner = area.inner(Margin {
        vertical: 0,
        horizontal: 1,
    });
    let header_chunks = Layout::default()
        .direction(Direction::Horizontal)
        .constraints([Constraint::Min(20), Constraint::Length(28)])
        .split(inner);

    let session = format_elapsed(app.session_started_at.elapsed());
    let run_label = app
        .current_result
        .as_ref()
        .map(|result| app.display_run_label(result))
        .unwrap_or_else(|| "run #--".to_string());
    let header_left = Line::from(vec![
        Span::styled("lobe", dim_style()),
        Span::styled(" ▸ ", mute_style()),
        Span::styled(app.target.as_str(), cyan_bold_style()),
        Span::styled("  probe every ", mute_style()),
        Span::styled(format!("{}s", app.interval_secs), base_style()),
        Span::styled(" · session ", mute_style()),
        Span::styled(session, base_style()),
    ]);
    let header_right = Line::from(vec![
        Span::styled(
            if app.pulse_on { "● " } else { "○ " },
            Style::default().fg(Color::Rgb(86, 211, 100)),
        ),
        Span::styled(
            if app.paused { "PAUSED" } else { "WATCHING" },
            Style::default()
                .fg(if app.paused {
                    Color::Rgb(227, 179, 65)
                } else {
                    Color::Rgb(86, 211, 100)
                })
                .add_modifier(Modifier::BOLD),
        ),
        Span::raw("  "),
        Span::styled(run_label, dim_style()),
    ]);

    frame.render_widget(Paragraph::new(header_left), header_chunks[0]);
    frame.render_widget(
        Paragraph::new(header_right).alignment(Alignment::Right),
        header_chunks[1],
    );
}

fn draw_body(frame: &mut ratatui::Frame<'_>, area: Rect, app: &WatchApp) {
    let body_chunks = Layout::default()
        .direction(Direction::Horizontal)
        .constraints([Constraint::Length(28), Constraint::Min(40)])
        .margin(1)
        .split(area);

    draw_left_column(frame, body_chunks[0], app);
    draw_right_column(frame, body_chunks[1], app);
}

fn draw_left_column(frame: &mut ratatui::Frame<'_>, area: Rect, app: &WatchApp) {
    let tls_height = if app.tls_detail_expanded { 11 } else { 7 };
    let chunks = Layout::default()
        .direction(Direction::Vertical)
        .constraints([
            Constraint::Length(10),
            Constraint::Length(7),
            Constraint::Length(tls_height),
            Constraint::Min(8),
        ])
        .split(area);

    draw_latest_run(frame, chunks[0], app);
    draw_percentiles(frame, chunks[1], app);
    draw_tls_panel(frame, chunks[2], app);
    draw_request_panel(frame, chunks[3], app);
}

fn draw_right_column(frame: &mut ratatui::Frame<'_>, area: Rect, app: &WatchApp) {
    let chunks = if app.history_expanded {
        Layout::default()
            .direction(Direction::Vertical)
            .constraints([
                Constraint::Length(8),
                Constraint::Min(12),
                Constraint::Length(5),
            ])
            .split(area)
    } else {
        Layout::default()
            .direction(Direction::Vertical)
            .constraints([
                Constraint::Length(8),
                Constraint::Min(10),
                Constraint::Length(5),
            ])
            .split(area)
    };

    draw_timing_breakdown(frame, chunks[0], app);
    draw_recent_runs(frame, chunks[1], app);
    draw_regressions(frame, chunks[2], app);
}

fn draw_latest_run(frame: &mut ratatui::Frame<'_>, area: Rect, app: &WatchApp) {
    let title = panel_title("LATEST RUN", None);
    let block = panel_block(title, None);
    let inner = block.inner(area);
    frame.render_widget(block, area);

    let mut lines = Vec::new();
    if let Some(result) = &app.current_result {
        lines.extend(stat_lines(&[
            (
                "dns".to_string(),
                format!("{}ms", result.report.dns_ms),
                style_for_ms(result.report.dns_ms, 30),
            ),
            (
                "tcp".to_string(),
                format!("{}ms", result.report.tcp_ms),
                style_for_ms(result.report.tcp_ms, 30),
            ),
            (
                "tls".to_string(),
                format!("{}ms", result.report.tls_ms),
                style_for_ms(result.report.tls_ms, 60),
            ),
            (
                "ttfb".to_string(),
                format!("{}ms", result.report.ttfb_ms),
                style_for_ms(result.report.ttfb_ms, 120),
            ),
            (
                "total".to_string(),
                format!("{}ms", result.report.total_ms),
                white_style(),
            ),
        ]));
        lines.push(Line::from(" "));
        lines.extend(stat_lines(&[
            (
                "status".to_string(),
                display_http_status(result),
                style_for_result_status(result),
            ),
            (
                "bytes".to_string(),
                result
                    .response_bytes
                    .map(format_bytes)
                    .unwrap_or_else(|| "n/a".to_string()),
                base_style(),
            ),
            (
                "at".to_string(),
                format_time(result.created_at_ms),
                mute_style(),
            ),
        ]));
    } else {
        lines.push(Line::from(Span::styled("waiting for first run", dim_style())));
    }

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

fn draw_percentiles(frame: &mut ratatui::Frame<'_>, area: Rect, app: &WatchApp) {
    let sample_count = app.stats_runs.len();
    let title = panel_title("P50/P95", Some(format!("({sample_count} runs)")));
    let block = panel_block(title, None);
    let inner = block.inner(area);
    frame.render_widget(block, area);

    let lines = vec![
        percentile_line("dns", percentile_pair(&app.stats_runs, |run| run.report.dns_ms)),
        percentile_line("tcp", percentile_pair(&app.stats_runs, |run| run.report.tcp_ms)),
        percentile_line("tls", percentile_pair(&app.stats_runs, |run| run.report.tls_ms)),
        percentile_line("ttfb", percentile_pair(&app.stats_runs, |run| run.report.ttfb_ms)),
        percentile_line("total", percentile_pair(&app.stats_runs, |run| run.report.total_ms)),
    ];

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

fn draw_tls_panel(frame: &mut ratatui::Frame<'_>, area: Rect, app: &WatchApp) {
    let title = panel_title("TLS", None);
    let block = panel_block(title, None);
    let inner = block.inner(area);
    frame.render_widget(block, area);

    let lines = if let Some(result) = &app.current_result {
        if let Some(tls) = &result.tls {
            let handshake_text = match tls.status {
                TlsStatus::NotUsed => "not used".to_string(),
                TlsStatus::HandshakeSucceeded => "✓ verified".to_string(),
                TlsStatus::HandshakeFailed => "failed".to_string(),
            };
            let handshake_style = match tls.status {
                TlsStatus::NotUsed => mute_style(),
                TlsStatus::HandshakeSucceeded => green_style(),
                TlsStatus::HandshakeFailed => red_style(),
            };
            let mut lines = vec![
                tls_line("handshake", handshake_text, handshake_style),
                tls_line(
                    "version",
                    tls.version.as_deref().unwrap_or("n/a").to_string(),
                    if tls.version.is_some() { cyan_style() } else { mute_style() },
                ),
                tls_line(
                    "cipher",
                    tls.cipher_suite
                        .as_deref()
                        .unwrap_or("n/a")
                        .to_string(),
                    base_style(),
                ),
            ];

            let cert_expiry = tls
                .certificate
                .as_ref()
                .and_then(|certificate| certificate.not_after.as_ref())
                .cloned()
                .unwrap_or_else(|| "n/a".to_string());
            let cert_issuer = tls
                .certificate
                .as_ref()
                .map(|certificate| certificate.issuer.clone())
                .unwrap_or_else(|| "n/a".to_string());

            if app.tls_detail_expanded {
                let cert_subject = tls
                    .certificate
                    .as_ref()
                    .map(|certificate| certificate.subject.clone())
                    .unwrap_or_else(|| "n/a".to_string());
                let valid_from = tls
                    .certificate
                    .as_ref()
                    .and_then(|certificate| certificate.not_before.as_ref())
                    .cloned()
                    .unwrap_or_else(|| "n/a".to_string());

                lines.push(tls_line("cert expires", cert_expiry, yellow_style()));
                lines.push(tls_line("issuer", cert_issuer, mute_style()));
                lines.push(tls_line("subject", cert_subject, base_style()));
                lines.push(tls_line("valid from", valid_from, mute_style()));
                if let Some(error_message) = &tls.error_message {
                    lines.push(tls_line("detail", error_message.clone(), red_style()));
                }
            } else {
                lines.push(tls_line("cert expires", cert_expiry, yellow_style()));
                lines.push(tls_line("issuer", cert_issuer, mute_style()));
            }

            lines
        } else {
            vec![Line::from(Span::styled("no tls data", dim_style()))]
        }
    } else {
        vec![Line::from(Span::styled("waiting for first run", dim_style()))]
    };

    frame.render_widget(Paragraph::new(lines).wrap(Wrap { trim: false }), inner);
}

fn draw_request_panel(frame: &mut ratatui::Frame<'_>, area: Rect, app: &WatchApp) {
    let title = panel_title("REQUEST", None);
    let block = panel_block(title, None);
    let inner = block.inner(area);
    frame.render_widget(block, area);

    let result = app
        .recent_runs
        .get(app.selected_history)
        .or(app.current_result.as_ref());

    let lines = if let Some(result) = result {
        let host = url::Url::parse(&result.target)
            .ok()
            .and_then(|url| url.host_str().map(|host| host.to_string()))
            .unwrap_or_else(|| "n/a".to_string());
        let scheme = url::Url::parse(&result.target)
            .ok()
            .map(|url| url.scheme().to_string())
            .unwrap_or_else(|| "n/a".to_string());

        let mut lines = vec![
            tls_line("path", result.request_path.clone(), cyan_style()),
            tls_line("host", host, base_style()),
            tls_line("scheme", scheme, mute_style()),
            tls_line("response", display_http_status(result), style_for_result_status(result)),
            tls_line(
                "bytes",
                result
                    .response_bytes
                    .map(format_bytes)
                    .unwrap_or_else(|| "n/a".to_string()),
                base_style(),
            ),
            tls_line(
                "run",
                app.display_run_label(result).replace("run ", ""),
                dim_style(),
            ),
        ];

        if let Some(error_message) = &result.error_message {
            lines.push(Line::from(" "));
            lines.push(Line::from(vec![
                Span::styled("error       ", mute_style()),
                Span::styled(error_message.clone(), red_style()),
            ]));
        }

        lines
    } else {
        vec![Line::from(Span::styled("waiting for first run", dim_style()))]
    };

    frame.render_widget(Paragraph::new(lines).wrap(Wrap { trim: false }), inner);
}

fn draw_timing_breakdown(frame: &mut ratatui::Frame<'_>, area: Rect, app: &WatchApp) {
    let title_suffix = app
        .current_result
        .as_ref()
        .map(|result| format!("{} · {}ms total", app.display_run_label(result), result.report.total_ms));
    let block = panel_block(panel_title("TIMING BREAKDOWN", None), title_suffix);
    let inner = block.inner(area);
    frame.render_widget(block, area);

    if let Some(result) = &app.current_result {
        let rows = Layout::default()
            .direction(Direction::Vertical)
            .constraints([
                Constraint::Length(1),
                Constraint::Length(1),
                Constraint::Length(1),
                Constraint::Length(1),
                Constraint::Length(1),
            ])
            .split(inner);
        let total = result.report.total_ms.max(1);

        render_phase_row(
            frame,
            rows[0],
            "dns",
            result.report.dns_ms,
            total,
            Color::Rgb(57, 208, 200),
        );
        render_phase_row(
            frame,
            rows[1],
            "tcp",
            result.report.tcp_ms,
            total,
            Color::Rgb(121, 192, 255),
        );
        render_phase_row(
            frame,
            rows[2],
            "tls",
            result.report.tls_ms,
            total,
            Color::Rgb(164, 143, 255),
        );
        render_phase_row(
            frame,
            rows[3],
            "ttfb",
            result.report.ttfb_ms,
            total,
            Color::Rgb(227, 179, 65),
        );
        render_phase_row(
            frame,
            rows[4],
            "total",
            result.report.total_ms,
            total,
            Color::Rgb(132, 141, 151),
        );
    } else {
        frame.render_widget(
            Paragraph::new("waiting for first run").style(dim_style()),
            inner,
        );
    }
}

fn draw_recent_runs(frame: &mut ratatui::Frame<'_>, area: Rect, app: &WatchApp) {
    let suffix = format!("last {} · sorted newest first", app.recent_runs.len());
    let block = panel_block(
        panel_title("RECENT RUNS", None),
        Some(suffix),
    );
    let inner = block.inner(area);
    frame.render_widget(block, area);

    let header = Row::new(vec![
        Cell::from("#").style(dim_style()),
        Cell::from("time").style(dim_style()),
        Cell::from("dns").style(dim_style()),
        Cell::from("tls").style(dim_style()),
        Cell::from("ttfb").style(dim_style()),
        Cell::from("total").style(dim_style()),
        Cell::from("status").style(dim_style()),
    ]);

    let rows = app.recent_runs.iter().enumerate().map(|(index, result)| {
        let highlight = index == app.selected_history;
        let base = if highlight {
            Style::default().bg(Color::Rgb(13, 43, 22))
        } else {
            Style::default()
        };
        let display_run_number = app
            .display_run_number(result)
            .map(|value| value.to_string())
            .unwrap_or_else(|| "--".to_string());
        Row::new(vec![
            Cell::from(display_run_number).style(base.fg(Color::Rgb(132, 141, 151))),
            Cell::from(format_time(result.created_at_ms)).style(base.fg(Color::Rgb(110, 118, 129))),
            Cell::from(format!("{}ms", result.report.dns_ms)).style(base.patch(style_for_ms(result.report.dns_ms, 30))),
            Cell::from(format!("{}ms", result.report.tls_ms)).style(base.patch(style_for_ms(result.report.tls_ms, 60))),
            Cell::from(format!("{}ms", result.report.ttfb_ms)).style(base.patch(style_for_ms(result.report.ttfb_ms, 120))),
            Cell::from(format!("{}ms", result.report.total_ms)).style(base.patch(white_style())),
            Cell::from(status_badge(result)).style(base.patch(style_for_result_status(result))),
        ])
    });

    let table = Table::new(
        rows,
        [
            Constraint::Length(4),
            Constraint::Length(10),
            Constraint::Length(8),
            Constraint::Length(8),
            Constraint::Length(9),
            Constraint::Length(9),
            Constraint::Length(8),
        ],
    )
    .header(header)
    .column_spacing(1);

    frame.render_widget(table, inner);
}

fn draw_regressions(frame: &mut ratatui::Frame<'_>, area: Rect, app: &WatchApp) {
    let block = panel_block(
        panel_title("REGRESSIONS", None),
        Some("vs p50 baseline".to_string()),
    )
    .border_style(Style::default().fg(Color::Rgb(94, 40, 40)));
    let inner = block.inner(area);
    frame.render_widget(block, area);

    let items = build_regression_items(app)
        .into_iter()
        .map(ListItem::new)
        .collect::<Vec<_>>();

    let list = if items.is_empty() {
        List::new(vec![ListItem::new(Line::from(Span::styled(
            "no regressions above baseline",
            dim_style(),
        )))])
    } else {
        List::new(items)
    };

    frame.render_widget(list, inner);
}

fn draw_footer(frame: &mut ratatui::Frame<'_>, area: Rect, app: &WatchApp) {
    let block = Block::default()
        .borders(Borders::TOP)
        .border_style(Style::default().fg(Color::Rgb(33, 38, 45)));
    frame.render_widget(block, area);

    let inner = area.inner(Margin {
        vertical: 0,
        horizontal: 1,
    });
    let chunks = Layout::default()
        .direction(Direction::Horizontal)
        .constraints([Constraint::Min(40), Constraint::Length(36)])
        .split(inner);

    let left = Line::from(vec![
        footer_key("q", "quit"),
        Span::raw(" "),
        footer_key("p", "pause"),
        Span::raw(" "),
        footer_key("r", "probe now"),
        Span::raw(" "),
        footer_key("h", "history"),
        Span::raw(" "),
        footer_key("t", "tls detail"),
        Span::raw(" "),
        footer_key("e", "export"),
        Span::raw(" "),
        footer_key("c", "clear"),
        Span::raw(" "),
        footer_key("?", "help"),
    ]);

    let (ok_count, fail_count) = app.success_failure_counts();
    let db_hint = app
        .last_export_path
        .as_ref()
        .map(|path| path.display().to_string())
        .or_else(|| app.flash_message.clone())
        .unwrap_or_else(|| "db: ~/.lobe/history.db".to_string());
    let right = Line::from(vec![
        Span::styled(db_hint, dim_style()),
        Span::raw("  "),
        Span::styled("✓", green_style()),
        Span::raw(" "),
        Span::styled(format!("{ok_count}/{}", app.session_results.len()), green_style()),
        Span::raw("  "),
        Span::styled("✕", red_style()),
        Span::raw(" "),
        Span::styled(format!("{fail_count}/{}", app.session_results.len()), red_style()),
    ]);

    frame.render_widget(Paragraph::new(left), chunks[0]);
    frame.render_widget(Paragraph::new(right).alignment(Alignment::Right), chunks[1]);
}

fn draw_help_overlay(frame: &mut ratatui::Frame<'_>) {
    let area = centered_rect(58, 52, frame.area());
    frame.render_widget(Clear, area);
    let block = Block::default()
        .title(Span::styled(" Help ", white_style()))
        .borders(Borders::ALL)
        .border_style(Style::default().fg(Color::Rgb(48, 54, 61)))
        .padding(Padding::uniform(1))
        .style(Style::default().bg(Color::Rgb(14, 17, 23)));
    let inner = block.inner(area);
    frame.render_widget(block, area);

    let lines = vec![
        Line::from(vec![footer_key("q", "quit"), Span::raw(" exit the TUI")]),
        Line::from(vec![footer_key("p", "pause"), Span::raw(" pause or resume probing")]),
        Line::from(vec![footer_key("r", "probe"), Span::raw(" run a probe immediately")]),
        Line::from(vec![footer_key("h", "history"), Span::raw(" expand or compress history area")]),
        Line::from(vec![footer_key("t", "tls"), Span::raw(" expand TLS detail panel")]),
        Line::from(vec![footer_key("e", "export"), Span::raw(" export current session to CSV")]),
        Line::from(vec![footer_key("c", "clear"), Span::raw(" clear saved history for current target")]),
        Line::from(vec![footer_key("↑↓", "select"), Span::raw(" move history selection")]),
        Line::from(vec![footer_key("?", "help"), Span::raw(" toggle this overlay")]),
    ];

    frame.render_widget(Paragraph::new(lines).wrap(Wrap { trim: false }), inner);
}

fn draw_clear_confirm_overlay(frame: &mut ratatui::Frame<'_>, app: &WatchApp) {
    let area = centered_rect(64, 22, frame.area());
    frame.render_widget(Clear, area);
    let block = Block::default()
        .title(Span::styled(" Clear History ", white_style()))
        .borders(Borders::ALL)
        .border_style(Style::default().fg(Color::Rgb(94, 40, 40)))
        .padding(Padding::uniform(1))
        .style(Style::default().bg(Color::Rgb(14, 17, 23)));
    let inner = block.inner(area);
    frame.render_widget(block, area);

    let lines = vec![
        Line::from(Span::styled(
            format!("Clear saved history for {} ?", app.target),
            base_style(),
        )),
        Line::from(" "),
        Line::from(vec![
            footer_key("y", "yes"),
            Span::raw(" "),
            footer_key("n", "no"),
        ]),
    ];

    frame.render_widget(
        Paragraph::new(lines)
            .alignment(Alignment::Center)
            .wrap(Wrap { trim: false }),
        inner,
    );
}

fn panel_block(title: Line<'static>, right_label: Option<String>) -> Block<'static> {
    let title_line = if let Some(label) = right_label {
        Line::from(vec![
            Span::raw(" "),
            Span::styled("▸ ", mute_style()),
            Span::styled(extract_title_text(&title), dim_style().add_modifier(Modifier::BOLD)),
            Span::styled(format!("{:>1$}", label, 44), dim_style()),
        ])
    } else {
        title
    };

    Block::default()
        .title(title_line)
        .borders(Borders::ALL)
        .border_style(Style::default().fg(Color::Rgb(48, 54, 61)))
        .padding(Padding::new(1, 1, 1, 0))
        .style(Style::default().bg(Color::Rgb(14, 17, 23)))
}

fn panel_title(title: &str, suffix: Option<String>) -> Line<'static> {
    let mut spans = vec![
        Span::styled("▸ ", mute_style()),
        Span::styled(title.to_string(), dim_style().add_modifier(Modifier::BOLD)),
    ];
    if let Some(suffix) = suffix {
        spans.push(Span::raw(" "));
        spans.push(Span::styled(suffix, dim_style()));
    }

    Line::from(spans)
}

fn render_phase_row(
    frame: &mut ratatui::Frame<'_>,
    area: Rect,
    label: &str,
    value_ms: u64,
    total_ms: u64,
    bar_color: Color,
) {
    let chunks = Layout::default()
        .direction(Direction::Horizontal)
        .constraints([
            Constraint::Length(6),
            Constraint::Min(10),
            Constraint::Length(8),
            Constraint::Length(5),
        ])
        .split(area);

    frame.render_widget(Paragraph::new(label).style(mute_style()), chunks[0]);

    let percent = if total_ms == 0 {
        0.0
    } else {
        value_ms as f64 / total_ms as f64
    };
    let bar_width = chunks[1].width.saturating_sub(1) as usize;
    let filled = ((bar_width as f64) * percent).round() as usize;
    let mut spans = Vec::with_capacity(bar_width);
    for index in 0..bar_width {
        let style = if index < filled {
            Style::default().fg(bar_color)
        } else {
            Style::default().fg(Color::Rgb(33, 38, 45))
        };
        spans.push(Span::styled(symbols::block::FULL, style));
    }
    frame.render_widget(Paragraph::new(Line::from(spans)), chunks[1]);
    frame.render_widget(
        Paragraph::new(format!("{}ms", value_ms)).style(style_for_ms(value_ms, 100)),
        chunks[2],
    );
    frame.render_widget(
        Paragraph::new(format!("{:>3}%", (percent * 100.0).round() as u64)).style(dim_style()),
        chunks[3],
    );
}

fn build_regression_items(app: &WatchApp) -> Vec<Line<'static>> {
    let Some(baseline) = percentile(&app.stats_runs, |run| run.report.ttfb_ms, 0.50) else {
        return Vec::new();
    };
    let Some(dns_baseline) = percentile(&app.stats_runs, |run| run.report.dns_ms, 0.50) else {
        return Vec::new();
    };

    let mut alerts = Vec::new();
    for run in &app.recent_runs {
        if baseline > 0 && run.report.ttfb_ms > baseline {
            let delta_percent = ((run.report.ttfb_ms as f64 - baseline as f64) / baseline as f64) * 100.0;
            if delta_percent >= app.threshold_percent {
                alerts.push((
                    delta_percent,
                    Line::from(vec![
                        Span::styled("▲ ", red_style()),
                        Span::styled(format!("{} · ttfb spike: ", app.display_run_label(run)), base_style()),
                        Span::styled(format!("{}ms", run.report.ttfb_ms), red_style()),
                        Span::styled(format!(" vs baseline {}ms", baseline), dim_style()),
                        Span::raw(" "),
                        Span::styled(format!("+{:.0}%", delta_percent), red_style()),
                    ]),
                ));
            }
        }

        if dns_baseline > 0 && run.report.dns_ms > dns_baseline {
            let delta_percent =
                ((run.report.dns_ms as f64 - dns_baseline as f64) / dns_baseline as f64) * 100.0;
            if delta_percent >= app.threshold_percent {
                alerts.push((
                    delta_percent,
                    Line::from(vec![
                        Span::styled("▲ ", yellow_style()),
                        Span::styled(format!("{} · dns elevated: ", app.display_run_label(run)), base_style()),
                        Span::styled(format!("{}ms", run.report.dns_ms), yellow_style()),
                        Span::styled(format!(" vs baseline {}ms", dns_baseline), dim_style()),
                        Span::raw(" "),
                        Span::styled(format!("+{:.0}%", delta_percent), yellow_style()),
                    ]),
                ));
            }
        }
    }

    alerts.sort_by(|a, b| b.0.partial_cmp(&a.0).unwrap_or(Ordering::Equal));
    alerts.into_iter().take(2).map(|(_, line)| line).collect()
}

fn stat_lines(items: &[(String, String, Style)]) -> Vec<Line<'static>> {
    items.iter()
        .map(|(label, value, style)| {
            Line::from(vec![
                Span::styled(format!("{label:<8}"), mute_style()),
                Span::styled(value.clone(), *style),
            ])
        })
        .collect()
}

fn percentile_line(label: &str, values: Option<(u64, u64)>) -> Line<'static> {
    let value = values
        .map(|(p50, p95)| format!("{p50}ms / {p95}ms"))
        .unwrap_or_else(|| "n/a / n/a".to_string());

    Line::from(vec![
        Span::styled(format!("{label:<8}"), mute_style()),
        Span::styled(value, base_style()),
    ])
}

fn percentile_pair<F>(runs: &[ProbeResult], selector: F) -> Option<(u64, u64)>
where
    F: Fn(&ProbeResult) -> u64 + Copy,
{
    let p50 = percentile(runs, selector, 0.50)?;
    let p95 = percentile(runs, selector, 0.95)?;
    Some((p50, p95))
}

fn percentile<F>(runs: &[ProbeResult], selector: F, percentile: f64) -> Option<u64>
where
    F: Fn(&ProbeResult) -> u64,
{
    if runs.is_empty() {
        return None;
    }

    let mut values = runs.iter().map(selector).collect::<Vec<_>>();
    values.sort_unstable();
    let index = ((values.len() - 1) as f64 * percentile).round() as usize;
    values.get(index).copied()
}

fn tls_line(label: &str, value: String, style: Style) -> Line<'static> {
    Line::from(vec![
        Span::styled(format!("{label:<12}"), mute_style()),
        Span::styled(value, style),
    ])
}

fn centered_rect(horizontal_percent: u16, vertical_percent: u16, area: Rect) -> Rect {
    let vertical = Layout::default()
        .direction(Direction::Vertical)
        .constraints([
            Constraint::Percentage((100 - vertical_percent) / 2),
            Constraint::Percentage(vertical_percent),
            Constraint::Percentage((100 - vertical_percent) / 2),
        ])
        .split(area);

    Layout::default()
        .direction(Direction::Horizontal)
        .constraints([
            Constraint::Percentage((100 - horizontal_percent) / 2),
            Constraint::Percentage(horizontal_percent),
            Constraint::Percentage((100 - horizontal_percent) / 2),
        ])
        .split(vertical[1])[1]
}

fn footer_key(key: &str, label: &str) -> Span<'static> {
    Span::styled(
        format!(" {key} {label} "),
        Style::default()
            .fg(Color::Rgb(139, 148, 158))
            .bg(Color::Rgb(33, 38, 45)),
    )
}

fn extract_title_text(line: &Line<'_>) -> String {
    line.spans
        .iter()
        .map(|span| span.content.as_ref())
        .collect::<String>()
        .trim()
        .trim_start_matches('▸')
        .trim()
        .to_string()
}

fn format_elapsed(duration: Duration) -> String {
    let minutes = duration.as_secs() / 60;
    let seconds = duration.as_secs() % 60;
    format!("00:{minutes:02}:{seconds:02}")
}

fn format_time(created_at_ms: i64) -> String {
    let created_at = DateTime::<Local>::from(std::time::UNIX_EPOCH + Duration::from_millis(created_at_ms as u64));
    created_at.format("%H:%M:%S").to_string()
}

fn format_status(status: ProbeStatus) -> &'static str {
    match status {
        ProbeStatus::Succeeded => "succeeded",
        ProbeStatus::Failed => "failed",
    }
}

fn style_for_status(status: ProbeStatus) -> Style {
    match status {
        ProbeStatus::Succeeded => green_style(),
        ProbeStatus::Failed => red_style(),
    }
}

fn status_badge(result: &ProbeResult) -> String {
    match result.response_status_code {
        Some(code) => code.to_string(),
        None => status_badge_from_probe_status(result.status),
    }
}

fn status_badge_from_probe_status(status: ProbeStatus) -> String {
    match status {
        ProbeStatus::Succeeded => "OK".to_string(),
        ProbeStatus::Failed => "ERR".to_string(),
    }
}

fn style_for_result_status(result: &ProbeResult) -> Style {
    match result.response_status_code {
        Some(code) if (200..300).contains(&code) => green_style(),
        Some(code) if (300..400).contains(&code) => yellow_style(),
        Some(code) if (400..600).contains(&code) => red_style(),
        Some(_) => base_style(),
        None => style_for_status(result.status),
    }
}

fn display_http_status(result: &ProbeResult) -> String {
    match (result.response_status_code, result.response_status_text.as_deref()) {
        (Some(code), Some(text)) => format!("{code} {text}"),
        (Some(code), None) => code.to_string(),
        (None, _) if matches!(result.status, ProbeStatus::Failed) => "ERR".to_string(),
        _ => format_status(result.status).to_string(),
    }
}

fn format_bytes(bytes: u64) -> String {
    if bytes >= 1024 {
        format!("{:.1} KB", bytes as f64 / 1024.0)
    } else {
        format!("{bytes} B")
    }
}

fn style_for_ms(value_ms: u64, warn_threshold: u64) -> Style {
    if value_ms == 0 {
        base_style()
    } else if value_ms >= warn_threshold {
        yellow_style()
    } else {
        green_style()
    }
}

fn cyan_bold_style() -> Style {
    Style::default()
        .fg(Color::Rgb(121, 192, 255))
        .add_modifier(Modifier::BOLD)
}

fn white_style() -> Style {
    Style::default()
        .fg(Color::Rgb(230, 237, 243))
        .add_modifier(Modifier::BOLD)
}

fn base_style() -> Style {
    Style::default().fg(Color::Rgb(201, 209, 217))
}

fn green_style() -> Style {
    Style::default().fg(Color::Rgb(86, 211, 100))
}

fn yellow_style() -> Style {
    Style::default().fg(Color::Rgb(227, 179, 65))
}

fn red_style() -> Style {
    Style::default().fg(Color::Rgb(248, 81, 73))
}

fn cyan_style() -> Style {
    Style::default().fg(Color::Rgb(121, 192, 255))
}

fn mute_style() -> Style {
    Style::default().fg(Color::Rgb(110, 118, 129))
}

fn dim_style() -> Style {
    Style::default().fg(Color::Rgb(132, 141, 151))
}
