use crate::usage_stats::aggregate::{
    build_session_detail_view, build_sessions_view, build_summary_view, build_trend_view,
};
use crate::usage_stats::model::{SessionId, SessionRecord, ToolKind};
use crate::usage_stats::path_display::display_path;
use crate::usage_stats::query::{GroupBy, TimeRangeArgs, parse_time_range};
use anyhow::Result;
use crossterm::event::{self, Event, KeyCode, KeyEventKind};
use ratatui::layout::{Constraint, Direction, Layout, Rect};
use ratatui::style::{Color, Modifier, Style};
use ratatui::text::{Line, Span};
use ratatui::widgets::{
    Block, Borders, Cell, Clear, Gauge, Paragraph, Row, Sparkline, Table, TableState, Tabs, Wrap,
};
use std::sync::Arc;
use std::sync::atomic::{AtomicBool, Ordering};
use std::sync::mpsc::{Receiver, Sender, channel};
use std::thread::JoinHandle;
use std::time::Duration;

#[derive(Debug, Clone)]
pub struct StatsTuiScanRequest {
    pub cwd: std::path::PathBuf,
    pub group_by: GroupBy,
    pub range: TimeRangeArgs,
    pub limit: usize,
    pub verbose_paths: bool,
    pub with_breakdown: bool,
    pub include_sidechain: bool,
}

#[derive(Debug, Clone)]
pub struct ScanProgress {
    pub label: &'static str,
    pub done: usize,
    pub total: usize,
}

#[derive(Debug, Clone)]
pub enum ScanMessage {
    Progress(ScanProgress),
    Done(Vec<SessionRecord>),
    Error(String),
}

pub type ScanFn = Arc<
    dyn Fn(StatsTuiScanRequest, Sender<ScanMessage>, Arc<AtomicBool>) -> Result<Vec<SessionRecord>>
        + Send
        + Sync,
>;

#[derive(Clone, Copy, Debug, PartialEq, Eq)]
enum StatsTab {
    Overview,
    Trend,
    Sessions,
    SessionDetail,
}

impl StatsTab {
    fn title(self) -> &'static str {
        match self {
            Self::Overview => "Overview",
            Self::Trend => "Trend",
            Self::Sessions => "Sessions",
            Self::SessionDetail => "Session",
        }
    }

    fn next(self) -> Self {
        match self {
            Self::Overview => Self::Trend,
            Self::Trend => Self::Sessions,
            Self::Sessions => Self::SessionDetail,
            Self::SessionDetail => Self::Overview,
        }
    }

    fn prev(self) -> Self {
        match self {
            Self::Overview => Self::SessionDetail,
            Self::Trend => Self::Overview,
            Self::Sessions => Self::Trend,
            Self::SessionDetail => Self::Sessions,
        }
    }
}

#[derive(Clone, Copy, Debug, PartialEq, Eq)]
enum TimePreset {
    All,
    Last7d,
    Last30d,
    Last90d,
    Custom,
}

impl TimePreset {
    fn label(self) -> &'static str {
        match self {
            Self::All => "All",
            Self::Last7d => "7d",
            Self::Last30d => "30d",
            Self::Last90d => "90d",
            Self::Custom => "Custom",
        }
    }
}

#[derive(Clone, Copy, Debug, PartialEq, Eq)]
enum FilterField {
    Preset,
    Since,
    Until,
    GroupBy,
    Breakdown,
    Sidechain,
    Apply,
    Cancel,
}

impl FilterField {
    fn all_for(tool: ToolKind) -> Vec<FilterField> {
        let mut fields = vec![
            FilterField::Preset,
            FilterField::Since,
            FilterField::Until,
            FilterField::GroupBy,
        ];

        match tool {
            ToolKind::Codex => fields.push(FilterField::Breakdown),
            ToolKind::ClaudeCode => fields.push(FilterField::Sidechain),
            ToolKind::Cursor => {}
        }

        fields.push(FilterField::Apply);
        fields.push(FilterField::Cancel);
        fields
    }
}

#[derive(Clone, Debug)]
struct FilterFormState {
    active_field_idx: usize,
    fields: Vec<FilterField>,
    preset: TimePreset,
    since: String,
    until: String,
    group_by: GroupBy,
    with_breakdown: bool,
    include_sidechain: bool,
    error: Option<String>,
}

impl FilterFormState {
    fn new(tool: ToolKind, request: &StatsTuiScanRequest) -> Self {
        let preset = match request.range.last.as_deref() {
            Some("7d") => TimePreset::Last7d,
            Some("30d") => TimePreset::Last30d,
            Some("90d") => TimePreset::Last90d,
            Some(_) => TimePreset::Custom,
            None => {
                if request.range.since.is_some() || request.range.until.is_some() {
                    TimePreset::Custom
                } else {
                    TimePreset::All
                }
            }
        };

        Self {
            active_field_idx: 0,
            fields: FilterField::all_for(tool),
            preset,
            since: request.range.since.clone().unwrap_or_default(),
            until: request.range.until.clone().unwrap_or_default(),
            group_by: request.group_by,
            with_breakdown: request.with_breakdown,
            include_sidechain: request.include_sidechain,
            error: None,
        }
    }

