//! Diff preview: summary + filterable entry list.

use ratatui::layout::{Alignment, Constraint, Layout};
use ratatui::style::{Modifier, Style};
use ratatui::text::{Line, Span};
use ratatui::widgets::{Block, List, ListItem, ListState, Paragraph};
use ratatui::Frame;

use crate::diff::EntryKind;
use crate::scan::fmt_bytes;
use crate::tui::app::{App, DiffState};
use crate::tui::theme::Theme;

const ESTIMATED_SPEED_BPS: u64 = 30 * 1024 * 1024;

pub fn render(f: &mut Frame, app: &App) {
    let theme = &app.theme;
    let area = f.area();

    match &app.diff_state {
        DiffState::Idle => render_idle(f, app, area),
        DiffState::Loading => render_loading(f, app, area),
        DiffState::Error(msg) => render_error(f, app, area, msg),
        DiffState::Ready {
            result,
            source,
            destination,
            profile_name,
            dap_id,
            mode,
        } => {
            render_ready(
                f,
                app,
                theme,
                area,
                result,
                source,
                destination,
                profile_name,
                dap_id,
                *mode,
            );
        }
    }
}

// ── Loading / idle / error screens ─────────────────────────────────────────

fn render_loading(f: &mut Frame, app: &App, area: ratatui::layout::Rect) {
    let theme = &app.theme;
    let outer = chrome(app, " dapctl — diff ");
    let inner = outer.inner(area);
    f.render_widget(outer, area);

    let [_, center, _] = Layout::vertical([
        Constraint::Fill(1),
        Constraint::Length(1),
        Constraint::Fill(1),
    ])
    .areas(inner);

    f.render_widget(
        Paragraph::new(Line::from(Span::styled(
            "  Computing diff…",
            Style::default()
                .fg(theme.muted)
                .add_modifier(Modifier::ITALIC),
        )))
        .alignment(Alignment::Center),
        center,
    );
}

fn render_idle(f: &mut Frame, app: &App, area: ratatui::layout::Rect) {
    let outer = chrome(app, " dapctl — diff ");
    let inner = outer.inner(area);
    f.render_widget(outer, area);
    let theme = &app.theme;

    let [_, center, _] = Layout::vertical([
        Constraint::Fill(1),
        Constraint::Length(1),
        Constraint::Fill(1),
    ])
    .areas(inner);

    f.render_widget(
        Paragraph::new(Line::from(Span::styled(
            "  press enter on a profile to compute diff",
            Style::default().fg(theme.muted),
        )))
        .alignment(Alignment::Center),
        center,
    );
}

fn render_error(f: &mut Frame, app: &App, area: ratatui::layout::Rect, msg: &str) {
    let theme = &app.theme;
    let outer = chrome(app, " dapctl — diff ");
    let inner = outer.inner(area);
    f.render_widget(outer, area);

    let [_, center, _] = Layout::vertical([
        Constraint::Fill(1),
        Constraint::Length(3),
        Constraint::Fill(1),
    ])
    .areas(inner);

    f.render_widget(
        Paragraph::new(vec![
            Line::from(Span::styled(
                "  Error",
                Style::default().fg(theme.err).add_modifier(Modifier::BOLD),
            )),
            Line::from(Span::styled(
                format!("  {msg}"),
                Style::default().fg(theme.err),
            )),
            Line::from(Span::styled(
                "  press esc to go back",
                Style::default().fg(theme.muted),
            )),
        ]),
        center,
    );
}

// ── Main diff view ──────────────────────────────────────────────────────────

