//! Rendering for LogView

use ratatui::{
    Frame,
    layout::{Constraint, Layout, Rect},
    style::{Color, Modifier, Style, Stylize},
    text::{Line, Span},
    widgets::Paragraph,
};

use crate::jj::constants;
use crate::model::{Change, Notification};
use crate::ui::{components, symbols, theme};

use super::{InputMode, LogView, RebaseMode, RebaseSource, empty_text};

impl LogView {
    /// Render the view with optional notification in title bar
    pub fn render(&mut self, frame: &mut Frame, area: Rect, notification: Option<&Notification>) {
        // Split area for input bar if in input modes
        let (log_area, input_area) = match self.input_mode {
            InputMode::Normal
            | InputMode::RebaseModeSelect
            | InputMode::RebaseSelect
            | InputMode::SquashSelect
            | InputMode::CompareSelect
            | InputMode::InterdiffSelect
            | InputMode::BisectSelect
            | InputMode::ParallelizeSelect => (area, None),
            InputMode::SearchInput
            | InputMode::RevsetInput
            | InputMode::DescribeInput
            | InputMode::BookmarkInput
            | InputMode::RebaseRevsetInput => {
                let chunks =
                    Layout::vertical([Constraint::Min(1), Constraint::Length(3)]).split(area);
                (chunks[0], Some(chunks[1]))
            }
        };

        self.render_log_list(frame, log_area, notification);

        // Render input bar if in input mode
        if let Some(input_area) = input_area {
            self.render_input_bar(frame, input_area);
        }
    }

    fn render_log_list(&self, frame: &mut Frame, area: Rect, notification: Option<&Notification>) {
        let title = self.build_title();

        // Build notification line for title bar (with truncation if needed)
        let title_width = title.width();
        let available_for_notif = area.width.saturating_sub(title_width as u16 + 4) as usize; // +4 for borders/padding
        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 block = components::bordered_block_with_notification(title, notif_line);

        if self.changes.is_empty() {
            self.render_empty_state(frame, area, block);
            return;
        }
        // AI filter on but nothing matches → AI-specific empty state (A2)
        if self.ai_filter_empty() {
            self.render_ai_empty_state(frame, area, block);
            return;
        }

        let inner_height = area.height.saturating_sub(2) as usize; // borders
        if inner_height == 0 {
            return;
        }

        // Scroll over VISIBLE rows (position within visible_indices), so a
        // sparse AI-filtered view scrolls naturally (A2).
        let visible = self.visible_indices();
        let scroll_pos = self.calculate_scroll_offset(inner_height);

        // Build lines from the visible rows only
        let mut lines: Vec<Line> = Vec::new();
        for &idx in visible.iter().skip(scroll_pos) {
            if lines.len() >= inner_height {
                break;
            }
            let change = &self.changes[idx];
            let is_selected = idx == self.selected_index && !change.is_graph_only;
            lines.push(self.build_change_line(change, is_selected));
        }

        let paragraph = Paragraph::new(lines).block(block);

        frame.render_widget(paragraph, area);
    }

    fn build_title(&self) -> Line<'static> {
        // Special title for RebaseModeSelect mode
        if self.input_mode == InputMode::RebaseModeSelect {
            return Line::from(" Tij - Log View [Rebase: Select mode (r/s/b/A/B)] ")
                .bold()
                .yellow()
                .centered();
        }

        // Special title for RebaseRevsetInput mode
        if self.input_mode == InputMode::RebaseRevsetInput {
            let mode_label = match self.rebase_mode {
                RebaseMode::Revision => "-r",
                RebaseMode::Source => "-s",
                RebaseMode::Branch => "-b",
                _ => "",
            };
            return Line::from(format!(
                " Tij - Log View [Rebase {}: Enter revset] ",
                mode_label
            ))
            .bold()
            .yellow()
            .centered();
        }

