use std::collections::{BTreeSet, HashMap};
use std::fs::File;
use std::io;
use std::path::PathBuf;
use std::sync::mpsc::{self, Receiver};
use std::time::{Duration, Instant};

use chrono::{DateTime, Local};
use crossterm::event::{self, Event, KeyCode, KeyEventKind};
use crossterm::execute;
use crossterm::event::{DisableFocusChange, EnableFocusChange};
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::widgets::{Block, Borders, Cell, Clear, Padding, Paragraph, Row, Table, Wrap};
use ratatui::Terminal;
use lobe_core::engine::capture::{CaptureProxy, CaptureSessionExport, CapturedExchange};
use lobe_core::error::Result;
use lobe_core::models::TlsStatus;

use crate::upload;

const MAX_CAPTURED_EVENTS: usize = 300;

pub async fn run_capture_tui(
    listen_addr: String,
    upstream: String,
    auto_upload: bool,
) -> Result<()> {
    let mut stdout = io::stdout();
    enable_raw_mode()?;
    execute!(stdout, EnterAlternateScreen, EnableFocusChange)?;
    let backend = CrosstermBackend::new(stdout);
    let mut terminal = Terminal::new(backend)?;
    terminal.clear()?;
    let _guard = TerminalGuard;

    let (sender, receiver) = mpsc::channel();
    let proxy = CaptureProxy::new(listen_addr.clone(), &upstream, sender.clone())?;
    let listen_label = proxy.listen_addr().to_string();
    let upstream_label = proxy.upstream().to_string();

    tokio::spawn(async move {
        if let Err(error) = proxy.run().await {
            let _ = sender.send(CapturedExchange {
                target: upstream_label,
                request_host: "n/a".to_string(),
                request_method: "SYSTEM".to_string(),
                request_path: "/".to_string(),
                request_headers: Vec::new(),
                response_headers: Vec::new(),
                protocol_version: None,
                redirect_location: None,
                response_status_code: Some(502),
                response_status_text: Some("Proxy Error".to_string()),
                response_bytes: 0,
                report: Default::default(),
                tls: None,
                error_message: Some(error.to_string()),
                created_at_ms: Local::now().timestamp_millis(),
                connection_reuse: "new".to_string(),
            });
        }
    });

    let mut app = CaptureApp::new(listen_label, upstream, receiver, auto_upload);
    let mut last_forced_refresh = Instant::now();

    while !app.should_quit {
        // Safety net for terminals that don't send focus events when the
        // user switches tabs — force a full redraw every ~3 seconds so the
        // screen never stays blank after coming back to the terminal.
        if last_forced_refresh.elapsed() >= Duration::from_secs(3) {
            app.needs_full_redraw = true;
            last_forced_refresh = Instant::now();
        }

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

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

        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);
                    }
                }
                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(), DisableFocusChange, LeaveAlternateScreen);
    }
}

#[derive(Debug, Clone, Copy, PartialEq, Eq)]
enum DetailPane {
    Request,
    Tls,
}

#[derive(Debug, Clone)]
struct RouteSummary {
    path: String,
    request_count: usize,
    failure_count: usize,
    average_total_ms: u64,
    average_ttfb_ms: u64,
    p95_total_ms: u64,
    p95_ttfb_ms: u64,
}

#[derive(Debug, Clone)]
struct HostSummary {
    host: String,
    request_count: usize,
    failure_count: usize,
    average_total_ms: u64,
}

#[derive(Debug)]
struct CaptureApp {
    listen_addr: String,
    upstream: String,
    receiver: Receiver<CapturedExchange>,
    events: Vec<CapturedExchange>,
    paused_buffer: Vec<CapturedExchange>,
    paused: bool,
    should_quit: bool,
    help_visible: bool,
    selected: usize,
    method_filter_index: usize,
    path_filter_index: usize,
    detail_pane: DetailPane,
    summary_mode: bool,
    session_started_at: Instant,
    pulse_on: bool,
    last_pulse_flip: Instant,
    flash_message: Option<String>,
    needs_full_redraw: bool,
    auto_upload: bool,
}

impl CaptureApp {
    fn new(
        listen_addr: String,
        upstream: String,
        receiver: Receiver<CapturedExchange>,
        auto_upload: bool,
    ) -> Self {
        let flash = if auto_upload {
            "browse the local proxy URL to capture requests · press `u` to upload".to_string()
        } else {
            "browse the local proxy URL to capture real requests".to_string()
        };
        Self {
            listen_addr,
            upstream,
            receiver,
            events: Vec::new(),
            paused_buffer: Vec::new(),
            paused: false,
            should_quit: false,
            help_visible: false,
            selected: 0,
            method_filter_index: 0,
            path_filter_index: 0,
            detail_pane: DetailPane::Request,
            summary_mode: false,
            session_started_at: Instant::now(),
            pulse_on: true,
            last_pulse_flip: Instant::now(),
            flash_message: Some(flash),
            needs_full_redraw: false,
            auto_upload,
        }
    }