#[allow(clippy::too_many_arguments)]
fn render_ready(
    f: &mut Frame,
    app: &App,
    theme: &Theme,
    area: ratatui::layout::Rect,
    result: &crate::diff::DiffResult,
    source: &camino::Utf8PathBuf,
    destination: &camino::Utf8PathBuf,
    profile_name: &str,
    dap_id: &str,
    mode: crate::config::Mode,
) {
    let plan = &result.plan;

    let title = format!(" dapctl — diff  {profile_name} ");
    let outer = chrome(app, &title);
    let inner = outer.inner(area);
    f.render_widget(outer, area);

    // ── Layout: summary | list_header | list | footer ───────────────────
    let [summary_area, list_header_area, list_area, footer_area] = Layout::vertical([
        Constraint::Length(10),
        Constraint::Length(1),
        Constraint::Fill(1),
        Constraint::Length(1),
    ])
    .areas(inner);

    render_summary(
        f,
        theme,
        summary_area,
        app,
        plan,
        source,
        destination,
        dap_id,
        mode,
    );
    render_list_header(f, theme, list_header_area, app, plan);
    render_entry_list(f, app, theme, list_area, plan, mode);
    render_footer(f, app, theme, footer_area, mode);
}

#[allow(clippy::too_many_arguments)]
fn render_summary(
    f: &mut Frame,
    theme: &Theme,
    area: ratatui::layout::Rect,
    app: &App,
    plan: &crate::diff::Plan,
    source: &camino::Utf8PathBuf,
    destination: &camino::Utf8PathBuf,
    dap_id: &str,
    mode: crate::config::Mode,
) {
    let src_short = truncate(source.as_str(), 45);
    let dst_short = truncate(destination.as_str(), 45);
    let mode_str = format!("{mode:?}").to_lowercase();
    let is_selective = matches!(mode, crate::config::Mode::Selective);

    let new_b = plan.total_bytes(EntryKind::New);
    let mod_b = plan.total_bytes(EntryKind::Modified);
    let orp_b = plan.total_bytes(EntryKind::Orphan);
    let same_b = plan.total_bytes(EntryKind::Same);
    let transfer = new_b + mod_b;
    let eta = plan.eta_secs(ESTIMATED_SPEED_BPS);

    let lines = vec![
        // source → dest
        Line::from(vec![
            Span::styled(format!("  {src_short}"), Style::default().fg(theme.fg)),
            Span::styled("  →  ", Style::default().fg(theme.muted)),
            Span::styled(dst_short.to_string(), Style::default().fg(theme.fg)),
            Span::styled(
                format!("  ({dap_id}, {mode_str})"),
                Style::default().fg(theme.muted),
            ),
        ]),
        Line::from(""),
        // counts
        Line::from(vec![
            Span::styled(
                "  [+]",
                Style::default().fg(theme.fg).add_modifier(Modifier::BOLD),
            ),
            Span::raw(format!(
                "  {:>6}  new          {}",
                plan.count(EntryKind::New),
                fmt_bytes(new_b)
            )),
        ]),
        Line::from(vec![
            Span::styled(
                "  [~]",
                Style::default().fg(theme.warn).add_modifier(Modifier::BOLD),
            ),
            Span::raw(format!(
                "  {:>6}  modified     {}",
                plan.count(EntryKind::Modified),
                fmt_bytes(mod_b)
            )),
        ]),
        Line::from(vec![
            Span::styled(
                "  [-]",
                Style::default().fg(theme.err).add_modifier(Modifier::BOLD),
            ),
            Span::raw(format!(
                "  {:>6}  orphans      {}",
                plan.count(EntryKind::Orphan),
                fmt_bytes(orp_b)
            )),
        ]),
        Line::from(vec![
            Span::styled(
                "  [=]",
                Style::default()
                    .fg(theme.muted)
                    .add_modifier(Modifier::BOLD),
            ),
            Span::styled(
                format!(
                    "  {:>6}  unchanged    {}",
                    plan.count(EntryKind::Same),
                    fmt_bytes(same_b)
                ),
                Style::default().fg(theme.muted),
            ),
        ]),
        Line::from(""),
        // ETA / selective selection count
        if is_selective {
            let selected_dirs = &app.selective_paths;
            let sel_count = plan
                .entries
                .iter()
                .filter(|e| {
                    let p = e.path.parent().map(|p| p.as_str()).unwrap_or("");
                    selected_dirs.contains(p)
                })
                .count();
            let total = plan.entries.len();
            Line::from(vec![
                Span::raw("  selected: "),
                Span::styled(
                    format!("{sel_count}/{total}"),
                    Style::default().fg(theme.warn).add_modifier(Modifier::BOLD),
                ),
                Span::raw("  files   transfer: "),
                Span::styled(
                    fmt_bytes(transfer),
                    Style::default().fg(theme.fg).add_modifier(Modifier::BOLD),
                ),
            ])
        } else {
            Line::from(vec![
                Span::raw("  transfer: "),
                Span::styled(
                    fmt_bytes(transfer),
                    Style::default().fg(theme.fg).add_modifier(Modifier::BOLD),
                ),
                Span::raw("   ETA: "),
                Span::styled(
                    fmt_eta(eta),
                    Style::default().fg(theme.fg).add_modifier(Modifier::BOLD),
                ),
            ])
        },
        // Warning line (or empty) — fits in the 10-line summary area
        if !plan.warnings.is_empty() {
            let n = plan.warnings.len();
            Line::from(Span::styled(
                format!("  ⚠  {} path warning{}", n, if n == 1 { "" } else { "s" }),
                Style::default().fg(theme.warn),
            ))
        } else {
            Line::from("")
        },
    ];

    f.render_widget(
        Paragraph::new(lines).style(Style::default().fg(theme.fg).bg(theme.bg)),
        area,
    );
}