    fn active_field(&self) -> FilterField {
        self.fields[self.active_field_idx]
    }

    fn move_next(&mut self) {
        self.active_field_idx = (self.active_field_idx + 1).min(self.fields.len() - 1);
    }

    fn move_prev(&mut self) {
        self.active_field_idx = self.active_field_idx.saturating_sub(1);
    }

    fn cycle_preset_left(&mut self) {
        self.preset = match self.preset {
            TimePreset::All => TimePreset::Custom,
            TimePreset::Last7d => TimePreset::All,
            TimePreset::Last30d => TimePreset::Last7d,
            TimePreset::Last90d => TimePreset::Last30d,
            TimePreset::Custom => TimePreset::Last90d,
        };
    }

    fn cycle_preset_right(&mut self) {
        self.preset = match self.preset {
            TimePreset::All => TimePreset::Last7d,
            TimePreset::Last7d => TimePreset::Last30d,
            TimePreset::Last30d => TimePreset::Last90d,
            TimePreset::Last90d => TimePreset::Custom,
            TimePreset::Custom => TimePreset::All,
        };
    }

    fn cycle_group_by_left(&mut self) {
        self.group_by = match self.group_by {
            GroupBy::Day => GroupBy::Month,
            GroupBy::Week => GroupBy::Day,
            GroupBy::Month => GroupBy::Week,
        };
    }

    fn cycle_group_by_right(&mut self) {
        self.group_by = match self.group_by {
            GroupBy::Day => GroupBy::Week,
            GroupBy::Week => GroupBy::Month,
            GroupBy::Month => GroupBy::Day,
        };
    }

    fn append_char(&mut self, c: char) {
        if self.preset != TimePreset::Custom {
            return;
        }
        match self.active_field() {
            FilterField::Since => self.since.push(c),
            FilterField::Until => self.until.push(c),
            _ => {}
        }
    }

    fn pop_char(&mut self) {
        if self.preset != TimePreset::Custom {
            return;
        }
        match self.active_field() {
            FilterField::Since => {
                self.since.pop();
            }
            FilterField::Until => {
                self.until.pop();
            }
            _ => {}
        }
    }

    fn build_range_args(&self) -> TimeRangeArgs {
        match self.preset {
            TimePreset::All => TimeRangeArgs::default(),
            TimePreset::Last7d => TimeRangeArgs {
                last: Some("7d".to_string()),
                ..TimeRangeArgs::default()
            },
            TimePreset::Last30d => TimeRangeArgs {
                last: Some("30d".to_string()),
                ..TimeRangeArgs::default()
            },
            TimePreset::Last90d => TimeRangeArgs {
                last: Some("90d".to_string()),
                ..TimeRangeArgs::default()
            },
            TimePreset::Custom => TimeRangeArgs {
                since: (!self.since.trim().is_empty()).then(|| self.since.trim().to_string()),
                until: (!self.until.trim().is_empty()).then(|| self.until.trim().to_string()),
                last: None,
            },
        }
    }
}

#[derive(Clone, Debug)]
struct ScanResultCache {
    sessions_all: Vec<SessionRecord>,
    sessions_list: Vec<SessionRecord>,
    summary: crate::usage_stats::SummaryView,
    trend: crate::usage_stats::TrendView,
}

#[derive(Clone, Debug)]
enum ScanStatus {
    Idle,
    Scanning(Option<ScanProgress>),
    Ready(Box<ScanResultCache>),
    Error(String),
}

#[derive(Clone, Copy, Debug, PartialEq, Eq)]
enum TrendMetric {
    Overall,
    Primary,
    Sidechain,
}

impl TrendMetric {
    fn label(self) -> &'static str {
        match self {
            Self::Overall => "overall",
            Self::Primary => "primary",
            Self::Sidechain => "sidechain",
        }
    }

    fn next(self) -> Self {
        match self {
            Self::Overall => Self::Primary,
            Self::Primary => Self::Sidechain,
            Self::Sidechain => Self::Overall,
        }
    }
}

struct TerminalRestoreGuard;

impl Drop for TerminalRestoreGuard {
    fn drop(&mut self) {
        ratatui::restore();
    }
}

pub fn run_stats_tui(tool: ToolKind, initial: StatsTuiScanRequest, scan_fn: ScanFn) -> Result<()> {
    let mut terminal = ratatui::try_init()?;
    let _restore = TerminalRestoreGuard;

    let mut app = StatsTuiApp::new(tool, initial, scan_fn);
    app.start_scan();

    loop {
        terminal.draw(|frame| app.draw(frame))?;

        app.drain_scan_messages();

        if !event::poll(Duration::from_millis(200))? {
            continue;
        }

        let Event::Key(key) = event::read()? else {
            continue;
        };
        if key.kind != KeyEventKind::Press {
            continue;
        }

        if app.handle_key(key.code)? {
            break;
        }
    }

    Ok(())
}

struct StatsTuiApp {
    tool: ToolKind,
    tab: StatsTab,
    request: StatsTuiScanRequest,
    scan_fn: ScanFn,
    scan_rx: Option<Receiver<ScanMessage>>,
    scan_cancel: Option<Arc<AtomicBool>>,
    scan_handle: Option<JoinHandle<()>>,
    status: ScanStatus,
    filter_open: bool,
    filter: FilterFormState,
    sessions_table_state: TableState,
    trend_metric: TrendMetric,
    trend_table_state: TableState,
    session_detail_id: Option<SessionId>,
}