        // Special title for RebaseSelect mode (varies by rebase_mode)
        if self.input_mode == InputMode::RebaseSelect {
            // When revset is active, show the revset string in the title
            if matches!(self.rebase_source, Some(RebaseSource::Revset(_))) {
                let revset_src = match &self.rebase_source {
                    Some(RebaseSource::Revset(s)) => s.as_str(),
                    _ => "?",
                };
                let mode_label = match self.rebase_mode {
                    RebaseMode::Revision => "-r",
                    RebaseMode::Source => "-s",
                    RebaseMode::Branch => "-b",
                    _ => "",
                };
                return Line::from(format!(
                    " Tij - Log View [Rebase {} \"{}\": Select destination] ",
                    mode_label, revset_src
                ))
                .bold()
                .yellow()
                .centered();
            }

            let title = match self.rebase_mode {
                RebaseMode::Revision => " Tij - Log View [Rebase: Select destination] ".to_string(),
                RebaseMode::Source => {
                    " Tij - Log View [Rebase -s: Select destination (with descendants)] "
                        .to_string()
                }
                RebaseMode::Branch => {
                    " Tij - Log View [Rebase -b: Select destination (branch)] ".to_string()
                }
                RebaseMode::InsertAfter => {
                    " Tij - Log View [Rebase: Select insert-after target] ".to_string()
                }
                RebaseMode::InsertBefore => {
                    " Tij - Log View [Rebase: Select insert-before target] ".to_string()
                }
            };
            return Line::from(title).bold().yellow().centered();
        }

        // Special title for SquashSelect mode
        if self.input_mode == InputMode::SquashSelect {
            return Line::from(" Tij - Log View [Squash: Select destination] ")
                .bold()
                .yellow()
                .centered();
        }

        // Special title for CompareSelect mode
        if self.input_mode == InputMode::CompareSelect {
            let from_id = self
                .compare_from
                .as_ref()
                .map(|(cid, _)| cid.as_str())
                .unwrap_or("?");
            return Line::from(format!(
                " Tij - Log View [Compare: From={}, Select To] ",
                from_id
            ))
            .bold()
            .yellow()
            .centered();
        }

        // Special title for InterdiffSelect mode
        if self.input_mode == InputMode::InterdiffSelect {
            let from_id = self
                .interdiff_from
                .as_ref()
                .map(|(cid, _)| cid.as_str())
                .unwrap_or("?");
            return Line::from(format!(
                " Tij - Log View [Interdiff: From={}, Select To] ",
                from_id
            ))
            .bold()
            .yellow()
            .centered();
        }

        // Special title for BisectSelect mode
        if self.input_mode == InputMode::BisectSelect {
            let bad_id = self
                .bisect_bad
                .as_ref()
                .map(|(_, short)| short.as_str())
                .unwrap_or("?");
            return Line::from(format!(
                " Tij - Log View [Bisect: Bad={}, Select Good] ",
                bad_id
            ))
            .bold()
            .yellow()
            .centered();
        }

        // Special title for ParallelizeSelect mode
        if self.input_mode == InputMode::ParallelizeSelect {
            let from_id = self
                .parallelize_from
                .as_ref()
                .map(|(cid, _)| cid.as_str())
                .unwrap_or("?");
            return Line::from(format!(
                " Tij - Log View [Parallelize: From={}, Select end] ",
                from_id
            ))
            .bold()
            .yellow()
            .centered();
        }

        // Build count suffix for revset queries and truncated default view
        let count_suffix = if self.current_revset.is_some() {
            let count = self.changes.iter().filter(|c| !c.is_graph_only).count();
            if self.truncated {
                format!(" ({}+)", count)
            } else {
                format!(" ({})", count)
            }
        } else if self.truncated {
            let count = self.changes.iter().filter(|c| !c.is_graph_only).count();
            format!(" ({}+)", count)
        } else {
            String::new()
        };

        // AI filter marker (A2). Separate chunk after revset/truncation so it
        // reads as "filter applied to the loaded set", with the AI-visible
        // count (not the loaded total).
        let ai_suffix = if self.ai_filter {
            format!(" [AI] ({})", self.visible_change_count())
        } else {
            String::new()
        };

