//! Rendering for DiffView

use ratatui::{
    prelude::*,
    style::Stylize,
    text::{Line, Span},
    widgets::Paragraph,
};

use crate::model::{
    CompareInfo, DiffDisplayFormat, DiffLine, DiffLineKind, DiffMode, Notification,
};
use crate::ui::{components, theme};

use super::DiffView;

impl DiffView {
    /// Render the diff view (without status bar - rendered by App)
    pub fn render(&self, frame: &mut Frame, area: Rect, notification: Option<&Notification>) {
        if self.compare_info.is_some() {
            self.render_compare(frame, area, notification);
        } else {
            self.render_normal(frame, area, notification);
        }
    }

    /// Render normal single-revision diff.
    ///
    /// Layout: header (dynamic via `header_height`) + context bar (1) + diff (rest).
    /// Two modes for the header:
    ///   - collapsed (default): cap so diff keeps at least 8 rows and header
    ///     never exceeds half the viewport. Overflowing description rows are
    ///     shown as a "(+N more)" hint by `render_header`.
    ///   - expanded (toggled with `t`): grow header to fit full description,
    ///     reserving only context bar + a small diff floor so users can read
    ///     the entire message in-place.
    fn render_normal(&self, frame: &mut Frame, area: Rect, notification: Option<&Notification>) {
        let header_height = self.header_height(area.height);
        let chunks = Layout::vertical([
            Constraint::Length(header_height),
            Constraint::Length(1),
            Constraint::Min(1),
        ])
        .split(area);

        self.render_header(frame, chunks[0], notification);
        self.render_context_bar(frame, chunks[1]);
        self.render_diff_content(frame, chunks[2]);
    }

    /// Render compare (two-revision) diff
    fn render_compare(&self, frame: &mut Frame, area: Rect, notification: Option<&Notification>) {
        let compare_info = self.compare_info.as_ref().unwrap();

        let header_height = self.header_height(area.height);
        let chunks = Layout::vertical([
            Constraint::Length(header_height),
            Constraint::Length(1),
            Constraint::Min(1),
        ])
        .split(area);

        self.render_compare_header(frame, chunks[0], compare_info, notification);
        self.render_context_bar(frame, chunks[1]);
        self.render_diff_content(frame, chunks[2]);
    }

    /// Render the header (commit info including description)
    fn render_header(&self, frame: &mut Frame, area: Rect, notification: Option<&Notification>) {
        let format_suffix = match self.display_format {
            DiffDisplayFormat::ColorWords => String::new(),
            fmt => format!(" [{}]", fmt.label()),
        };
        // Stack mode: `revision` holds a revset string, so show a change count
        // instead of a (meaningless) truncated revset prefix.
        let (title_label, title_id) = if self.mode == DiffMode::Stack {
            (
                " Tij - Stack Diff ",
                format!("{} changes", self.change_count()),
            )
        } else {
            (
                " Tij - Diff View ",
                self.revision.chars().take(8).collect::<String>(),
            )
        };
        let mut title_spans = vec![
            Span::raw(title_label).bold(),
            Span::raw("["),
            Span::styled(title_id, Style::default().fg(theme::log_view::CHANGE_ID)),
            Span::raw("]"),
            Span::styled(format_suffix, Style::default().fg(Color::Yellow).bold()),
        ];
        if self.description_expanded {
            title_spans.push(Span::styled(
                " [full desc]",
                Style::default().fg(Color::Cyan).bold(),
            ));
        }
        title_spans.push(Span::raw(" "));
        let title = Line::from(title_spans).centered();

        // Build notification line for title bar (right-aligned)
        let title_width = title.width();
        let available_for_notif = area.width.saturating_sub(title_width as u16 + 4) as usize;
        let notif_line = notification
            .filter(|n| !n.is_expired())
            .map(|n| components::build_notification_title(n, Some(available_for_notif)))
            .filter(|line| !line.spans.is_empty());

        let mut header_text = vec![
            Line::from(vec![
                Span::raw("Commit: "),
                Span::styled(
                    self.content.commit_id.chars().take(40).collect::<String>(),
                    Style::default().fg(theme::log_view::CHANGE_ID),
                ),
            ]),
            Line::from(vec![
                Span::raw("Author: "),
                Span::raw(&self.content.author),
                Span::raw("  "),
                Span::styled(
                    &self.content.timestamp,
                    Style::default().fg(Color::DarkGray),
                ),
            ]),
        ];

        // Show description, truncating with a hint if it would not fit in the
        // header height we were given (Paragraph silently clips overflowing
        // lines, which would otherwise hide the fact that the message was cut).
        // area.height = top border (1) + commit (1) + author (1) + description rows.
        let desc_rows_available = (area.height as usize).saturating_sub(3);
        if self.content.description.is_empty() {
            header_text.push(Line::from(vec![Span::styled(
                "(no description)",
                Style::default().fg(Color::DarkGray).italic(),
            )]));
        } else {
            let lines: Vec<&str> = self.content.description.lines().collect();
            let total = lines.len();
            let truncated = total > desc_rows_available;
            let take_n = if truncated {
                desc_rows_available.saturating_sub(1) // leave a row for the hint
            } else {
                total
            };
            for line in lines.iter().take(take_n) {
                header_text.push(Line::from(vec![Span::styled(
                    line.to_string(),
                    Style::default().fg(Color::White).bold(),
                )]));
            }
            if truncated {
                let hidden = total - take_n;
                let suffix = if self.description_expanded {
                    // Already expanded but the terminal is too short to fit
                    // everything; pressing 't' would only collapse, so steer
                    // the user toward a different remedy.
                    " — terminal too small; resize or yank with 'y'"
                } else {
                    " — press 't' to expand"
                };
                header_text.push(Line::from(vec![Span::styled(
                    format!(
                        "(+{} more line{} hidden{})",
                        hidden,
                        if hidden == 1 { "" } else { "s" },
                        suffix,
                    ),
                    Style::default().fg(Color::DarkGray).italic(),
                )]));
            }
        }

        // Use header_block with notification on right
        let block = if let Some(notif) = notif_line {
            components::header_block(title).title(notif.right_aligned())
        } else {
            components::header_block(title)
        };

        let header = Paragraph::new(header_text).block(block);

        frame.render_widget(header, area);
    }