impl StatsTuiApp {
    fn new(tool: ToolKind, request: StatsTuiScanRequest, scan_fn: ScanFn) -> Self {
        let filter = FilterFormState::new(tool, &request);
        Self {
            tool,
            tab: StatsTab::Overview,
            request,
            scan_fn,
            scan_rx: None,
            scan_cancel: None,
            scan_handle: None,
            status: ScanStatus::Idle,
            filter_open: false,
            filter,
            sessions_table_state: TableState::default(),
            trend_metric: TrendMetric::Overall,
            trend_table_state: TableState::default(),
            session_detail_id: None,
        }
    }

    fn start_scan(&mut self) {
        self.stop_scan();

        let (tx, rx) = channel::<ScanMessage>();
        self.scan_rx = Some(rx);
        self.status = ScanStatus::Scanning(None);

        let scan_fn = Arc::clone(&self.scan_fn);
        let request = self.request.clone();
        let cancel = Arc::new(AtomicBool::new(false));
        self.scan_cancel = Some(Arc::clone(&cancel));
        self.scan_handle = Some(std::thread::spawn(move || {
            let result = (scan_fn)(request, tx.clone(), Arc::clone(&cancel));
            if cancel.load(Ordering::Relaxed) {
                return;
            }
            match result {
                Ok(sessions) => {
                    let _ = tx.send(ScanMessage::Done(sessions));
                }
                Err(err) => {
                    let _ = tx.send(ScanMessage::Error(err.to_string()));
                }
            }
        }));
    }

    fn stop_scan(&mut self) {
        if let Some(cancel) = self.scan_cancel.take() {
            cancel.store(true, Ordering::Relaxed);
        }
        self.scan_rx = None;
        if let Some(handle) = self.scan_handle.take() {
            let _ = handle.join();
        }
    }

    fn drain_scan_messages(&mut self) {
        let Some(rx) = &self.scan_rx else {
            return;
        };
        while let Ok(msg) = rx.try_recv() {
            match msg {
                ScanMessage::Progress(progress) => {
                    self.status = ScanStatus::Scanning(Some(progress));
                }
                ScanMessage::Done(sessions) => {
                    let cache = self.build_cache(&sessions);
                    self.sessions_table_state
                        .select((!cache.sessions_list.is_empty()).then_some(0));
                    self.trend_table_state.select(
                        (!cache.trend.buckets.is_empty())
                            .then_some(cache.trend.buckets.len().saturating_sub(1)),
                    );
                    self.status = ScanStatus::Ready(Box::new(cache));
                    self.session_detail_id = None;
                }
                ScanMessage::Error(message) => {
                    self.status = ScanStatus::Error(message);
                }
            }
        }
    }

    fn build_cache(&self, sessions: &[SessionRecord]) -> ScanResultCache {
        let summary = build_summary_view(sessions);
        let trend = build_trend_view(sessions, self.request.group_by);
        let sessions_view = build_sessions_view(sessions, self.request.limit);

        ScanResultCache {
            sessions_all: sessions.to_vec(),
            sessions_list: sessions_view.sessions,
            summary,
            trend,
        }
    }

    fn handle_key(&mut self, key: KeyCode) -> Result<bool> {
        if self.filter_open {
            return Ok(self.handle_filter_key(key));
        }

        match key {
            KeyCode::Char('q') | KeyCode::Esc => return Ok(true),
            KeyCode::Tab => self.tab = self.tab.next(),
            KeyCode::BackTab => self.tab = self.tab.prev(),
            KeyCode::Char('f') => self.open_filter(),
            KeyCode::Up | KeyCode::Char('k') => match self.tab {
                StatsTab::Sessions => self.sessions_move(-1),
                StatsTab::Trend => self.trend_move(-1),
                _ => {}
            },
            KeyCode::Down | KeyCode::Char('j') => match self.tab {
                StatsTab::Sessions => self.sessions_move(1),
                StatsTab::Trend => self.trend_move(1),
                _ => {}
            },
            KeyCode::Enter => self.sessions_enter(),
            KeyCode::Char('c') => self.trend_cycle_metric(),
            _ => {}
        }
        Ok(false)
    }

    fn open_filter(&mut self) {
        self.filter = FilterFormState::new(self.tool, &self.request);
        self.filter_open = true;
    }

