use std::time::Instant;

use ratatui::Frame;
use ratatui::layout::Rect;
use ratatui::style::{Modifier, Style};
use ratatui::text::{Line, Span};
use ratatui::widgets::{Paragraph, Wrap};

use crate::app::model::app_state::AppState;
use crate::app::model::explain_context::CompareSlot;
use crate::app::model::shared::flash_timer::FlashId;
use crate::domain::explain_plan::{self, ComparisonVerdict};
use crate::primitives::utils::text_utils::truncate_to_width_with;
use crate::theme::ThemePalette;

pub fn render(
    frame: &mut Frame,
    area: Rect,
    state: &AppState,
    now: Instant,
    theme: &ThemePalette,
) -> u16 {
    let can_yank = state.explain.left.is_some() && state.explain.right.is_some();
    let left = state.explain.left.as_ref();
    let right = state.explain.right.as_ref();
    let scroll_offset = state.explain.compare_scroll_offset;

    let mut lines: Vec<Line> = Vec::new();
    let mut flash_mask: Vec<bool> = Vec::new();

    if let (Some(l), Some(r)) = (left, right) {
        render_verdict_section(&mut lines, &mut flash_mask, l, r, area.width, theme);
    }

    if area.width >= 60 {
        render_slot_columns(&mut lines, &mut flash_mask, left, right, area.width, theme);
    } else {
        render_slot_stacked(&mut lines, &mut flash_mask, left, right, theme);
    }

    if lines.is_empty() {
        lines.push(Line::from(Span::styled(
            " Run EXPLAIN (Ctrl+E) to start comparing.",
            Style::default().fg(theme.semantic.text.placeholder),
        )));
        flash_mask.push(false);
    }

    let max_scroll = lines.len().saturating_sub(area.height as usize);
    let clamped = scroll_offset.min(max_scroll);
    let mut visible: Vec<Line> = lines.into_iter().skip(clamped).collect();
    let visible_mask: Vec<bool> = flash_mask.into_iter().skip(clamped).collect();

    let flash_active = can_yank && state.flash_timers.is_active(FlashId::SqlModal, now);
    crate::primitives::atoms::apply_yank_flash_masked(
        &mut visible,
        flash_active,
        &visible_mask,
        theme,
    );

    frame.render_widget(
        Paragraph::new(visible)
            .style(Style::default().fg(theme.semantic.text.primary))
            .wrap(Wrap { trim: false }),
        area,
    );
    area.height
}

fn push_empty(lines: &mut Vec<Line>, flash_mask: &mut Vec<bool>) {
    lines.push(Line::raw(""));
    flash_mask.push(false);
}

// Copied to clipboard — flash on yank
fn push_content(lines: &mut Vec<Line>, flash_mask: &mut Vec<bool>, line: Line<'static>) {
    lines.push(line);
    flash_mask.push(true);
}

// UI chrome — never flash
fn push_chrome(lines: &mut Vec<Line>, flash_mask: &mut Vec<bool>, line: Line<'static>) {
    lines.push(line);
    flash_mask.push(false);
}

// ── Verdict (only when both slots are populated) ─────────────────────────────

fn render_verdict_section(
    lines: &mut Vec<Line>,
    flash_mask: &mut Vec<bool>,
    left: &CompareSlot,
    right: &CompareSlot,
    width: u16,
    theme: &ThemePalette,
) {
    let result = explain_plan::compare_plans(&left.plan, &right.plan);

    let (verdict_label, verdict_style) = match result.verdict {
        ComparisonVerdict::Improved => (
            "\u{2193} Improved",
            Style::default()
                .fg(theme.semantic.status.success)
                .add_modifier(Modifier::BOLD),
        ),
        ComparisonVerdict::Worsened => (
            "\u{2191} Worsened",
            Style::default()
                .fg(theme.semantic.status.error)
                .add_modifier(Modifier::BOLD),
        ),
        ComparisonVerdict::Similar => (
            "\u{2248} Similar",
            Style::default()
                .fg(theme.semantic.text.accent)
                .add_modifier(Modifier::BOLD),
        ),
        ComparisonVerdict::Unavailable => (
            "Comparison unavailable",
            Style::default()
                .fg(theme.semantic.text.muted)
                .add_modifier(Modifier::BOLD),
        ),
    };

    push_empty(lines, flash_mask);
    push_content(
        lines,
        flash_mask,
        Line::from(Span::styled(format!(" {verdict_label}"), verdict_style)),
    );
    push_empty(lines, flash_mask);

    for reason in &result.reasons {
        push_content(
            lines,
            flash_mask,
            Line::from(vec![
                Span::styled(
                    "  \u{2022} ",
                    Style::default().fg(theme.semantic.text.muted),
                ),
                Span::styled(
                    reason.clone(),
                    Style::default().fg(theme.semantic.text.primary),
                ),
            ]),
        );
    }
    if !result.reasons.is_empty() {
        push_empty(lines, flash_mask);
    }

    let sep = "\u{2500}".repeat(width.saturating_sub(2) as usize);
    push_chrome(
        lines,
        flash_mask,
        Line::styled(format!(" {sep}"), theme.modal_border_style()),
    );
    push_empty(lines, flash_mask);
}