    fn drain_events(&mut self) {
        while let Ok(exchange) = self.receiver.try_recv() {
            if self.paused {
                self.paused_buffer.push(exchange);
            } else {
                self.push_event(exchange);
            }
        }
    }

    fn push_event(&mut self, exchange: CapturedExchange) {
        self.events.insert(0, exchange);
        if self.events.len() > MAX_CAPTURED_EVENTS {
            self.events.truncate(MAX_CAPTURED_EVENTS);
        }
        self.selected = 0;
    }

    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 toggle_pause(&mut self) {
        self.paused = !self.paused;
        if !self.paused {
            let pending = std::mem::take(&mut self.paused_buffer);
            for exchange in pending.into_iter().rev() {
                self.push_event(exchange);
            }
            self.flash_message = Some("capture resumed".to_string());
        } else {
            self.flash_message = Some("capture paused".to_string());
        }
    }

    fn cycle_method_filter(&mut self) {
        let len = self.method_filters().len();
        self.method_filter_index = (self.method_filter_index + 1) % len.max(1);
        self.selected = 0;
        self.flash_message = Some(format!("method filter: {}", self.current_method_filter_label()));
    }

    fn cycle_path_filter(&mut self) {
        let len = self.path_filters().len();
        self.path_filter_index = (self.path_filter_index + 1) % len.max(1);
        self.selected = 0;
        self.flash_message = Some(format!("path filter: {}", self.current_path_filter_label()));
    }

    fn toggle_detail_pane(&mut self) {
        self.detail_pane = match self.detail_pane {
            DetailPane::Request => DetailPane::Tls,
            DetailPane::Tls => DetailPane::Request,
        };
        self.flash_message = Some(match self.detail_pane {
            DetailPane::Request => "detail pane: request".to_string(),
            DetailPane::Tls => "detail pane: tls".to_string(),
        });
    }

    fn toggle_summary_mode(&mut self) {
        self.summary_mode = !self.summary_mode;
        self.flash_message = Some(if self.summary_mode {
            "summary view".to_string()
        } else {
            "request stream view".to_string()
        });
    }

    fn method_filters(&self) -> Vec<String> {
        let mut values = BTreeSet::new();
        for event in &self.events {
            values.insert(event.request_method.clone());
        }

        let mut filters = vec!["ALL".to_string()];
        filters.extend(values);
        filters
    }

    fn path_filters(&self) -> Vec<String> {
        let mut values = BTreeSet::new();
        for event in &self.events {
            values.insert(event.request_path.clone());
        }

        let mut filters = vec!["ALL".to_string()];
        filters.extend(values);
        filters
    }

    fn current_method_filter_label(&self) -> String {
        self.method_filters()
            .get(self.method_filter_index)
            .cloned()
            .unwrap_or_else(|| "ALL".to_string())
    }

    fn current_path_filter_label(&self) -> String {
        self.path_filters()
            .get(self.path_filter_index)
            .cloned()
            .unwrap_or_else(|| "ALL".to_string())
    }

    fn filtered_events(&self) -> Vec<&CapturedExchange> {
        let method_filter = self.current_method_filter_label();
        let path_filter = self.current_path_filter_label();

        self.events
            .iter()
            .filter(|event| {
                if method_filter == "ALL" {
                    true
                } else {
                    event.request_method.eq_ignore_ascii_case(&method_filter)
                }
            })
            .filter(|event| {
                if path_filter == "ALL" {
                    true
                } else {
                    event.request_path == path_filter
                }
            })
            .collect()
    }

    fn selected_event(&self) -> Option<&CapturedExchange> {
        self.filtered_events().get(self.selected).copied()
    }

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

    fn move_selection_down(&mut self) {
        let filtered_len = self.filtered_events().len();
        if self.selected + 1 < filtered_len {
            self.selected += 1;
        }
    }

    fn clear_session(&mut self) {
        self.events.clear();
        self.paused_buffer.clear();
        self.selected = 0;
        self.method_filter_index = 0;
        self.path_filter_index = 0;
        self.flash_message = Some("capture session cleared".to_string());
    }

    fn export_session_json(&mut self) -> Result<()> {
        let path = PathBuf::from(format!(
            "/tmp/lobe-capture-session-{}.json",
            Local::now().format("%Y%m%d-%H%M%S")
        ));
        let file = File::create(&path)?;
        let payload = CaptureSessionExport {
            version: 1,
            listen_addr: self.listen_addr.clone(),
            upstream: self.upstream.clone(),
            exported_at_ms: Local::now().timestamp_millis(),
            event_count: self.events.len(),
            events: self.events.iter().rev().cloned().collect(),
        };

        serde_json::to_writer_pretty(file, &payload)
            .map_err(|error| io::Error::other(error.to_string()))?;

        self.flash_message = Some(format!("✎ Exner exported to {}", path.display()));
        Ok(())
    }