    /// Render compare header with From/To revision info
    fn render_compare_header(
        &self,
        frame: &mut Frame,
        area: Rect,
        compare_info: &CompareInfo,
        notification: Option<&Notification>,
    ) {
        let format_suffix = match self.display_format {
            DiffDisplayFormat::ColorWords => String::new(),
            fmt => format!(" [{}]", fmt.label()),
        };
        let mode_label = match self.mode {
            DiffMode::Interdiff => "Interdiff",
            _ => "Compare Diff",
        };
        let title_text = format!(" Tij - {}{} ", mode_label, format_suffix);
        let title = Line::from(title_text).bold().cyan().centered();

        // Build notification for title bar
        let title_width = title.width();
        let available_for_notif = area.width.saturating_sub(title_width as u16 + 4) as usize;
        let notif_line = notification
            .filter(|n| !n.is_expired())
            .map(|n| components::build_notification_title(n, Some(available_for_notif)))
            .filter(|line| !line.spans.is_empty());

        // Build from/to lines
        let from = &compare_info.from;
        let to = &compare_info.to;

        let from_bookmarks = if from.bookmarks.is_empty() {
            String::new()
        } else {
            format!(" ({})", from.bookmarks.join(", "))
        };
        let to_bookmarks = if to.bookmarks.is_empty() {
            String::new()
        } else {
            format!(" ({})", to.bookmarks.join(", "))
        };

        let from_desc = if from.description.is_empty() {
            "(no description)".to_string()
        } else {
            from.description.clone()
        };
        let to_desc = if to.description.is_empty() {
            "(no description)".to_string()
        } else {
            to.description.clone()
        };

        let (from_label, to_label) = match self.mode {
            DiffMode::Interdiff => ("Interdiff From: ", "Interdiff To:   "),
            _ => ("From: ", "To:   "),
        };

        let header_text = vec![
            Line::from(vec![
                Span::styled(from_label, Style::default().fg(Color::Red).bold()),
                Span::styled(
                    from.change_id.to_string(),
                    Style::default().fg(theme::log_view::CHANGE_ID),
                ),
                Span::styled(from_bookmarks, Style::default().fg(Color::Magenta)),
                Span::raw(format!(" {} ", from.author)),
                Span::raw(from_desc),
            ]),
            Line::from(vec![
                Span::styled(to_label, Style::default().fg(Color::Green).bold()),
                Span::styled(
                    to.change_id.to_string(),
                    Style::default().fg(theme::log_view::CHANGE_ID),
                ),
                Span::styled(to_bookmarks, Style::default().fg(Color::Magenta)),
                Span::raw(format!(" {} ", to.author)),
                Span::raw(to_desc),
            ]),
            Line::from(vec![Span::styled(
                format!("{} file(s) changed", self.file_count()),
                Style::default().fg(Color::DarkGray),
            )]),
        ];

        let block = if let Some(notif) = notif_line {
            components::header_block(title).title(notif.right_aligned())
        } else {
            components::header_block(title)
        };

        let header = Paragraph::new(header_text).block(block);
        frame.render_widget(header, area);
    }

    /// Render the context bar (current file name + progress)
    fn render_context_bar(&self, frame: &mut Frame, area: Rect) {
        let file_info = if self.file_count() > 0 {
            let file_name = self.current_file_name().unwrap_or("(unknown)");
            format!(
                " {} [{}/{}]",
                file_name,
                self.current_file_index + 1,
                self.file_count()
            )
        } else {
            " (no files)".to_string()
        };

        let bar = Paragraph::new(Line::from(vec![Span::styled(
            file_info,
            Style::default().fg(Color::Cyan).bold(),
        )]))
        .block(components::side_borders_block());

        frame.render_widget(bar, area);
    }