// ── Side-by-side slot columns (shared across all states) ─────────────────────

fn render_slot_columns(
    lines: &mut Vec<Line>,
    flash_mask: &mut Vec<bool>,
    left: Option<&CompareSlot>,
    right: Option<&CompareSlot>,
    total_width: u16,
    theme: &ThemePalette,
) {
    let half = (total_width.saturating_sub(3) / 2) as usize;
    let sep = Span::styled(" \u{2502} ", theme.modal_border_style());

    let active_header = Style::default()
        .fg(theme.semantic.text.accent)
        .add_modifier(Modifier::BOLD);
    let empty_header = Style::default()
        .fg(theme.semantic.text.dim)
        .add_modifier(Modifier::BOLD);

    let left_label = match left {
        Some(s) => format!(" {}", s.source.label()),
        None => " Previous".to_string(),
    };
    let right_label = match right {
        Some(s) => format!(" {}", s.source.label()),
        None => " Latest".to_string(),
    };

    push_chrome(
        lines,
        flash_mask,
        Line::from(vec![
            Span::styled(
                pad_or_truncate(&left_label, half),
                if left.is_some() {
                    active_header
                } else {
                    empty_header
                },
            ),
            sep.clone(),
            Span::styled(
                pad_or_truncate(&right_label, half),
                if right.is_some() {
                    active_header
                } else {
                    empty_header
                },
            ),
        ]),
    );

    let detail_style = Style::default().fg(theme.semantic.text.muted);
    let placeholder_style = Style::default().fg(theme.semantic.text.placeholder);

    let left_detail = slot_detail_text(left);
    let right_detail = slot_detail_text(right);

    push_chrome(
        lines,
        flash_mask,
        Line::from(vec![
            Span::styled(
                pad_or_truncate(&left_detail, half),
                if left.is_some() {
                    detail_style
                } else {
                    placeholder_style
                },
            ),
            sep.clone(),
            Span::styled(
                pad_or_truncate(&right_detail, half),
                if right.is_some() {
                    detail_style
                } else {
                    placeholder_style
                },
            ),
        ]),
    );

    let thin_sep = "\u{2500}".repeat(half.saturating_sub(1));
    push_chrome(
        lines,
        flash_mask,
        Line::from(vec![
            Span::styled(
                format!(" {thin_sep}"),
                Style::default().fg(theme.semantic.text.dim),
            ),
            sep.clone(),
            Span::styled(
                format!(" {thin_sep}"),
                Style::default().fg(theme.semantic.text.dim),
            ),
        ]),
    );

    let dim_style = Style::default().fg(theme.semantic.text.dim);

    let l_plan: Vec<&str> = left
        .map(|s| s.plan.raw_text.lines().collect())
        .unwrap_or_default();
    let r_plan: Vec<&str> = right
        .map(|s| s.plan.raw_text.lines().collect())
        .unwrap_or_default();
    let max = l_plan.len().max(r_plan.len());

    for i in 0..max {
        let l = l_plan.get(i).unwrap_or(&"");
        let r = r_plan.get(i).unwrap_or(&"");

        let mut row_spans = vec![Span::styled(" ".to_string(), dim_style)];
        row_spans.extend(super::plan_highlight::highlight_truncated(
            l,
            half.saturating_sub(1),
            theme,
        ));
        row_spans.push(sep.clone());
        row_spans.push(Span::styled(" ".to_string(), dim_style));
        row_spans.extend(super::plan_highlight::highlight_truncated(
            r,
            half.saturating_sub(1),
            theme,
        ));
        push_content(lines, flash_mask, Line::from(row_spans));
    }
}

// ── Stacked layout (narrow terminals) ────────────────────────────────────────

fn render_slot_stacked(
    lines: &mut Vec<Line>,
    flash_mask: &mut Vec<bool>,
    left: Option<&CompareSlot>,
    right: Option<&CompareSlot>,
    theme: &ThemePalette,
) {
    let header_style = Style::default()
        .fg(theme.semantic.text.accent)
        .add_modifier(Modifier::BOLD);
    let badge_style = Style::default().fg(theme.semantic.text.muted);

    render_stacked_slot(
        lines,
        flash_mask,
        left,
        " Previous",
        header_style,
        badge_style,
        theme,
    );
    push_empty(lines, flash_mask);
    render_stacked_slot(
        lines,
        flash_mask,
        right,
        " Latest",
        header_style,
        badge_style,
        theme,
    );
}