    /// Build the same session payload as `export_session_json` and POST it to
    /// the user's Lobe cloud. Called from the `u` key. Blocks the TUI for the
    /// duration of the HTTP call — usually well under a second.
    fn upload_session_now(&mut self) {
        if self.events.is_empty() {
            self.flash_message = Some("no requests captured yet — nothing to upload".to_string());
            return;
        }

        let payload = CaptureSessionExport {
            version: 1,
            listen_addr: self.listen_addr.clone(),
            upstream: self.upstream.clone(),
            exported_at_ms: Local::now().timestamp_millis(),
            event_count: self.events.len(),
            events: self.events.iter().rev().cloned().collect(),
        };

        self.flash_message = Some("🧬 Hippocampus syncing…".to_string());

        let result = tokio::task::block_in_place(|| {
            tokio::runtime::Handle::current().block_on(upload::upload_session(&payload))
        });

        match result {
            Ok(response) => {
                self.flash_message = Some(format!(
                    "✓ Hippocampus synced {} requests",
                    response.event_count,
                ));
            }
            Err(err) => {
                self.flash_message = Some(format!("upload failed: {err}"));
            }
        }
    }
}

fn handle_key_event(app: &mut CaptureApp, key: KeyCode) {
    match key {
        KeyCode::Char('q') => app.should_quit = true,
        KeyCode::Char('p') => app.toggle_pause(),
        KeyCode::Char('m') => app.cycle_method_filter(),
        KeyCode::Char('f') => app.cycle_path_filter(),
        KeyCode::Char('s') => app.toggle_summary_mode(),
        KeyCode::Char('c') => app.clear_session(),
        KeyCode::Char('e') => {
            if let Err(error) = app.export_session_json() {
                app.flash_message = Some(format!("capture export failed: {error}"));
            } else if app.auto_upload {
                app.upload_session_now();
            }
        }
        KeyCode::Char('u') => {
            app.upload_session_now();
        }
        KeyCode::Tab | KeyCode::Char('d') => app.toggle_detail_pane(),
        KeyCode::Char('?') => app.help_visible = !app.help_visible,
        KeyCode::Up => app.move_selection_up(),
        KeyCode::Down => app.move_selection_down(),
        _ => {}
    }
}

fn draw(frame: &mut ratatui::Frame<'_>, app: &CaptureApp) {
    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);
    }
}

fn draw_header(frame: &mut ratatui::Frame<'_>, area: Rect, app: &CaptureApp) {
    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 chunks = Layout::default()
        .direction(Direction::Horizontal)
        .constraints([Constraint::Min(20), Constraint::Length(30)])
        .split(inner);

    let left = Line::from(vec![
        Span::styled("lobe", dim_style()),
        Span::styled(" ▸ ", mute_style()),
        Span::styled("capture", cyan_bold_style()),
        Span::styled("  local ", mute_style()),
        Span::styled(format!("http://{}", app.listen_addr), base_style()),
        Span::styled(" → upstream ", mute_style()),
        Span::styled(app.upstream.as_str(), base_style()),
    ]);
    let 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 { "CAPTURING" },
            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(format!("{} reqs", app.events.len()), dim_style()),
    ]);

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

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

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

fn draw_left_column(frame: &mut ratatui::Frame<'_>, area: Rect, app: &CaptureApp) {
    let chunks = Layout::default()
        .direction(Direction::Vertical)
        .constraints([
            Constraint::Length(6),
            Constraint::Length(6),
            Constraint::Length(10),
            Constraint::Min(8),
        ])
        .split(area);

    draw_stream_panel(frame, chunks[0], app);
    draw_filters_panel(frame, chunks[1], app);
    draw_selected_request_panel(frame, chunks[2], app);
    draw_detail_panel(frame, chunks[3], app);
}

fn draw_right_column(frame: &mut ratatui::Frame<'_>, area: Rect, app: &CaptureApp) {
    if app.summary_mode {
        draw_capture_summary_view(frame, area, app);
        return;
    }

    let chunks = Layout::default()
        .direction(Direction::Vertical)
        .constraints([Constraint::Min(12), Constraint::Length(5)])
        .split(area);

    draw_capture_table(frame, chunks[0], app);
    draw_summary_panel(frame, chunks[1], app);
}