        let title_text = match (&self.current_revset, &self.last_search_query) {
            (Some(revset), Some(query)) => {
                format!(
                    " Tij - Log View [{}{}]{} [Search: {}] ",
                    revset, count_suffix, ai_suffix, query
                )
            }
            (Some(revset), None) => {
                format!(" Tij - Log View [{}{}]{} ", revset, count_suffix, ai_suffix)
            }
            (None, Some(query)) => {
                format!(
                    " Tij - Log View{}{} [Search: {}] ",
                    count_suffix, ai_suffix, query
                )
            }
            (None, None) => {
                if count_suffix.is_empty() && ai_suffix.is_empty() {
                    " Tij - Log View ".to_string()
                } else {
                    format!(" Tij - Log View{}{} ", count_suffix, ai_suffix)
                }
            }
        };
        Line::from(title_text).bold().cyan().centered()
    }

    fn render_empty_state(
        &self,
        frame: &mut Frame,
        area: Rect,
        block: ratatui::widgets::Block<'static>,
    ) {
        let paragraph =
            components::empty_state(empty_text::TITLE, Some(empty_text::HINT)).block(block);

        frame.render_widget(paragraph, area);
    }

    /// Empty state shown when the AI filter is on but nothing matches (A2).
    /// The filter stays on; the hint tells the user how to clear it.
    fn render_ai_empty_state(
        &self,
        frame: &mut Frame,
        area: Rect,
        block: ratatui::widgets::Block<'static>,
    ) {
        let paragraph = components::empty_state(
            "No AI-attributed changes",
            Some("Filter on — ':' filter-ai to clear, or 'r' to change revset"),
        )
        .block(block);
        frame.render_widget(paragraph, area);
    }

    /// Scroll offset as a POSITION within `visible_indices` (the first visible
    /// row to draw). Uses the selected row's visible position so a sparse
    /// AI-filtered list scrolls correctly (A2). Falls back to position 0 when
    /// the selection is not currently visible.
    fn calculate_scroll_offset(&self, viewport_rows: usize) -> usize {
        if viewport_rows == 0 {
            return 0;
        }
        let visible = self.visible_indices();
        // Position of the selected absolute index within the visible rows.
        let sel_pos = visible
            .iter()
            .position(|&i| i == self.selected_index)
            .unwrap_or(0);

        let mut offset = self.scroll_offset.min(visible.len().saturating_sub(1));
        if sel_pos < offset {
            offset = sel_pos;
        } else if sel_pos >= offset + viewport_rows {
            offset = sel_pos - viewport_rows + 1;
        }
        offset
    }

    fn build_change_line(&self, change: &Change, is_selected: bool) -> Line<'static> {
        let mut spans = Vec::new();

        // Graph prefix (from jj output)
        if !change.graph_prefix.is_empty() {
            spans.push(Span::styled(
                change.graph_prefix.clone(),
                Style::default().fg(theme::log_view::GRAPH_LINE),
            ));
        }

        // For graph-only lines, just return the prefix
        if change.is_graph_only {
            return Line::from(spans);
        }

        // Change ID
        spans.push(Span::styled(
            format!("{} ", change.short_id()),
            Style::default().fg(theme::log_view::CHANGE_ID),
        ));

        // Author (if not root)
        if change.change_id != constants::ROOT_CHANGE_ID {
            spans.push(Span::raw(format!("{} ", change.author)));
            spans.push(Span::styled(
                format!("{} ", change.timestamp),
                Style::default().fg(theme::log_view::TIMESTAMP),
            ));
        }

        // Bookmarks
        if !change.bookmarks.is_empty() {
            spans.push(Span::styled(
                format!("{} ", change.bookmarks.join(", ")),
                Style::default().fg(theme::log_view::BOOKMARK),
            ));
        }

        // Workspace markers (other workspaces' working copies)
        // Skip if this is the current WC and only 1 workspace name (already shown as @ in graph)
        let show_ws_markers = !(change.working_copy_names.is_empty()
            || change.is_working_copy && change.working_copy_names.len() == 1);
        if show_ws_markers {
            let marker = change
                .working_copy_names
                .iter()
                .map(|name| format!("{}@", name))
                .collect::<Vec<_>>()
                .join(" ");
            spans.push(Span::styled(
                format!("{} ", marker),
                Style::default().fg(Color::Magenta),
            ));
        }

        // Conflict indicator
        if change.has_conflict {
            spans.push(Span::styled(
                "[CONFLICT] ",
                Style::default().fg(Color::Red).add_modifier(Modifier::BOLD),
            ));
        }

        // Agent Trace AI badge (before description; §7.1 of agent-trace SoW)
        // [AI] = confirmed (vcs.type "jj"), [AI?] = heuristic (git SHA — may
        // be anchored one change off, see SoW §6.3)
        let commit_key = change.commit_id.as_str();
        if self.ai_badges.confirmed.contains(commit_key) {
            spans.push(Span::styled(
                "[AI] ",
                Style::default()
                    .fg(theme::log_view::AI_BADGE)
                    .add_modifier(Modifier::BOLD),
            ));
        } else if self.ai_badges.heuristic.contains(commit_key) {
            spans.push(Span::styled(
                "[AI?] ",
                Style::default().fg(theme::log_view::AI_BADGE),
            ));
        }

        // Description
        let description = change.display_description();
        if change.is_empty && description == symbols::empty::NO_DESCRIPTION {
            spans.push(Span::styled(
                format!("{} ", symbols::empty::CHANGE_LABEL),
                Style::default().fg(theme::log_view::EMPTY_LABEL),
            ));
        }
        spans.push(Span::raw(description.to_string()));

        let mut line = Line::from(spans);

        // Check if this is the rebase source (in RebaseModeSelect, RebaseSelect, or RebaseRevsetInput mode)
        let is_rebase_source = matches!(
            self.input_mode,
            InputMode::RebaseModeSelect | InputMode::RebaseSelect | InputMode::RebaseRevsetInput
        ) && matches!(
            &self.rebase_source,
            Some(RebaseSource::Selected { change_id, .. }) if *change_id == change.change_id
        );

        // Check if this is the squash source (in SquashSelect mode)
        let is_squash_source = self.input_mode == InputMode::SquashSelect
            && self
                .squash_source
                .as_ref()
                .is_some_and(|(cid, _)| *cid == change.change_id);

        // Check if this is the compare "from" (in CompareSelect mode)
        let is_compare_from = self.input_mode == InputMode::CompareSelect
            && self
                .compare_from
                .as_ref()
                .is_some_and(|(cid, _)| *cid == change.change_id);

        // Check if this is the interdiff "from" (in InterdiffSelect mode)
        let is_interdiff_from = self.input_mode == InputMode::InterdiffSelect
            && self
                .interdiff_from
                .as_ref()
                .is_some_and(|(cid, _)| *cid == change.change_id);

        // Check if this is the bisect "bad" (in BisectSelect mode)
        let is_bisect_bad = self.input_mode == InputMode::BisectSelect
            && self
                .bisect_bad
                .as_ref()
                .is_some_and(|(cid, _)| *cid == change.change_id);

        // Check if this is the parallelize "from" (in ParallelizeSelect mode)
        let is_parallelize_from = self.input_mode == InputMode::ParallelizeSelect
            && self
                .parallelize_from
                .as_ref()
                .is_some_and(|(cid, _)| *cid == change.change_id);

        // Apply styling
        if is_rebase_source
            || is_squash_source
            || is_compare_from
            || is_interdiff_from
            || is_bisect_bad
            || is_parallelize_from
        {
            // Highlight rebase/squash source with distinct background
            line = line.style(
                Style::default()
                    .bg(Color::DarkGray)
                    .fg(Color::Yellow)
                    .add_modifier(Modifier::BOLD),
            );
        } else if is_selected {
            line = line.style(
                Style::default()
                    .fg(theme::selection::FG)
                    .bg(theme::selection::BG)
                    .add_modifier(Modifier::BOLD),
            );
        }

        line
    }

    fn render_input_bar(&self, frame: &mut Frame, area: Rect) {
        let Some((prompt, title)) = self.input_mode.input_bar_meta() else {
            return;
        };

        let input_text = format!("{}{}", prompt, self.input_buffer);

        // Calculate available width (area width minus borders)
        let available_width = area.width.saturating_sub(2) as usize;

        // Early return if no space for input
        if available_width == 0 {
            return;
        }

        // Truncate display text if too long (show end of input, UTF-8 safe)
        let char_count = input_text.chars().count();
        let display_text = if char_count > available_width {
            let skip = char_count.saturating_sub(available_width.saturating_sub(1)); // -1 for ellipsis
            format!("…{}", input_text.chars().skip(skip).collect::<String>())
        } else {
            input_text.clone()
        };

        let paragraph =
            Paragraph::new(display_text).block(components::bordered_block(Line::from(title)));

        frame.render_widget(paragraph, area);

        // Show cursor (clamped to available width, character-based)
        let cursor_pos = char_count.min(available_width);
        frame.set_cursor_position((area.x + cursor_pos as u16 + 1, area.y + 1));
    }
}