fn render_list_header(
    f: &mut Frame,
    theme: &Theme,
    area: ratatui::layout::Rect,
    app: &App,
    plan: &crate::diff::Plan,
) {
    use crate::tui::app::EntryFilter;

    let tabs: &[(EntryFilter, &str, Option<EntryKind>)] = &[
        (EntryFilter::All, "All", None),
        (EntryFilter::New, "+New", Some(EntryKind::New)),
        (EntryFilter::Modified, "~Mod", Some(EntryKind::Modified)),
        (EntryFilter::Orphan, "-Orphan", Some(EntryKind::Orphan)),
        (EntryFilter::Same, "=Same", Some(EntryKind::Same)),
    ];

    let mut spans = vec![Span::raw("  ")];
    for (tab_filter, label, kind_opt) in tabs {
        let count = match kind_opt {
            Some(k) => plan.entries.iter().filter(|e| e.kind == *k).count(),
            None => plan.entries.len(),
        };
        let is_active = app.diff_entry_filter == *tab_filter;
        let text = format!(" {label} ({count}) ");
        let style = if is_active {
            Style::default()
                .fg(theme.sel_fg)
                .bg(theme.sel_bg)
                .add_modifier(Modifier::BOLD)
        } else {
            Style::default().fg(theme.muted)
        };
        spans.push(Span::styled(text, style));
        spans.push(Span::raw(" "));
    }

    f.render_widget(
        Paragraph::new(Line::from(spans)).style(Style::default().bg(theme.bg)),
        area,
    );
}