    fn handle_filter_key(&mut self, key: KeyCode) -> bool {
        match key {
            KeyCode::Esc => {
                self.filter_open = false;
            }
            KeyCode::Up => self.filter.move_prev(),
            KeyCode::Down => self.filter.move_next(),
            KeyCode::Left => match self.filter.active_field() {
                FilterField::Preset => self.filter.cycle_preset_left(),
                FilterField::GroupBy => self.filter.cycle_group_by_left(),
                FilterField::Breakdown => self.filter.with_breakdown = !self.filter.with_breakdown,
                FilterField::Sidechain => {
                    self.filter.include_sidechain = !self.filter.include_sidechain;
                }
                _ => {}
            },
            KeyCode::Right => match self.filter.active_field() {
                FilterField::Preset => self.filter.cycle_preset_right(),
                FilterField::GroupBy => self.filter.cycle_group_by_right(),
                FilterField::Breakdown => self.filter.with_breakdown = !self.filter.with_breakdown,
                FilterField::Sidechain => {
                    self.filter.include_sidechain = !self.filter.include_sidechain;
                }
                _ => {}
            },
            KeyCode::Backspace => self.filter.pop_char(),
            KeyCode::Char(c) => self.filter.append_char(c),
            KeyCode::Enter => match self.filter.active_field() {
                FilterField::Apply if self.apply_filter_form() => {
                    self.filter_open = false;
                }
                FilterField::Cancel => self.filter_open = false,
                _ => {}
            },
            _ => {}
        }
        false
    }

    fn apply_filter_form(&mut self) -> bool {
        let range = self.filter.build_range_args();
        if let Err(err) = parse_time_range(&range, chrono::Utc::now()) {
            self.filter.error = Some(err.to_string());
            return false;
        }

        self.request.group_by = self.filter.group_by;
        self.request.range = range;
        self.request.with_breakdown = self.filter.with_breakdown;
        self.request.include_sidechain = self.filter.include_sidechain;

        self.start_scan();
        true
    }

    fn sessions_move(&mut self, delta: isize) {
        if self.tab != StatsTab::Sessions {
            return;
        }
        let ScanStatus::Ready(cache) = &self.status else {
            return;
        };
        if cache.sessions_list.is_empty() {
            self.sessions_table_state.select(None);
            return;
        }
        let current = self.sessions_table_state.selected().unwrap_or(0) as isize;
        let next = (current + delta).clamp(0, cache.sessions_list.len().saturating_sub(1) as isize);
        self.sessions_table_state.select(Some(next as usize));
    }

    fn trend_move(&mut self, delta: isize) {
        if self.tab != StatsTab::Trend {
            return;
        }
        let ScanStatus::Ready(cache) = &self.status else {
            return;
        };
        if cache.trend.buckets.is_empty() {
            self.trend_table_state.select(None);
            return;
        }
        let current = self.trend_table_state.selected().unwrap_or(0) as isize;
        let next = (current + delta).clamp(0, cache.trend.buckets.len().saturating_sub(1) as isize);
        self.trend_table_state.select(Some(next as usize));
    }

    fn trend_cycle_metric(&mut self) {
        if self.tab != StatsTab::Trend {
            return;
        }
        if self.tool != ToolKind::ClaudeCode {
            return;
        }
        self.trend_metric = self.trend_metric.next();
    }

    fn sessions_enter(&mut self) {
        if self.tab != StatsTab::Sessions {
            return;
        }
        let ScanStatus::Ready(cache) = &self.status else {
            return;
        };
        let Some(idx) = self.sessions_table_state.selected() else {
            return;
        };
        let Some(session) = cache.sessions_list.get(idx) else {
            return;
        };
        self.session_detail_id = Some(session.id.clone());
        self.tab = StatsTab::SessionDetail;
    }

    fn draw(&mut self, frame: &mut ratatui::Frame) {
        let area = frame.area();
        let chunks = Layout::default()
            .direction(Direction::Vertical)
            .constraints(
                [
                    Constraint::Length(3),
                    Constraint::Min(1),
                    Constraint::Length(1),
                ]
                .as_ref(),
            )
            .split(area);

        self.draw_tabs(frame, chunks[0]);
        self.draw_body(frame, chunks[1]);
        self.draw_help(frame, chunks[2]);

        if self.filter_open {
            self.draw_filter_modal(frame, area);
        }
    }

    fn draw_tabs(&self, frame: &mut ratatui::Frame, area: Rect) {
        let titles = [
            StatsTab::Overview.title(),
            StatsTab::Trend.title(),
            StatsTab::Sessions.title(),
            StatsTab::SessionDetail.title(),
        ]
        .iter()
        .map(|t| Line::from(Span::styled(*t, Style::default().fg(Color::White))))
        .collect::<Vec<_>>();

        let selected = match self.tab {
            StatsTab::Overview => 0,
            StatsTab::Trend => 1,
            StatsTab::Sessions => 2,
            StatsTab::SessionDetail => 3,
        };

        let tabs = Tabs::new(titles)
            .block(
                Block::default()
                    .borders(Borders::ALL)
                    .title(tool_title(self.tool)),
            )
            .highlight_style(Style::default().add_modifier(Modifier::REVERSED))
            .select(selected);
        frame.render_widget(tabs, area);
    }

    fn draw_help(&self, frame: &mut ratatui::Frame, area: Rect) {
        let help = "Tab/Shift+Tab switch · ↑↓/j/k move · Enter drilldown · f filter · c cycle(trend/Claude) · q quit";
        frame.render_widget(
            Paragraph::new(Line::from(help)).style(Style::default().fg(Color::DarkGray)),
            area,
        );
    }