fn render_stacked_slot(
    lines: &mut Vec<Line>,
    flash_mask: &mut Vec<bool>,
    slot: Option<&CompareSlot>,
    empty_label: &str,
    active_style: Style,
    badge_style: Style,
    theme: &ThemePalette,
) {
    if let Some(s) = slot {
        push_chrome(
            lines,
            flash_mask,
            Line::from(Span::styled(format!(" {}", s.source.label()), active_style)),
        );
        let time_secs = s.plan.execution_secs();
        push_chrome(
            lines,
            flash_mask,
            Line::from(Span::styled(
                format!("  {}  ({:.2}s)", mode_label(s.plan.is_analyze), time_secs),
                badge_style,
            )),
        );
        for line in s.plan.raw_text.lines() {
            push_content(
                lines,
                flash_mask,
                super::plan_highlight::highlight_plan_line(line, theme),
            );
        }
    } else {
        push_chrome(
            lines,
            flash_mask,
            Line::from(Span::styled(
                empty_label.to_string(),
                Style::default()
                    .fg(theme.semantic.text.dim)
                    .add_modifier(Modifier::BOLD),
            )),
        );
        push_chrome(
            lines,
            flash_mask,
            Line::from(Span::styled(
                "  Run EXPLAIN again to compare",
                Style::default().fg(theme.semantic.text.placeholder),
            )),
        );
    }
}

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

fn slot_detail_text(slot: Option<&CompareSlot>) -> String {
    match slot {
        Some(s) => {
            let time_secs = s.plan.execution_secs();
            format!(" {}  ({:.2}s)", mode_label(s.plan.is_analyze), time_secs)
        }
        None => " Run EXPLAIN again".to_string(),
    }
}

fn mode_label(is_analyze: bool) -> &'static str {
    if is_analyze { "ANALYZE" } else { "EXPLAIN" }
}

pub(super) fn pad_or_truncate(s: &str, width: usize) -> String {
    use unicode_width::UnicodeWidthStr;

    // Truncation can land short of `width` (a 2-cell char may not fit the
    // last cell), so pad the remainder to keep side-by-side columns aligned
    let truncated = truncate_to_width_with(s, width, "\u{2026}");
    let pad = width.saturating_sub(UnicodeWidthStr::width(truncated.as_str()));
    format!("{truncated}{}", " ".repeat(pad))
}

#[cfg(test)]
mod tests {
    use super::*;
    use crate::app::model::explain_context::SlotSource;
    use crate::domain::explain_plan::ExplainPlan;
    use crate::theme::DEFAULT_THEME;

    mod pad_or_truncate_tests {
        use super::super::pad_or_truncate;
        use rstest::rstest;
        use unicode_width::UnicodeWidthStr;

        #[rstest]
        #[case("abc", 5, "abc  ")]
        #[case("abcdef", 5, "abcd\u{2026}")]
        #[case("日本語テスト", 5, "日本\u{2026}")]
        #[case("日本語", 4, "日\u{2026} ")]
        fn fills_exact_display_width(
            #[case] input: &str,
            #[case] width: usize,
            #[case] expected: &str,
        ) {
            let result = pad_or_truncate(input, width);

            assert_eq!(result, expected);
            assert_eq!(UnicodeWidthStr::width(result.as_str()), width);
        }
    }

    fn sample_slot(label: SlotSource, plan: &str) -> CompareSlot {
        CompareSlot {
            plan: ExplainPlan {
                raw_text: plan.to_string(),
                top_node_type: Some("Seq Scan".to_string()),
                total_cost: Some(10.0),
                estimated_rows: Some(1),
                is_analyze: false,
                execution_time_ms: 250,
            },
            query_snippet: "SELECT 1".to_string(),
            full_query: "SELECT 1".to_string(),
            source: label,
        }
    }

    #[test]
    fn stacked_compare_flashes_only_plan_content_rows() {
        let left = sample_slot(
            SlotSource::AutoPrevious,
            "Seq Scan on users  (cost=0.00..10.00 rows=1 width=32)\n  Filter: (id > 1)",
        );
        let right = sample_slot(
            SlotSource::AutoLatest,
            "Index Scan using users_pkey on users  (cost=0.00..5.00 rows=1 width=32)",
        );

        let mut lines = Vec::new();
        let mut flash_mask = Vec::new();
        render_slot_stacked(
            &mut lines,
            &mut flash_mask,
            Some(&left),
            Some(&right),
            &DEFAULT_THEME,
        );

        let rendered: Vec<String> = lines
            .iter()
            .map(|line| {
                line.spans
                    .iter()
                    .map(|span| span.content.as_ref())
                    .collect()
            })
            .collect();

        assert_eq!(lines.len(), flash_mask.len());
        assert_eq!(
            flash_mask,
            vec![false, false, true, true, false, false, false, true]
        );
        assert!(rendered[0].contains("Previous"));
        assert!(rendered[1].contains("EXPLAIN"));
        assert!(rendered[2].contains("Seq Scan on users"));
        assert!(rendered[3].contains("Filter:"));
        assert!(rendered[4].is_empty());
        assert!(rendered[5].contains("Latest"));
        assert!(rendered[6].contains("EXPLAIN"));
        assert!(rendered[7].contains("Index Scan using users_pkey"));
    }

    #[test]
    fn stacked_compare_empty_slot_never_marks_flashable_rows() {
        let mut lines = Vec::new();
        let mut flash_mask = Vec::new();
        render_slot_stacked(&mut lines, &mut flash_mask, None, None, &DEFAULT_THEME);

        assert_eq!(lines.len(), flash_mask.len());
        assert!(flash_mask.iter().all(|&flash| !flash));
    }
}