#[cfg(test)]
mod tests {
    use super::LogView;
    use crate::jj::constants;
    use crate::model::{Change, ChangeId, CommitId};

    fn create_selectable_changes(count: usize) -> Vec<Change> {
        (0..count)
            .map(|i| Change {
                change_id: ChangeId::new(format!("chg{i:05}")),
                commit_id: CommitId::new(format!("commit{i:05}")),
                author: "user@example.com".to_string(),
                timestamp: "2024-01-29".to_string(),
                description: format!("Commit {i}"),
                is_working_copy: i == 0,
                is_empty: false,
                bookmarks: vec![],
                graph_prefix: if i == 0 {
                    "@  ".to_string()
                } else {
                    "○  ".to_string()
                },
                is_graph_only: false,
                has_conflict: false,
                working_copy_names: Vec::new(),
            })
            .collect()
    }

    fn title_text(view: &LogView) -> String {
        let mut text = String::new();
        for span in view.build_title().spans {
            text.push_str(span.content.as_ref());
        }
        text
    }

    fn line_text(view: &LogView, change: &Change) -> String {
        let mut text = String::new();
        for span in view.build_change_line(change, false).spans {
            text.push_str(span.content.as_ref());
        }
        text
    }

    // ── Agent Trace AI badges (agent-trace SoW §7.1) ──