    fn draw_overview(
        frame: &mut ratatui::Frame,
        area: Rect,
        summary: &crate::usage_stats::SummaryView,
    ) {
        let chunks = Layout::default()
            .direction(Direction::Vertical)
            .constraints([Constraint::Percentage(50), Constraint::Percentage(50)].as_ref())
            .split(area);

        let top = Layout::default()
            .direction(Direction::Horizontal)
            .constraints([Constraint::Percentage(50), Constraint::Percentage(50)].as_ref())
            .split(chunks[0]);
        let bottom = Layout::default()
            .direction(Direction::Horizontal)
            .constraints([Constraint::Percentage(50), Constraint::Percentage(50)].as_ref())
            .split(chunks[1]);

        let mut token_lines = Vec::new();
        token_lines.push(Line::from(Span::styled(
            format_u64_count(summary.totals.tokens_total_known),
            Style::default().add_modifier(Modifier::BOLD),
        )));
        if let Some(sidechain) = &summary.sidechain_totals {
            token_lines.push(Line::from(format!(
                "primary {} · side {}",
                format_u64_count(sidechain.primary.tokens_total_known),
                format_u64_count(sidechain.sidechain.tokens_total_known)
            )));
        }
        frame.render_widget(
            Paragraph::new(token_lines)
                .block(
                    Block::default()
                        .borders(Borders::ALL)
                        .title("Tokens (known-only)"),
                )
                .wrap(Wrap { trim: true }),
            top[0],
        );

        let sessions_total = summary.coverage.total_sessions;
        let sessions_known = summary.coverage.known_token_sessions;
        let sessions_lines = vec![
            Line::from(Span::styled(
                format_usize_count(sessions_total),
                Style::default().add_modifier(Modifier::BOLD),
            )),
            Line::from(format!(
                "known tokens: {}",
                format_usize_count(sessions_known)
            )),
        ];
        frame.render_widget(
            Paragraph::new(sessions_lines)
                .block(Block::default().borders(Borders::ALL).title("Sessions"))
                .wrap(Wrap { trim: true }),
            top[1],
        );

        let coverage_percent = (sessions_known.saturating_mul(100))
            .checked_div(sessions_total)
            .unwrap_or(0)
            .min(100) as u16;
        let coverage_label = format!(
            "{}/{} ({}%)",
            format_usize_count(sessions_known),
            format_usize_count(sessions_total),
            coverage_percent
        );
        frame.render_widget(
            Gauge::default()
                .block(Block::default().borders(Borders::ALL).title("Coverage"))
                .gauge_style(Style::default().fg(Color::Green))
                .label(coverage_label)
                .percent(coverage_percent),
            bottom[0],
        );

        let latest = summary
            .latest_end_ts
            .map(|ts| {
                ts.with_timezone(&chrono::Local)
                    .format("%Y-%m-%d %H:%M:%S")
                    .to_string()
            })
            .unwrap_or_else(|| "-".to_string());
        frame.render_widget(
            Paragraph::new(Line::from(Span::styled(
                latest,
                Style::default().add_modifier(Modifier::BOLD),
            )))
            .block(Block::default().borders(Borders::ALL).title("Latest"))
            .wrap(Wrap { trim: true }),
            bottom[1],
        );
    }

    fn draw_trend(
        frame: &mut ratatui::Frame,
        area: Rect,
        trend: &crate::usage_stats::TrendView,
        tool: ToolKind,
        metric: TrendMetric,
        table_state: &mut TableState,
    ) {
        let values = trend
            .buckets
            .iter()
            .map(|bucket| bucket_tokens(bucket, metric).unwrap_or(0))
            .collect::<Vec<_>>();

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

        let title = if tool == ToolKind::ClaudeCode {
            format!("Tokens ({})", metric.label())
        } else {
            "Tokens".to_string()
        };
        frame.render_widget(
            Sparkline::default()
                .block(Block::default().borders(Borders::ALL).title(title))
                .data(values)
                .style(Style::default().fg(Color::Cyan)),
            chunks[0],
        );

        let header = Row::new(["Bucket", "Tokens", "Sessions (known/total)"]).style(
            Style::default()
                .fg(Color::White)
                .add_modifier(Modifier::BOLD),
        );
        let rows = trend
            .buckets
            .iter()
            .map(|bucket| {
                let tokens = bucket_tokens(bucket, metric)
                    .map(format_u64_count)
                    .unwrap_or_else(|| "-".to_string());
                Row::new(vec![
                    Cell::new(bucket.label.clone()),
                    Cell::new(tokens),
                    Cell::new(format!(
                        "{}/{}",
                        format_usize_count(bucket.coverage.known_token_sessions),
                        format_usize_count(bucket.coverage.total_sessions)
                    )),
                ])
            })
            .collect::<Vec<_>>();

        let table = Table::new(
            rows,
            [
                Constraint::Length(12),
                Constraint::Length(12),
                Constraint::Min(20),
            ],
        )
        .header(header)
        .block(Block::default().borders(Borders::ALL).title("Buckets"))
        .row_highlight_style(Style::default().add_modifier(Modifier::REVERSED))
        .highlight_symbol("> ");

        frame.render_stateful_widget(table, chunks[1], table_state);
    }