fn render_entry_list(
    f: &mut Frame,
    app: &App,
    theme: &Theme,
    area: ratatui::layout::Rect,
    plan: &crate::diff::Plan,
    mode: crate::config::Mode,
) {
    let filter = app.diff_entry_filter;
    let is_selective = matches!(mode, crate::config::Mode::Selective);
    let sel_col = if is_selective { 4 } else { 0 };
    let path_width = (area.width as usize).saturating_sub(20 + sel_col).max(20);

    let filtered: Vec<&crate::diff::Entry> = plan
        .entries
        .iter()
        .filter(|e| filter.matches(e.kind))
        .collect();

    let items: Vec<ListItem> = filtered
        .iter()
        .map(|e| {
            let (tag, tag_style) = match e.kind {
                EntryKind::New => ("[+]", Style::default().fg(theme.fg)),
                EntryKind::Modified => ("[~]", Style::default().fg(theme.warn)),
                EntryKind::Orphan => ("[-]", Style::default().fg(theme.err)),
                EntryKind::Same => ("[=]", Style::default().fg(theme.muted)),
            };
            let path = truncate(e.path.as_str(), path_width);
            let size = fmt_bytes(e.size_bytes);

            let mut spans = vec![Span::raw("  ")];

            if is_selective {
                let parent = e.path.parent().map(|p| p.as_str()).unwrap_or("");
                let selected = app.selective_paths.contains(parent);
                let (sym, sym_style) = if selected {
                    (
                        "◆ ",
                        Style::default().fg(theme.fg).add_modifier(Modifier::BOLD),
                    )
                } else {
                    ("◇ ", Style::default().fg(theme.muted))
                };
                spans.push(Span::styled(sym, sym_style));
            }

            spans.push(Span::styled(tag, tag_style.add_modifier(Modifier::BOLD)));
            spans.push(Span::raw("  "));
            spans.push(Span::styled(
                format!("{path:<path_width$}"),
                if e.kind == EntryKind::Same {
                    Style::default().fg(theme.muted)
                } else {
                    Style::default().fg(theme.fg)
                },
            ));
            spans.push(Span::styled(
                format!("  {size:>10}"),
                Style::default().fg(theme.muted),
            ));

            ListItem::new(Line::from(spans))
        })
        .collect();

    let selected = app.diff_entry_idx.min(filtered.len().saturating_sub(1));
    let mut state = ListState::default();
    if !filtered.is_empty() {
        state.select(Some(selected));
    }

    let list = List::new(items)
        .style(Style::default().bg(theme.bg))
        .highlight_style(
            Style::default()
                .fg(theme.sel_fg)
                .bg(theme.sel_bg)
                .add_modifier(Modifier::BOLD),
        );

    f.render_stateful_widget(list, area, &mut state);
}

fn render_footer(
    f: &mut Frame,
    app: &App,
    theme: &Theme,
    area: ratatui::layout::Rect,
    mode: crate::config::Mode,
) {
    let is_selective = matches!(mode, crate::config::Mode::Selective);
    let footer_line = if let Some(ref msg) = app.flash {
        Line::from(Span::styled(
            format!("  {msg}"),
            Style::default().fg(theme.warn),
        ))
    } else if is_selective {
        Line::from(vec![
            kb("j/k", theme),
            Span::raw(" scroll  "),
            kb("tab", theme),
            Span::raw(" filter  "),
            kb("x", theme),
            Span::raw(" toggle album  "),
            kb("r", theme),
            Span::raw(" re-diff  "),
            kb("y", theme),
            Span::raw(" sync selected  "),
            kb("esc", theme),
            Span::raw(" back"),
        ])
    } else {
        Line::from(vec![
            kb("j/k", theme),
            Span::raw(" scroll  "),
            kb("tab", theme),
            Span::raw(" filter  "),
            kb("space", theme),
            Span::raw(" preview  "),
            kb("r", theme),
            Span::raw(" re-diff  "),
            kb("y", theme),
            Span::raw(" sync  "),
            kb("esc", theme),
            Span::raw(" back"),
        ])
    };
    f.render_widget(
        Paragraph::new(footer_line).style(Style::default().fg(theme.muted)),
        area,
    );
}

// ── Helpers ─────────────────────────────────────────────────────────────────

fn chrome<'a>(app: &'a App, title: &'a str) -> Block<'a> {
    Block::bordered()
        .title(
            Line::from(title).style(
                Style::default()
                    .fg(app.theme.fg)
                    .add_modifier(Modifier::BOLD),
            ),
        )
        .border_style(Style::default().fg(app.theme.muted))
        .style(Style::default().bg(app.theme.bg))
}

fn kb<'a>(key: &'a str, theme: &'a Theme) -> Span<'a> {
    Span::styled(
        key.to_string(),
        Style::default().fg(theme.fg).add_modifier(Modifier::BOLD),
    )
}

fn truncate(s: &str, max: usize) -> &str {
    if s.len() <= max {
        s
    } else {
        &s[..max]
    }
}

fn fmt_eta(secs: u64) -> String {
    if secs == 0 {
        return "< 1s".to_owned();
    }
    if secs < 60 {
        return format!("{secs}s");
    }
    let m = secs / 60;
    let s = secs % 60;
    if m < 60 {
        return format!("{m}m {s:02}s");
    }
    let h = m / 60;
    let m = m % 60;
    format!("{h}h {m:02}m")
}