fn draw_capture_summary_view(frame: &mut ratatui::Frame<'_>, area: Rect, app: &CaptureApp) {
    let chunks = Layout::default()
        .direction(Direction::Vertical)
        .constraints([
            Constraint::Length(5),
            Constraint::Length(8),
            Constraint::Length(8),
            Constraint::Min(8),
        ])
        .split(area);

    draw_summary_panel(frame, chunks[0], app);
    draw_top_slowest_paths(frame, chunks[1], app);
    draw_top_failing_paths(frame, chunks[2], app);

    let bottom = Layout::default()
        .direction(Direction::Horizontal)
        .constraints([Constraint::Percentage(50), Constraint::Percentage(50)])
        .split(chunks[3]);
    draw_top_hosts(frame, bottom[0], app);
    draw_route_stats(frame, bottom[1], app);
}

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

    let session = format_elapsed(app.session_started_at.elapsed());
    let lines = vec![
        info_line("listen", format!("http://{}", app.listen_addr), cyan_style()),
        info_line("upstream", app.upstream.clone(), base_style()),
        info_line("session", session, mute_style()),
        info_line(
            "buffered",
            app.paused_buffer.len().to_string(),
            if app.paused_buffer.is_empty() { mute_style() } else { yellow_style() },
        ),
    ];

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

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

    let lines = vec![
        info_line("method", app.current_method_filter_label(), cyan_style()),
        info_line("path", app.current_path_filter_label(), cyan_style()),
        info_line(
            "detail",
            match app.detail_pane {
                DetailPane::Request => "request".to_string(),
                DetailPane::Tls => "tls".to_string(),
            },
            base_style(),
        ),
        info_line(
            "state",
            if app.paused { "paused" } else { "live" }.to_string(),
            if app.paused { yellow_style() } else { green_style() },
        ),
    ];

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

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

    let lines = if let Some(event) = app.selected_event() {
        vec![
            info_line("method", event.request_method.clone(), style_for_method(&event.request_method)),
            info_line("host", event.request_host.clone(), mute_style()),
            info_line("path", display_request_path(event), cyan_style()),
            info_line("status", display_status(event), style_for_status_code(event.response_status_code)),
            info_line("bytes", format_bytes(event.response_bytes), base_style()),
            info_line("ttfb", format!("{}ms", event.report.ttfb_ms), style_for_ms(event.report.ttfb_ms, 120)),
            info_line("download", format!("{}ms", event.report.download_ms), style_for_ms(event.report.download_ms, 120)),
            info_line("total", format!("{}ms", event.report.total_ms), white_style()),
            info_line("at", format_time(event.created_at_ms), mute_style()),
        ]
    } else {
        vec![Line::from(Span::styled("waiting for traffic", dim_style()))]
    };

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