    fn draw_body(&mut self, frame: &mut ratatui::Frame, area: Rect) {
        match &self.status {
            ScanStatus::Idle => {
                frame.render_widget(
                    Paragraph::new("Idle").block(Block::default().borders(Borders::ALL)),
                    area,
                );
            }
            ScanStatus::Scanning(progress) => {
                let mut lines = vec!["Scanning…".to_string()];
                if let Some(progress) = progress {
                    lines.push(format!(
                        "{}: {}/{}",
                        progress.label, progress.done, progress.total
                    ));
                }
                frame.render_widget(
                    Paragraph::new(lines.join("\n")).block(Block::default().borders(Borders::ALL)),
                    area,
                );
            }
            ScanStatus::Error(message) => {
                frame.render_widget(
                    Paragraph::new(message.as_str())
                        .block(Block::default().borders(Borders::ALL).title("Error"))
                        .wrap(Wrap { trim: true }),
                    area,
                );
            }
            ScanStatus::Ready(cache) => match self.tab {
                StatsTab::Overview => {
                    Self::draw_overview(frame, area, &cache.summary);
                }
                StatsTab::Trend => {
                    let metric = if self.tool == ToolKind::ClaudeCode {
                        self.trend_metric
                    } else {
                        TrendMetric::Overall
                    };
                    Self::draw_trend(
                        frame,
                        area,
                        &cache.trend,
                        self.tool,
                        metric,
                        &mut self.trend_table_state,
                    );
                }
                StatsTab::Sessions => {
                    let is_claude = self.tool == ToolKind::ClaudeCode;
                    let header = if is_claude {
                        Row::new(["End", "Known", "Id", "SC", "Cwd", "Title"])
                    } else {
                        Row::new(["End", "Known", "Id", "Cwd", "Title"])
                    }
                    .style(
                        Style::default()
                            .fg(Color::White)
                            .add_modifier(Modifier::BOLD),
                    );

                    let rows = cache
                        .sessions_list
                        .iter()
                        .map(|session| {
                            let end = session
                                .end_ts
                                .with_timezone(&chrono::Local)
                                .format("%Y-%m-%d %H:%M")
                                .to_string();
                            let tokens = session
                                .token_usage
                                .total
                                .map(|v| v.to_string())
                                .unwrap_or_else(|| "-".to_string());
                            let cwd = display_path(
                                &session.cwd,
                                &self.request.cwd,
                                self.request.verbose_paths,
                            );
                            let title = session.title.as_deref().unwrap_or("-");

                            if is_claude {
                                let sidechain = match session.is_sidechain {
                                    Some(true) => "S",
                                    Some(false) => "",
                                    None => "-",
                                };
                                Row::new(vec![
                                    Cell::new(end),
                                    Cell::new(tokens),
                                    Cell::new(session.id.to_string()),
                                    Cell::new(sidechain),
                                    Cell::new(cwd),
                                    Cell::new(title.to_string()),
                                ])
                            } else {
                                Row::new(vec![
                                    Cell::new(end),
                                    Cell::new(tokens),
                                    Cell::new(session.id.to_string()),
                                    Cell::new(cwd),
                                    Cell::new(title.to_string()),
                                ])
                            }
                        })
                        .collect::<Vec<_>>();

                    let widths = if is_claude {
                        vec![
                            Constraint::Length(16),
                            Constraint::Length(10),
                            Constraint::Length(24),
                            Constraint::Length(2),
                            Constraint::Min(24),
                            Constraint::Min(20),
                        ]
                    } else {
                        vec![
                            Constraint::Length(16),
                            Constraint::Length(10),
                            Constraint::Length(24),
                            Constraint::Min(24),
                            Constraint::Min(20),
                        ]
                    };

                    let table = Table::new(rows, widths)
                        .header(header)
                        .block(Block::default().borders(Borders::ALL).title("Sessions"))
                        .row_highlight_style(Style::default().add_modifier(Modifier::REVERSED))
                        .highlight_symbol("> ");

                    frame.render_stateful_widget(table, area, &mut self.sessions_table_state);
                }
                StatsTab::SessionDetail => {
                    let Some(id) = &self.session_detail_id else {
                        frame.render_widget(
                            Paragraph::new("Select a session from Sessions tab and press Enter.")
                                .block(Block::default().borders(Borders::ALL)),
                            area,
                        );
                        return;
                    };
                    let Some(view) = build_session_detail_view(&cache.sessions_all, id) else {
                        frame.render_widget(
                            Paragraph::new("Session not found.")
                                .block(Block::default().borders(Borders::ALL)),
                            area,
                        );
                        return;
                    };
                    let session = view.session;
                    let mut lines = Vec::new();
                    lines.push(format!("Id: {}", session.id));
                    lines.push(format!(
                        "End: {}",
                        session
                            .end_ts
                            .with_timezone(&chrono::Local)
                            .format("%Y-%m-%d %H:%M:%S")
                    ));
                    if let Some(start) = session.start_ts {
                        lines.push(format!(
                            "Start: {}",
                            start
                                .with_timezone(&chrono::Local)
                                .format("%Y-%m-%d %H:%M:%S")
                        ));
                    }
                    lines.push(format!(
                        "Cwd: {}",
                        display_path(&session.cwd, &self.request.cwd, self.request.verbose_paths)
                    ));
                    if let Some(title) = session.title {
                        lines.push(format!("Title: {title}"));
                    }
                    if let Some(total) = session.token_usage.total {
                        lines.push(format!("Tokens (known): total={total}"));
                    } else {
                        lines.push("Tokens: unknown".to_string());
                    }
                    if let Some(v) = session.token_usage.input {
                        lines.push(format!("  input={v}"));
                    }
                    if let Some(v) = session.token_usage.output {
                        lines.push(format!("  output={v}"));
                    }
                    if let Some(v) = session.token_usage.cache {
                        lines.push(format!("  cache={v}"));
                    }
                    if let Some(v) = session.token_usage.reasoning {
                        lines.push(format!("  reasoning={v}"));
                    }

                    frame.render_widget(
                        Paragraph::new(lines.join("\n"))
                            .block(Block::default().borders(Borders::ALL))
                            .wrap(Wrap { trim: true }),
                        area,
                    );
                }
            },
        }
    }