    /// Render the diff content (scrollable)
    fn render_diff_content(&self, frame: &mut Frame, area: Rect) {
        // Bottom border closes the frame (1 row reserved), so the visible
        // content is one row shorter than the area. Must match
        // `diff_content_height`, which the scroll math uses.
        let inner_height = (area.height as usize).saturating_sub(1);

        if self.content.lines.is_empty() {
            // Empty state
            let empty_msg =
                components::no_changes_state().block(components::side_bottom_borders_block());
            frame.render_widget(empty_msg, area);
            return;
        }

        // Build visible lines. With an AI overlay (Phase 3), every line gets
        // a 2-char gutter — "▎ " (AI hint) or "  " — so columns stay aligned.
        let has_overlay = self.has_ai_overlay();
        let lines: Vec<Line> = self
            .content
            .lines
            .iter()
            .enumerate()
            .skip(self.scroll_offset)
            .take(inner_height)
            .map(|(i, diff_line)| {
                let mut line = self.render_diff_line(diff_line);
                if has_overlay {
                    let marked = self.ai_line_marks.get(i).copied().unwrap_or(false);
                    let gutter = if marked {
                        Span::styled("▎ ", Style::default().fg(theme::diff_view::AI_MARK).bold())
                    } else {
                        Span::raw("  ")
                    };
                    line.spans.insert(0, gutter);
                }
                line
            })
            .collect();

        let diff = Paragraph::new(lines).block(components::side_bottom_borders_block());

        frame.render_widget(diff, area);
    }

    /// Render a single diff line
    fn render_diff_line(&self, line: &DiffLine) -> Line<'static> {
        let show_line_nums = self.display_format == DiffDisplayFormat::ColorWords;

        match line.kind {
            DiffLineKind::FileHeader => Line::from(Span::styled(
                format!("── {} ──", line.content),
                Style::default().fg(theme::diff_view::FILE_HEADER).bold(),
            )),
            DiffLineKind::ChangeHeader => Line::from(Span::styled(
                line.content.clone(),
                Style::default().fg(theme::diff_view::CHANGE_HEADER).bold(),
            )),
            DiffLineKind::Separator => Line::from(""),
            DiffLineKind::Context => {
                if show_line_nums {
                    let line_nums = self.format_line_numbers(line.line_numbers);
                    Line::from(vec![
                        Span::styled(
                            line_nums,
                            Style::default().fg(theme::diff_view::LINE_NUMBER),
                        ),
                        Span::raw("  "),
                        Span::raw(line.content.clone()),
                    ])
                } else {
                    Line::from(Span::raw(format!(" {}", line.content)))
                }
            }
            DiffLineKind::Added => {
                if show_line_nums {
                    let line_nums = self.format_line_numbers(line.line_numbers);
                    Line::from(vec![
                        Span::styled(
                            line_nums,
                            Style::default().fg(theme::diff_view::LINE_NUMBER),
                        ),
                        Span::styled(" +", Style::default().fg(theme::diff_view::ADDED)),
                        Span::styled(
                            line.content.clone(),
                            Style::default().fg(theme::diff_view::ADDED),
                        ),
                    ])
                } else {
                    Line::from(Span::styled(
                        format!(" +{}", line.content),
                        Style::default().fg(theme::diff_view::ADDED),
                    ))
                }
            }
            DiffLineKind::Deleted => {
                if show_line_nums {
                    let line_nums = self.format_line_numbers(line.line_numbers);
                    Line::from(vec![
                        Span::styled(
                            line_nums,
                            Style::default().fg(theme::diff_view::LINE_NUMBER),
                        ),
                        Span::styled(" -", Style::default().fg(theme::diff_view::DELETED)),
                        Span::styled(
                            line.content.clone(),
                            Style::default().fg(theme::diff_view::DELETED),
                        ),
                    ])
                } else {
                    Line::from(Span::styled(
                        format!(" -{}", line.content),
                        Style::default().fg(theme::diff_view::DELETED),
                    ))
                }
            }
        }
    }

    /// Format line numbers for display
    fn format_line_numbers(&self, line_nums: Option<(Option<usize>, Option<usize>)>) -> String {
        match line_nums {
            Some((old, new)) => {
                let old_str = old
                    .map(|n| format!("{:4}", n))
                    .unwrap_or_else(|| "    ".to_string());
                let new_str = new
                    .map(|n| format!("{:4}", n))
                    .unwrap_or_else(|| "    ".to_string());
                format!("{} {}", old_str, new_str)
            }
            None => "         ".to_string(),
        }
    }
}