fn draw_detail_panel(frame: &mut ratatui::Frame<'_>, area: Rect, app: &CaptureApp) {
    let title = match app.detail_pane {
        DetailPane::Request => "REQUEST DETAIL",
        DetailPane::Tls => "TLS DETAIL",
    };
    let block = panel_block(panel_title(title, None), None);
    let inner = block.inner(area);
    frame.render_widget(block, area);

    let lines = match app.selected_event() {
        Some(event) if matches!(app.detail_pane, DetailPane::Request) => {
            let mut lines = vec![
                info_line(
                    "protocol",
                    event.protocol_version.clone().unwrap_or_else(|| "n/a".to_string()),
                    base_style(),
                ),
                phase_bar_line(&event.report),
                info_line("dns", format!("{}ms", event.report.dns_ms), style_for_ms(event.report.dns_ms, 30)),
                info_line("tcp", format!("{}ms", event.report.tcp_ms), style_for_ms(event.report.tcp_ms, 30)),
                info_line("tls", format!("{}ms", event.report.tls_ms), style_for_ms(event.report.tls_ms, 60)),
                info_line("download", format!("{}ms", event.report.download_ms), style_for_ms(event.report.download_ms, 120)),
                info_line("target", event.target.clone(), mute_style()),
            ];
            if let Some(location) = &event.redirect_location {
                lines.push(info_line("redirect", location.clone(), cyan_style()));
            }
            let request_summary = summarize_headers(&event.request_headers, REQUEST_HEADER_KEYS);
            if !request_summary.is_empty() {
                lines.push(Line::from(" "));
                lines.push(Line::from(Span::styled("request headers", dim_style())));
                lines.extend(request_summary.into_iter().map(|(name, value)| info_line(&name, value, mute_style())));
            }
            let response_summary = summarize_headers(&event.response_headers, RESPONSE_HEADER_KEYS);
            if !response_summary.is_empty() {
                lines.push(Line::from(" "));
                lines.push(Line::from(Span::styled("response headers", dim_style())));
                lines.extend(response_summary.into_iter().map(|(name, value)| info_line(&name, value, mute_style())));
            }
            if let Some(error) = &event.error_message {
                lines.push(Line::from(" "));
                lines.push(Line::from(vec![
                    Span::styled("error       ", mute_style()),
                    Span::styled(error.clone(), red_style()),
                ]));
            }
            lines
        }
        Some(event) => {
            let tls = event.tls.as_ref();
            vec![
                info_line(
                    "handshake",
                    tls.map(|value| match value.status {
                        TlsStatus::NotUsed => "not used".to_string(),
                        TlsStatus::HandshakeSucceeded => "verified".to_string(),
                        TlsStatus::HandshakeFailed => "failed".to_string(),
                    })
                    .unwrap_or_else(|| "n/a".to_string()),
                    match tls.map(|value| value.status) {
                        Some(TlsStatus::HandshakeSucceeded) => green_style(),
                        Some(TlsStatus::HandshakeFailed) => red_style(),
                        _ => mute_style(),
                    },
                ),
                info_line(
                    "version",
                    tls.and_then(|value| value.version.clone()).unwrap_or_else(|| "n/a".to_string()),
                    base_style(),
                ),
                info_line(
                    "cipher",
                    tls.and_then(|value| value.cipher_suite.clone()).unwrap_or_else(|| "n/a".to_string()),
                    mute_style(),
                ),
                info_line(
                    "issuer",
                    tls.and_then(|value| value.certificate.as_ref().map(|cert| cert.issuer.clone()))
                        .unwrap_or_else(|| "n/a".to_string()),
                    mute_style(),
                ),
                info_line(
                    "expires",
                    tls.and_then(|value| value.certificate.as_ref().and_then(|cert| cert.not_after.clone()))
                        .unwrap_or_else(|| "n/a".to_string()),
                    yellow_style(),
                ),
            ]
        }
        None => vec![Line::from(Span::styled("waiting for traffic", dim_style()))],
    };

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

fn draw_capture_table(frame: &mut ratatui::Frame<'_>, area: Rect, app: &CaptureApp) {
    let filtered = app.filtered_events();
    let suffix = format!("{} shown · newest first", filtered.len());
    let block = panel_block(panel_title("CAPTURED REQUESTS", 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("method").style(dim_style()),
        Cell::from("host").style(dim_style()),
        Cell::from("path").style(dim_style()),
        Cell::from("status").style(dim_style()),
        Cell::from("ttfb").style(dim_style()),
        Cell::from("total").style(dim_style()),
        Cell::from("bytes").style(dim_style()),
    ]);

    let rows = filtered.iter().enumerate().map(|(index, event)| {
        let highlight = index == app.selected;
        let base = if highlight {
            Style::default().bg(Color::Rgb(13, 43, 22))
        } else {
            Style::default()
        };

        Row::new(vec![
            Cell::from((filtered.len().saturating_sub(index)).to_string()).style(base.patch(dim_style())),
            Cell::from(format_time(event.created_at_ms)).style(base.patch(mute_style())),
            Cell::from(event.request_method.clone()).style(base.patch(style_for_method(&event.request_method))),
            Cell::from(event.request_host.clone()).style(base.patch(mute_style())),
            Cell::from(display_request_path(event)).style(base.patch(cyan_style())),
            Cell::from(status_badge(event)).style(base.patch(style_for_status_code(event.response_status_code))),
            Cell::from(format!("{}ms", event.report.ttfb_ms)).style(base.patch(style_for_ms(event.report.ttfb_ms, 120))),
            Cell::from(format!("{}ms", event.report.total_ms)).style(base.patch(white_style())),
            Cell::from(format_bytes(event.response_bytes)).style(base.patch(base_style())),
        ])
    });

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

    frame.render_widget(table, inner);
}

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

    let filtered = app.filtered_events();
    let success_count = filtered
        .iter()
        .filter(|event| event.response_status_code.map(|code| code < 400).unwrap_or(false))
        .count();
    let error_count = filtered.len().saturating_sub(success_count);
    let line = Line::from(vec![
        Span::styled("shown ", mute_style()),
        Span::styled(filtered.len().to_string(), base_style()),
        Span::styled("  ok ", mute_style()),
        Span::styled(success_count.to_string(), green_style()),
        Span::styled("  err ", mute_style()),
        Span::styled(error_count.to_string(), red_style()),
    ]);

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

fn draw_top_slowest_paths(frame: &mut ratatui::Frame<'_>, area: Rect, app: &CaptureApp) {
    let block = panel_block(panel_title("TOP SLOWEST PATHS", None), None);
    let inner = block.inner(area);
    frame.render_widget(block, area);

    let mut summaries = route_summaries(&app.filtered_events());
    summaries.sort_by(|a, b| {
        b.p95_total_ms
            .cmp(&a.p95_total_ms)
            .then_with(|| b.average_total_ms.cmp(&a.average_total_ms))
            .then_with(|| b.request_count.cmp(&a.request_count))
            .then_with(|| a.path.cmp(&b.path))
    });

    let lines = if summaries.is_empty() {
        vec![Line::from(Span::styled("waiting for traffic", dim_style()))]
    } else {
        summaries
            .into_iter()
            .take(5)
            .map(|summary| {
                Line::from(vec![
                    Span::styled(truncate_text(&summary.path, 26), cyan_style()),
                    Span::raw(" "),
                    Span::styled(format!("p95 {}ms", summary.p95_total_ms), yellow_style()),
                    Span::raw(" "),
                    Span::styled(format!("avg {}ms", summary.average_total_ms), mute_style()),
                ])
            })
            .collect()
    };

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

fn draw_top_failing_paths(frame: &mut ratatui::Frame<'_>, area: Rect, app: &CaptureApp) {
    let block = panel_block(panel_title("TOP FAILING PATHS", None), None);
    let inner = block.inner(area);
    frame.render_widget(block, area);

    let mut summaries = route_summaries(&app.filtered_events());
    summaries.sort_by(|a, b| {
        b.failure_count
            .cmp(&a.failure_count)
            .then_with(|| b.request_count.cmp(&a.request_count))
            .then_with(|| a.path.cmp(&b.path))
    });

    let failing = summaries
        .into_iter()
        .filter(|summary| summary.failure_count > 0)
        .take(5)
        .collect::<Vec<_>>();

    let lines = if failing.is_empty() {
        vec![Line::from(Span::styled("no failing paths in current view", dim_style()))]
    } else {
        failing
            .into_iter()
            .map(|summary| {
                Line::from(vec![
                    Span::styled(truncate_text(&summary.path, 24), cyan_style()),
                    Span::raw(" "),
                    Span::styled(
                        format!("{}/{} fail", summary.failure_count, summary.request_count),
                        red_style(),
                    ),
                ])
            })
            .collect()
    };

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

fn draw_top_hosts(frame: &mut ratatui::Frame<'_>, area: Rect, app: &CaptureApp) {
    let block = panel_block(panel_title("TOP HOSTS", None), None);
    let inner = block.inner(area);
    frame.render_widget(block, area);

    let mut summaries = host_summaries(&app.filtered_events());
    summaries.sort_by(|a, b| {
        b.request_count
            .cmp(&a.request_count)
            .then_with(|| b.average_total_ms.cmp(&a.average_total_ms))
            .then_with(|| a.host.cmp(&b.host))
    });

    let lines = if summaries.is_empty() {
        vec![Line::from(Span::styled("waiting for traffic", dim_style()))]
    } else {
        summaries
            .into_iter()
            .take(5)
            .map(|summary| {
                Line::from(vec![
                    Span::styled(truncate_text(&summary.host, 24), cyan_style()),
                    Span::raw(" "),
                    Span::styled(format!("{} req", summary.request_count), base_style()),
                    Span::raw(" "),
                    Span::styled(format!("avg {}ms", summary.average_total_ms), mute_style()),
                    if summary.failure_count > 0 {
                        Span::styled(format!(" err {}", summary.failure_count), red_style())
                    } else {
                        Span::raw("")
                    },
                ])
            })
            .collect()
    };

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

fn draw_route_stats(frame: &mut ratatui::Frame<'_>, area: Rect, app: &CaptureApp) {
    let block = panel_block(panel_title("ROUTE STATS", None), None);
    let inner = block.inner(area);
    frame.render_widget(block, area);

    let mut summaries = route_summaries(&app.filtered_events());
    summaries.sort_by(|a, b| {
        b.request_count
            .cmp(&a.request_count)
            .then_with(|| {
                b.average_ttfb_ms
                    .cmp(&a.average_ttfb_ms)
                    .then_with(|| b.p95_ttfb_ms.cmp(&a.p95_ttfb_ms))
            })
            .then_with(|| a.path.cmp(&b.path))
    });

    let lines = if summaries.is_empty() {
        vec![Line::from(Span::styled("waiting for traffic", dim_style()))]
    } else {
        summaries
            .into_iter()
            .take(5)
            .map(|summary| {
                Line::from(vec![
                    Span::styled(truncate_text(&summary.path, 20), cyan_style()),
                    Span::raw(" "),
                    Span::styled(format!("{}x", summary.request_count), base_style()),
                    Span::raw(" "),
                    Span::styled(format!("avg {}ms", summary.average_ttfb_ms), mute_style()),
                    Span::raw(" "),
                    Span::styled(format!("p95 {}ms", summary.p95_ttfb_ms), yellow_style()),
                ])
            })
            .collect()
    };

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

fn draw_footer(frame: &mut ratatui::Frame<'_>, area: Rect, app: &CaptureApp) {
    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(44)])
        .split(inner);

    let left = Line::from(vec![
        footer_key("q", "quit"),
        Span::raw(" "),
        footer_key("p", "pause"),
        Span::raw(" "),
        footer_key("m", "method"),
        Span::raw(" "),
        footer_key("f", "path"),
        Span::raw(" "),
        footer_key("s", "summary"),
        Span::raw(" "),
        footer_key("tab", "detail"),
        Span::raw(" "),
        footer_key("e", "export"),
        Span::raw(" "),
        footer_key("u", "upload"),
        Span::raw(" "),
        footer_key("c", "clear"),
        Span::raw(" "),
        footer_key("?", "help"),
    ]);
    let right = Line::from(vec![
        Span::styled(
            app.flash_message
                .clone()
                .unwrap_or_else(|| "point your browser or app at the local proxy".to_string()),
            dim_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, 48, frame.area());
    frame.render_widget(Clear, area);
    let block = Block::default()
        .title(Span::styled(" Capture 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 capture mode")]),
        Line::from(vec![footer_key("p", "pause"), Span::raw(" pause or resume ingesting the live stream")]),
        Line::from(vec![footer_key("m", "method"), Span::raw(" cycle method filter")]),
        Line::from(vec![footer_key("f", "path"), Span::raw(" cycle path filter")]),
        Line::from(vec![footer_key("s", "summary"), Span::raw(" toggle summary view")]),
        Line::from(vec![footer_key("tab", "detail"), Span::raw(" switch request/tls detail pane")]),
        Line::from(vec![footer_key("e", "export"), Span::raw(" export the current capture session to JSON")]),
        Line::from(vec![footer_key("u", "upload"), Span::raw(" sync the session to your Lobe cloud (requires `lobe login`)")]),
        Line::from(vec![footer_key("↑↓", "select"), Span::raw(" move request selection")]),
        Line::from(vec![footer_key("c", "clear"), Span::raw(" clear the current capture session")]),
    ];

    frame.render_widget(Paragraph::new(lines).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, 30), 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 info_line(label: &str, value: String, style: Style) -> Line<'static> {
    Line::from(vec![
        Span::styled(format!("{label:<10}"), mute_style()),
        Span::styled(value, style),
    ])
}

const PHASE_BAR_WIDTH: usize = 40;

fn phase_bar_line(report: &lobe_core::models::TimingReport) -> Line<'static> {
    let phases: [(u64, Color, &str); 5] = [
        (report.dns_ms, Color::Rgb(142, 161, 198), "DNS"),
        (report.tcp_ms, Color::Rgb(111, 166, 255), "TCP"),
        (report.tls_ms, Color::Rgb(126, 164, 255), "TLS"),
        (report.ttfb_ms, Color::Rgb(232, 184, 76), "TTFB"),
        (report.download_ms, Color::Rgb(81, 200, 120), "Download"),
    ];

    let phase_sum: u64 = phases.iter().map(|(v, _, _)| *v).sum();
    let other_ms = report.total_ms.saturating_sub(phase_sum);
    let total_ms = phase_sum + other_ms;

    let mut spans = vec![Span::styled(format!("{:<10}", "phases"), mute_style())];

    if total_ms == 0 {
        spans.push(Span::styled("─".repeat(PHASE_BAR_WIDTH), dim_style()));
        return Line::from(spans);
    }

    let mut cells_used: usize = 0;
    for (i, (value_ms, color, _label)) in phases.iter().enumerate() {
        if *value_ms == 0 {
            continue;
        }
        let is_last = i == phases.len() - 1 && other_ms == 0;
        let mut cells = ((*value_ms as usize * PHASE_BAR_WIDTH) / total_ms as usize).max(1);
        if is_last {
            cells = PHASE_BAR_WIDTH.saturating_sub(cells_used);
        }
        if cells_used + cells > PHASE_BAR_WIDTH {
            cells = PHASE_BAR_WIDTH.saturating_sub(cells_used);
        }
        if cells == 0 {
            continue;
        }
        spans.push(Span::styled("█".repeat(cells), Style::default().fg(*color)));
        cells_used += cells;
    }

    if other_ms > 0 && cells_used < PHASE_BAR_WIDTH {
        let remaining = PHASE_BAR_WIDTH - cells_used;
        spans.push(Span::styled(
            "▒".repeat(remaining),
            Style::default().fg(Color::Rgb(90, 100, 120)),
        ));
    }

    Line::from(spans)
}

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 display_status(event: &CapturedExchange) -> String {
    match (event.response_status_code, event.response_status_text.as_deref()) {
        (Some(code), Some(text)) => format!("{code} {text}"),
        (Some(code), None) => code.to_string(),
        (None, _) => "ERR".to_string(),
    }
}

const REQUEST_HEADER_KEYS: &[&str] = &[
    "accept",
    "accept-language",
    "content-type",
    "origin",
    "referer",
    "user-agent",
];

const RESPONSE_HEADER_KEYS: &[&str] = &[
    "cache-control",
    "content-encoding",
    "content-type",
    "location",
    "server",
    "set-cookie",
    "via",
];

fn display_request_path(event: &CapturedExchange) -> String {
    match &event.redirect_location {
        Some(location) => format!("{} -> {}", event.request_path, shorten_redirect(location)),
        None => event.request_path.clone(),
    }
}

fn shorten_redirect(location: &str) -> String {
    if let Ok(url) = url::Url::parse(location) {
        let host = url.host_str().unwrap_or_default();
        let mut path = url.path().to_string();
        if path.is_empty() {
            path.push('/');
        }
        if host.is_empty() {
            path
        } else {
            format!("{host}{path}")
        }
    } else {
        location.to_string()
    }
}

fn summarize_headers(headers: &[(String, String)], interesting_keys: &[&str]) -> Vec<(String, String)> {
    let mut summary = Vec::new();

    for key in interesting_keys {
        if let Some((_, value)) = headers
            .iter()
            .find(|(name, _)| name.eq_ignore_ascii_case(key))
        {
            summary.push(((*key).to_string(), value.clone()));
        }
    }

    summary
}

fn route_summaries(events: &[&CapturedExchange]) -> Vec<RouteSummary> {
    let mut by_path: HashMap<&str, Vec<&CapturedExchange>> = HashMap::new();
    for event in events {
        by_path.entry(event.request_path.as_str()).or_default().push(*event);
    }

    by_path
        .into_iter()
        .map(|(path, events)| {
            let request_count = events.len();
            let failure_count = events
                .iter()
                .filter(|event| event.response_status_code.map(|code| code >= 400).unwrap_or(true))
                .count();
            let total_values = events.iter().map(|event| event.report.total_ms).collect::<Vec<_>>();
            let ttfb_values = events.iter().map(|event| event.report.ttfb_ms).collect::<Vec<_>>();

            RouteSummary {
                path: path.to_string(),
                request_count,
                failure_count,
                average_total_ms: average_u64(&total_values),
                average_ttfb_ms: average_u64(&ttfb_values),
                p95_total_ms: percentile_u64(&total_values, 0.95),
                p95_ttfb_ms: percentile_u64(&ttfb_values, 0.95),
            }
        })
        .collect()
}

fn host_summaries(events: &[&CapturedExchange]) -> Vec<HostSummary> {
    let mut by_host: HashMap<&str, Vec<&CapturedExchange>> = HashMap::new();
    for event in events {
        by_host.entry(event.request_host.as_str()).or_default().push(*event);
    }

    by_host
        .into_iter()
        .map(|(host, events)| {
            let request_count = events.len();
            let failure_count = events
                .iter()
                .filter(|event| event.response_status_code.map(|code| code >= 400).unwrap_or(true))
                .count();
            let total_values = events.iter().map(|event| event.report.total_ms).collect::<Vec<_>>();

            HostSummary {
                host: host.to_string(),
                request_count,
                failure_count,
                average_total_ms: average_u64(&total_values),
            }
        })
        .collect()
}

fn average_u64(values: &[u64]) -> u64 {
    if values.is_empty() {
        return 0;
    }

    values.iter().sum::<u64>() / values.len() as u64
}

fn percentile_u64(values: &[u64], percentile: f64) -> u64 {
    if values.is_empty() {
        return 0;
    }

    let mut sorted = values.to_vec();
    sorted.sort_unstable();
    let index = ((sorted.len() - 1) as f64 * percentile).round() as usize;
    sorted[index]
}

fn truncate_text(value: &str, max_chars: usize) -> String {
    let mut chars = value.chars();
    let truncated = chars.by_ref().take(max_chars).collect::<String>();
    if chars.next().is_some() {
        format!("{truncated}…")
    } else {
        truncated
    }
}

fn status_badge(event: &CapturedExchange) -> String {
    event.response_status_code
        .map(|code| code.to_string())
        .unwrap_or_else(|| "ERR".to_string())
}

fn style_for_status_code(status_code: Option<u16>) -> Style {
    match 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 => red_style(),
    }
}

fn style_for_method(method: &str) -> Style {
    match method {
        "GET" => green_style(),
        "POST" => cyan_style(),
        "PUT" | "PATCH" => yellow_style(),
        "DELETE" => red_style(),
        _ => base_style(),
    }
}

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(139, 148, 158))
}