    fn draw_filter_modal(&self, frame: &mut ratatui::Frame, area: Rect) {
        let popup_area = centered_rect(70, 70, area);
        frame.render_widget(Clear, popup_area);

        let block = Block::default().borders(Borders::ALL).title("Filter");
        frame.render_widget(block, popup_area);

        let inner = Rect {
            x: popup_area.x + 1,
            y: popup_area.y + 1,
            width: popup_area.width.saturating_sub(2),
            height: popup_area.height.saturating_sub(2),
        };

        let mut lines = Vec::new();
        lines.push(line_field(
            self.filter.active_field() == FilterField::Preset,
            format!("Preset: {}", self.filter.preset.label()),
        ));
        lines.push(line_field(
            self.filter.active_field() == FilterField::Since,
            format!("Since: {}", self.filter.since),
        ));
        lines.push(line_field(
            self.filter.active_field() == FilterField::Until,
            format!("Until: {}", self.filter.until),
        ));
        lines.push(line_field(
            self.filter.active_field() == FilterField::GroupBy,
            format!("Group-by: {:?}", self.filter.group_by),
        ));

        match self.tool {
            ToolKind::Codex => {
                lines.push(line_field(
                    self.filter.active_field() == FilterField::Breakdown,
                    format!(
                        "Breakdown: {}",
                        if self.filter.with_breakdown {
                            "on"
                        } else {
                            "off"
                        }
                    ),
                ));
            }
            ToolKind::ClaudeCode => {
                lines.push(line_field(
                    self.filter.active_field() == FilterField::Sidechain,
                    format!(
                        "Include sidechain: {}",
                        if self.filter.include_sidechain {
                            "yes"
                        } else {
                            "no"
                        }
                    ),
                ));
            }
            ToolKind::Cursor => {}
        }

        lines.push(Line::from(""));
        lines.push(line_field(
            self.filter.active_field() == FilterField::Apply,
            "Apply (Enter)".to_string(),
        ));
        lines.push(line_field(
            self.filter.active_field() == FilterField::Cancel,
            "Cancel (Enter/Esc)".to_string(),
        ));

        if let Some(err) = &self.filter.error {
            lines.push(Line::from(""));
            lines.push(Line::from(Span::styled(
                format!("Error: {err}"),
                Style::default().fg(Color::Red),
            )));
        }

        let paragraph = Paragraph::new(lines)
            .block(Block::default())
            .wrap(Wrap { trim: true });
        frame.render_widget(paragraph, inner);
    }
}

impl Drop for StatsTuiApp {
    fn drop(&mut self) {
        self.stop_scan();
    }
}

fn tool_title(tool: ToolKind) -> &'static str {
    match tool {
        ToolKind::Codex => "codex stats",
        ToolKind::ClaudeCode => "claude-code stats",
        ToolKind::Cursor => "cursor stats",
    }
}

fn format_u64_count(value: u64) -> String {
    let s = value.to_string();
    let mut out = String::with_capacity(s.len().saturating_add(s.len() / 3));
    for (idx, ch) in s.chars().enumerate() {
        out.push(ch);
        let remaining = s.len().saturating_sub(idx).saturating_sub(1);
        if remaining > 0 && remaining % 3 == 0 {
            out.push(',');
        }
    }
    out
}

fn format_usize_count(value: usize) -> String {
    format_u64_count(value as u64)
}

fn bucket_tokens(bucket: &crate::usage_stats::StatsBucket, metric: TrendMetric) -> Option<u64> {
    match metric {
        TrendMetric::Overall => Some(bucket.totals.tokens_total_known),
        TrendMetric::Primary => bucket
            .sidechain_totals
            .as_ref()
            .map(|totals| totals.primary.tokens_total_known),
        TrendMetric::Sidechain => bucket
            .sidechain_totals
            .as_ref()
            .map(|totals| totals.sidechain.tokens_total_known),
    }
}

fn line_field(active: bool, text: String) -> Line<'static> {
    if active {
        Line::from(Span::styled(
            format!("> {text}"),
            Style::default()
                .fg(Color::Yellow)
                .add_modifier(Modifier::BOLD),
        ))
    } else {
        Line::from(Span::raw(format!("  {text}")))
    }
}