    #[test]
    fn ai_badge_confirmed_renders_before_description() {
        let mut view = LogView::new();
        let changes = create_selectable_changes(2);
        let mut badges = crate::trace::AiBadgeSets::default();
        badges.confirmed.insert("commit00000".to_string());
        view.set_changes(changes.clone());
        view.set_ai_badges(badges);

        let text = line_text(&view, &changes[0]);
        assert!(text.contains("[AI] Commit 0"), "got: {text}");
        // Other rows are unaffected
        let text1 = line_text(&view, &changes[1]);
        assert!(!text1.contains("[AI]"), "got: {text1}");
    }

    #[test]
    fn ai_badge_heuristic_renders_with_question_mark() {
        let mut view = LogView::new();
        let changes = create_selectable_changes(1);
        let mut badges = crate::trace::AiBadgeSets::default();
        badges.heuristic.insert("commit00000".to_string());
        view.set_changes(changes.clone());
        view.set_ai_badges(badges);

        let text = line_text(&view, &changes[0]);
        assert!(text.contains("[AI?] Commit 0"), "got: {text}");
    }

    #[test]
    fn no_badges_means_unchanged_line() {
        let mut view = LogView::new();
        let changes = create_selectable_changes(1);
        view.set_changes(changes.clone());

        let text = line_text(&view, &changes[0]);
        assert!(!text.contains("[AI"), "got: {text}");
    }

    #[test]
    fn confirmed_takes_precedence_over_heuristic() {
        let mut view = LogView::new();
        let changes = create_selectable_changes(1);
        let mut badges = crate::trace::AiBadgeSets::default();
        badges.confirmed.insert("commit00000".to_string());
        badges.heuristic.insert("commit00000".to_string());
        view.set_changes(changes.clone());
        view.set_ai_badges(badges);

        let text = line_text(&view, &changes[0]);
        assert!(text.contains("[AI] "), "got: {text}");
        assert!(!text.contains("[AI?]"), "got: {text}");
    }

    #[test]
    fn test_build_title_includes_revset_count() {
        let mut view = LogView::new();
        view.current_revset = Some("ancestors(@, 5)".to_string());
        view.set_changes(create_selectable_changes(5));

        assert_eq!(title_text(&view), " Tij - Log View [ancestors(@, 5) (5)] ");
    }

    #[test]
    fn test_build_title_ai_filter_marker_and_count() {
        let mut view = LogView::new();
        view.set_changes(create_selectable_changes(5));
        // badge 2 of them, then filter on
        let mut b = crate::trace::AiBadgeSets::default();
        b.confirmed.insert("commit00000".to_string());
        b.confirmed.insert("commit00002".to_string());
        view.set_ai_badges(b);
        view.toggle_ai_filter();

        assert_eq!(title_text(&view), " Tij - Log View [AI] (2) ");
    }

    #[test]
    fn test_build_title_ai_filter_with_truncated_revset() {
        let mut view = LogView::new();
        view.current_revset = Some("all()".to_string());
        let limit = constants::DEFAULT_LOG_LIMIT.parse::<usize>().unwrap();
        view.set_changes(create_selectable_changes(limit));
        view.truncated = true;
        let mut b = crate::trace::AiBadgeSets::default();
        b.confirmed.insert("commit00000".to_string());
        view.set_ai_badges(b);
        view.toggle_ai_filter();

        // truncation (N+) on the loaded set, then the AI-visible count
        assert_eq!(
            title_text(&view),
            format!(" Tij - Log View [all() ({}+)] [AI] (1) ", limit)
        );
    }

    #[test]
    fn test_build_title_includes_truncated_indicator_for_revset() {
        let mut view = LogView::new();
        view.current_revset = Some("all()".to_string());
        let limit = constants::DEFAULT_LOG_LIMIT.parse::<usize>().unwrap();
        view.set_changes(create_selectable_changes(limit));
        view.truncated = true;

        assert_eq!(
            title_text(&view),
            format!(" Tij - Log View [all() ({}+)] ", limit)
        );
    }

    #[test]
    fn test_build_title_includes_truncated_indicator_without_revset() {
        let mut view = LogView::new();
        let limit = constants::DEFAULT_LOG_LIMIT.parse::<usize>().unwrap();
        view.set_changes(create_selectable_changes(limit));
        view.truncated = true;

        assert_eq!(title_text(&view), format!(" Tij - Log View ({}+) ", limit));
    }
}