fn centered_rect(percent_x: u16, percent_y: u16, r: Rect) -> Rect {
    let popup_layout = Layout::default()
        .direction(Direction::Vertical)
        .constraints(
            [
                Constraint::Percentage((100 - percent_y) / 2),
                Constraint::Percentage(percent_y),
                Constraint::Percentage((100 - percent_y) / 2),
            ]
            .as_ref(),
        )
        .split(r);

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

#[cfg(test)]
mod tests {
    use super::*;
    use chrono::TimeZone;
    use std::path::PathBuf;

    #[test]
    fn filter_form_last_7d_builds_last_args() {
        let request = StatsTuiScanRequest {
            cwd: "/p".into(),
            group_by: GroupBy::Day,
            range: TimeRangeArgs::default(),
            limit: 200,
            verbose_paths: false,
            with_breakdown: false,
            include_sidechain: true,
        };

        let mut form = FilterFormState::new(ToolKind::Codex, &request);
        form.preset = TimePreset::Last7d;
        let range = form.build_range_args();
        assert_eq!(range.last.as_deref(), Some("7d"));
        assert!(range.since.is_none());
        assert!(range.until.is_none());
    }

    #[test]
    fn filter_form_custom_builds_since_until() {
        let request = StatsTuiScanRequest {
            cwd: "/p".into(),
            group_by: GroupBy::Day,
            range: TimeRangeArgs::default(),
            limit: 200,
            verbose_paths: false,
            with_breakdown: false,
            include_sidechain: true,
        };

        let mut form = FilterFormState::new(ToolKind::Codex, &request);
        form.preset = TimePreset::Custom;
        form.since = "2026-02-01".to_string();
        form.until = "2026-03-01".to_string();
        let range = form.build_range_args();
        assert_eq!(range.since.as_deref(), Some("2026-02-01"));
        assert_eq!(range.until.as_deref(), Some("2026-03-01"));
        assert!(range.last.is_none());
    }

    fn make_app(tool: ToolKind) -> StatsTuiApp {
        let request = StatsTuiScanRequest {
            cwd: PathBuf::from("/p"),
            group_by: GroupBy::Day,
            range: TimeRangeArgs::default(),
            limit: 200,
            verbose_paths: false,
            with_breakdown: false,
            include_sidechain: true,
        };
        let scan_fn: ScanFn = Arc::new(|_request, _tx, _cancel| Ok(vec![]));
        StatsTuiApp::new(tool, request, scan_fn)
    }

    fn make_session(id: &str, end_ts: chrono::DateTime<chrono::Utc>) -> SessionRecord {
        SessionRecord {
            tool: ToolKind::Codex,
            id: SessionId(id.to_string()),
            cwd: PathBuf::from("/p"),
            title: None,
            start_ts: None,
            end_ts,
            token_usage: crate::usage_stats::TokenUsage {
                total: Some(1),
                ..crate::usage_stats::TokenUsage::default()
            },
            is_sidechain: None,
        }
    }

    #[test]
    fn trend_metric_cycles_for_claude_in_trend_tab() {
        let mut app = make_app(ToolKind::ClaudeCode);
        app.tab = StatsTab::Trend;
        assert_eq!(app.trend_metric, TrendMetric::Overall);
        app.trend_cycle_metric();
        assert_eq!(app.trend_metric, TrendMetric::Primary);
        app.trend_cycle_metric();
        assert_eq!(app.trend_metric, TrendMetric::Sidechain);
        app.trend_cycle_metric();
        assert_eq!(app.trend_metric, TrendMetric::Overall);
    }

    #[test]
    fn trend_move_updates_table_selection() {
        let mut app = make_app(ToolKind::Codex);
        app.tab = StatsTab::Trend;

        let sessions = vec![
            make_session(
                "t1",
                chrono::Utc.with_ymd_and_hms(2026, 1, 1, 12, 0, 0).unwrap(),
            ),
            make_session(
                "t2",
                chrono::Utc.with_ymd_and_hms(2026, 1, 10, 12, 0, 0).unwrap(),
            ),
        ];
        let cache = app.build_cache(&sessions);
        app.status = ScanStatus::Ready(Box::new(cache));
        app.trend_table_state.select(Some(0));

        app.trend_move(1);
        assert_eq!(app.trend_table_state.selected(), Some(1));
        app.trend_move(1);
        assert_eq!(app.trend_table_state.selected(), Some(1));
        app.trend_move(-1);
        assert_eq!(app.trend_table_state.selected(), Some(0));
    }

    #[test]
    fn sessions_move_updates_table_selection() {
        let mut app = make_app(ToolKind::Codex);
        app.tab = StatsTab::Sessions;

        let sessions = vec![
            make_session(
                "t1",
                chrono::Utc.with_ymd_and_hms(2026, 1, 1, 12, 0, 0).unwrap(),
            ),
            make_session(
                "t2",
                chrono::Utc.with_ymd_and_hms(2026, 1, 2, 12, 0, 0).unwrap(),
            ),
        ];
        let cache = app.build_cache(&sessions);
        app.status = ScanStatus::Ready(Box::new(cache));
        app.sessions_table_state.select(Some(0));

        app.sessions_move(1);
        assert_eq!(app.sessions_table_state.selected(), Some(1));
        app.sessions_move(1);
        assert_eq!(app.sessions_table_state.selected(), Some(1));
        app.sessions_move(-1);
        assert_eq!(app.sessions_table_state.selected(), Some(0));
    }
}
