//! Git History Editor — interactive commit history editing.
//!
//! Allows the user to reword, move, squash, or drop commits via keyboard
//! actions.  Each confirmed operation runs `git rebase -i` immediately as a
//! background task; undo reverses the change with `git reset --hard`.
//!
//! Layout (list mode):
//! ```text
//! ─ Git History Editor ──────────────────────────── [ undo: 2 ]
//!   abc1234  Fix parser crash             you  2h ago
//! ▶ def5678  Add tests                   you  1d ago
//!   ghi9012  Refactor layout             you  3d ago
//! ─────────────────────────────────────────────────────────────
//!   r:reword  u/d:move  s:squash  del:drop  ^z:undo
//! ```
//!
//! Layout (editor mode — reword / squash):
//! ```text
//!   abc1234  Fix parser crash             you  2h ago
//! ▶ def5678  [REWORDING]                 you  1d ago
//!   ...
//! ─ Commit Message ─────────────────────────────────────────────
//!   │ Fix parser crash in tokenizer
//!   │ _
//! ─────────────────────────────────────────────────────────────
//!   [ctrl+s: apply]  [Esc: cancel]
//! ```
//!
//! Key bindings:
//!   Up / Down / PgUp / PgDn — navigate commit list
//!   `r`                     — reword selected commit message
//!   `u`                     — move commit toward older (up in visual order)
//!   `d`                     — move commit toward newer (down in visual order)
//!   Enter                   — confirm pending move (trigger git rebase)
//!   `e`                     — edit commit content (open CommitWindow)
//!   `s`                     — squash with the next-older commit
//!   Del                     — drop commit (second press confirms if not merged)
//!   ctrl+z                  — undo last operation (git reset --hard)
//!   Esc                     — cancel pending move / close view
//!
//! In editor mode (reword / squash):
//!   printable + Backspace    — edit message buffer
//!   Enter                    — insert newline in message
//!   Arrow keys               — move cursor within buffer
//!   ctrl+s                   — apply edited message (trigger git rebase)
//!   Esc                      — cancel editor, return to list mode

use std::cell::Cell;
use std::path::PathBuf;

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

use crate::editor::buffer::Buffer;
use crate::input::{Key, KeyEvent, Modifiers};
use crate::operation::{Event, HistoryEditorOp, Operation};
use crate::settings::Settings;
use crate::vcs::CommitInfo;
use crate::views::View;

// ---------------------------------------------------------------------------
// Types
// ---------------------------------------------------------------------------

/// Mode the editor is currently in.
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum EditorMode {
    /// Showing the commit list; normal navigation.
    List,
    /// Editing a commit message for a reword operation.
    Reword,
    /// Editing the combined message for a squash operation.
    /// `target_idx` is the index of the *older* commit being squashed into.
    Squash { target_idx: usize },
}

/// A record saved before each operation so it can be undone.
#[derive(Debug, Clone)]
pub struct UndoEntry {
    /// The HEAD sha before the operation ran.
    pub pre_op_sha: String,
    pub description: String,
}

// ---------------------------------------------------------------------------
// GitHistoryEditorView
// ---------------------------------------------------------------------------

#[derive(Debug)]
pub struct GitHistoryEditorView {
    pub repo_path: PathBuf,

    /// Commit list, newest-first (index 0 = HEAD).
    pub commits: Vec<CommitInfo>,

    /// Cursor index into `commits`.
    pub cursor: usize,

    /// Scroll offset (first visible row index).
    scroll: Cell<usize>,

    /// Last rendered visible height (used to drive scroll).
    last_height: Cell<usize>,

    /// Current UI mode.
    pub mode: EditorMode,

    /// Message buffer, active when `mode != List`.
    pub message_buf: Buffer,

    /// Undo stack (oldest entry at index 0).
    pub undo_stack: Vec<UndoEntry>,

    /// Set when `u`/`d` is pressed: the cursor was at this index before the move
    /// started, so we can highlight the "moved" item and cancel with Esc.
    pub pending_move_origin: Option<usize>,

    /// Sha of the commit pending a second `Del` press to confirm drop.
    pub pending_delete_sha: Option<String>,

    /// Non-fatal status message shown in the footer.
    pub status_msg: Option<String>,

    /// True while an async commit-load or git operation is in progress.
    pub loading: bool,
}

impl GitHistoryEditorView {
    pub fn new(repo_path: PathBuf) -> Self {
        Self {
            repo_path,
            commits: Vec::new(),
            cursor: 0,
            scroll: Cell::new(0),
            last_height: Cell::new(0),
            mode: EditorMode::List,
            message_buf: Buffer::from_lines(vec![String::new()], None),
            undo_stack: Vec::new(),
            pending_move_origin: None,
            pending_delete_sha: None,
            status_msg: None,
            loading: true,
        }
    }

    // -----------------------------------------------------------------------
    // Navigation helpers
    // -----------------------------------------------------------------------

    fn move_up(&mut self) {
        if self.cursor > 0 {
            self.cursor -= 1;
            // Scroll up if cursor is above the visible window.
            if self.cursor < self.scroll.get() {
                self.scroll.set(self.cursor);
            }
        }
    }

    fn move_down(&mut self) {
        if !self.commits.is_empty() && self.cursor + 1 < self.commits.len() {
            self.cursor += 1;
            let vh = self.last_height.get();
            if vh > 0 && self.cursor >= self.scroll.get() + vh {
                self.scroll.set(self.cursor + 1 - vh);
            }
        }
    }

    // -----------------------------------------------------------------------
    // Commit-list mutation helpers (in-memory only; git is triggered via ops)
    // -----------------------------------------------------------------------

    /// Swap `commits[cursor]` with `commits[cursor + 1]` (move toward older).
    pub fn swap_toward_older(&mut self) {
        let i = self.cursor;
        if i + 1 < self.commits.len() {
            self.commits.swap(i, i + 1);
            self.cursor = i + 1;
            // Maintain scroll so new cursor position is visible.
            let vh = self.last_height.get();
            if vh > 0 && self.cursor >= self.scroll.get() + vh {
                self.scroll.set(self.cursor + 1 - vh);
            }
        }
    }

    /// Swap `commits[cursor]` with `commits[cursor - 1]` (move toward newer).
    pub fn swap_toward_newer(&mut self) {
        let i = self.cursor;
        if i > 0 {
            self.commits.swap(i, i - 1);
            self.cursor = i - 1;
            if self.cursor < self.scroll.get() {
                self.scroll.set(self.cursor);
            }
        }
    }

    // -----------------------------------------------------------------------
    // Editor-mode helpers
    // -----------------------------------------------------------------------

    /// Enter reword mode: pre-fill the buffer with the full commit message.
    pub fn enter_reword_mode(&mut self) {
        let msg = self
            .commits
            .get(self.cursor)
            .map(|c| {
                if c.message.is_empty() { c.summary.clone() } else { c.message.clone() }
            })
            .unwrap_or_default();
        // Normalize: split into lines, strip trailing blank lines.
        let mut lines: Vec<String> = msg.lines().map(|l| l.to_owned()).collect();
        while lines.last().map(|l: &String| l.trim().is_empty()).unwrap_or(false) {
            lines.pop();
        }
        if lines.is_empty() {
            lines.push(String::new());
        }
        self.message_buf = Buffer::from_lines(lines, None);
        // Move cursor to end of last line.
        if let Some((last_row, last_len)) = self.message_buf.lines().len().checked_sub(1).map(|r| {
            let len = self.message_buf.lines()[r].len();
            (r, len)
        }) {
            self.message_buf.set_cursor(crate::editor::position::Position::new(last_row, last_len));
        }
        self.mode = EditorMode::Reword;
        self.status_msg = None;
    }

    /// Enter squash mode: pre-fill the buffer with combined messages of cursor
    /// commit (newer) and `target_idx` commit (older).
    pub fn enter_squash_mode(&mut self, target_idx: usize) {
        let full_msg = |c: &CommitInfo| {
            if c.message.is_empty() { c.summary.clone() } else { c.message.clone() }
        };
        let newer_msg = self.commits.get(self.cursor).map(full_msg).unwrap_or_default();
        let older_msg = self.commits.get(target_idx).map(full_msg).unwrap_or_default();
        let combined = format!("{}\n\n{}", newer_msg.trim_end(), older_msg.trim_end());
        self.message_buf = Buffer::from_lines(
            combined.lines().map(|l| l.to_owned()).collect(),
            None,
        );
        self.mode = EditorMode::Squash { target_idx };
        self.status_msg = None;
    }

    /// Return the current message buffer content as a single string.
    pub fn current_message(&self) -> String {
        self.message_buf.lines().join("\n")
    }

    pub fn message_is_empty(&self) -> bool {
        self.message_buf.lines().iter().all(|l| l.is_empty())
    }
}

// ---------------------------------------------------------------------------
// View trait
// ---------------------------------------------------------------------------

impl View for GitHistoryEditorView {
    const KIND: crate::views::ViewKind = crate::views::ViewKind::Primary;

    fn save_state(&mut self, _app: &mut crate::app_state::AppState) {}

    fn handle_key(&self, key: KeyEvent) -> Vec<Operation> {
        match &self.mode {
            EditorMode::List => handle_key_list(self, key),
            EditorMode::Reword | EditorMode::Squash { .. } => handle_key_editor(self, key),
        }
    }

    fn handle_operation(&mut self, op: &Operation, _settings: &Settings) -> Option<Event> {
        match op {
            Operation::NavigateUp => {
                if self.mode == EditorMode::List {
                    self.move_up();
                    return Some(Event::applied("history_editor", op.clone()));
                }
                // In editor mode: move cursor in buffer.
                let pos = self.message_buf.offset_up(self.message_buf.cursor());
                self.message_buf.set_cursor(pos);
                Some(Event::applied("history_editor", op.clone()))
            }

            Operation::NavigateDown => {
                if self.mode == EditorMode::List {
                    self.move_down();
                    return Some(Event::applied("history_editor", op.clone()));
                }
                let pos = self.message_buf.offset_down(self.message_buf.cursor());
                self.message_buf.set_cursor(pos);
                Some(Event::applied("history_editor", op.clone()))
            }

            Operation::NavigatePageUp => {
                if self.mode == EditorMode::List {
                    for _ in 0..10 {
                        self.move_up();
                    }
                }
                Some(Event::applied("history_editor", op.clone()))
            }

            Operation::NavigatePageDown => {
                if self.mode == EditorMode::List {
                    for _ in 0..10 {
                        self.move_down();
                    }
                }
                Some(Event::applied("history_editor", op.clone()))
            }

            // Escape / Close in editor mode → cancel back to list mode.
            Operation::Close if self.mode != EditorMode::List => {
                self.mode = EditorMode::List;
                self.message_buf = Buffer::from_lines(vec![String::new()], None);
                self.status_msg = None;
                Some(Event::applied("history_editor", op.clone()))
            }

            Operation::HistoryEditorLocal(he_op) => {
                match he_op {
                    HistoryEditorOp::CommitsLoaded(commits) => {
                        self.commits = commits.clone();
                        // Clamp cursor to valid range after reload.
                        if !self.commits.is_empty() {
                            self.cursor = self.cursor.min(self.commits.len() - 1);
                        } else {
                            self.cursor = 0;
                        }
                        self.loading = false;
                    }

                    HistoryEditorOp::OperationCompleted { commits, pre_sha, description } => {
                        self.commits = commits.clone();
                        self.loading = false;
                        self.status_msg = None;
                        if let Some(sha) = pre_sha
                            && !sha.is_empty() {
                                self.undo_stack.push(UndoEntry {
                                    pre_op_sha: sha.clone(),
                                    description: description.clone(),
                                });
                            }
                        if !self.commits.is_empty() {
                            self.cursor = self.cursor.min(self.commits.len() - 1);
                        }
                    }

                    HistoryEditorOp::OperationFailed(msg) => {
                        self.loading = false;
                        self.status_msg = Some(format!("Error: {msg}"));
                        // Cancel any pending move on failure.
                        if let Some(origin) = self.pending_move_origin.take() {
                            self.cursor = origin;
                        }
                    }

                    // These are all intercepted by app.rs and do not need view-local handling.
                    HistoryEditorOp::MoveUp
                    | HistoryEditorOp::MoveDown
                    | HistoryEditorOp::ConfirmMove
                    | HistoryEditorOp::StartReword
                    | HistoryEditorOp::StartSquash { .. }
                    | HistoryEditorOp::ConfirmEdit
                    | HistoryEditorOp::DeleteCommit
                    | HistoryEditorOp::UndoLast => {}

                    // Buffer editing (handled here in the view).
                    HistoryEditorOp::EditorInsertChar(c) => {
                        self.message_buf.begin_transaction(
                            crate::editor::history::ChangeKind::InsertText,
                        );
                        self.message_buf.insert(&c.to_string());
                        self.message_buf.end_transaction();
                    }
                    HistoryEditorOp::EditorInsertNewline => {
                        self.message_buf.begin_transaction(
                            crate::editor::history::ChangeKind::InsertText,
                        );
                        self.message_buf.insert_newline();
                        self.message_buf.end_transaction();
                    }
                    HistoryEditorOp::EditorBackspace => {
                        self.message_buf.begin_transaction(
                            crate::editor::history::ChangeKind::DeleteText,
                        );
                        self.message_buf.delete_backward();
                        self.message_buf.end_transaction();
                    }
                    HistoryEditorOp::EditorCursorLeft => {
                        let pos = self.message_buf.offset_left(self.message_buf.cursor());
                        self.message_buf.set_cursor(pos);
                    }
                    HistoryEditorOp::EditorCursorRight => {
                        let pos = self.message_buf.offset_right(self.message_buf.cursor());
                        self.message_buf.set_cursor(pos);
                    }
                }
                Some(Event::applied("history_editor", op.clone()))
            }

            _ => None,
        }
    }

    fn render(&self, frame: &mut Frame, area: Rect, theme: &crate::theme::Theme) {
        match &self.mode {
            EditorMode::List => render_list_mode(self, frame, area, theme),
            EditorMode::Reword | EditorMode::Squash { .. } => {
                render_editor_mode(self, frame, area, theme)
            }
        }
    }
}

// ---------------------------------------------------------------------------
// Key handlers
// ---------------------------------------------------------------------------

fn handle_key_list(view: &GitHistoryEditorView, key: KeyEvent) -> Vec<Operation> {
    // ctrl+z — undo
    if key.key == Key::Char('z') && key.modifiers.contains(Modifiers::CTRL) {
        return vec![Operation::HistoryEditorLocal(HistoryEditorOp::UndoLast)];
    }

    match key.key {
        Key::ArrowUp | Key::Char('k') => vec![Operation::NavigateUp],
        Key::ArrowDown | Key::Char('j') => vec![Operation::NavigateDown],
        Key::PageUp => vec![Operation::NavigatePageUp],
        Key::PageDown => vec![Operation::NavigatePageDown],

        Key::Enter => {
            if view.pending_move_origin.is_some() {
                vec![Operation::HistoryEditorLocal(HistoryEditorOp::ConfirmMove)]
            } else {
                vec![]
            }
        }

        Key::Escape => {
            if view.pending_move_origin.is_some() {
                // Cancel pending move — app.rs will restore cursor position.
                vec![Operation::HistoryEditorLocal(HistoryEditorOp::ConfirmMove)]
            } else {
                vec![Operation::Close]
            }
        }

        Key::Char('r') if !view.loading && !view.commits.is_empty() => {
            vec![Operation::HistoryEditorLocal(HistoryEditorOp::StartReword)]
        }

        Key::Char('u') if !view.loading && !view.commits.is_empty() => {
            vec![Operation::HistoryEditorLocal(HistoryEditorOp::MoveUp)]
        }

        Key::Char('d') if !view.loading && !view.commits.is_empty() => {
            vec![Operation::HistoryEditorLocal(HistoryEditorOp::MoveDown)]
        }

        Key::Char('s') if !view.loading && view.cursor + 1 < view.commits.len() => {
            let target_idx = view.cursor + 1;
            vec![Operation::HistoryEditorLocal(HistoryEditorOp::StartSquash {
                target_idx,
            })]
        }

        Key::Delete | Key::Backspace if !view.loading && !view.commits.is_empty() => {
            vec![Operation::HistoryEditorLocal(HistoryEditorOp::DeleteCommit)]
        }

        _ => vec![],
    }
}

fn handle_key_editor(_view: &GitHistoryEditorView, key: KeyEvent) -> Vec<Operation> {
    // ctrl+s — apply edited message
    if key.key == Key::Char('s') && key.modifiers.contains(Modifiers::CTRL) {
        return vec![Operation::HistoryEditorLocal(HistoryEditorOp::ConfirmEdit)];
    }

    match (key.modifiers, key.key) {
        // Cancel editor without running git.
        (_, Key::Escape) => vec![Operation::Close],

        // Buffer navigation.
        (_, Key::ArrowUp) => vec![Operation::NavigateUp],
        (_, Key::ArrowDown) => vec![Operation::NavigateDown],
        (_, Key::ArrowLeft) => {
            vec![Operation::HistoryEditorLocal(HistoryEditorOp::EditorCursorLeft)]
        }
        (_, Key::ArrowRight) => {
            vec![Operation::HistoryEditorLocal(HistoryEditorOp::EditorCursorRight)]
        }

        // Text input.
        (_, Key::Enter) => {
            vec![Operation::HistoryEditorLocal(HistoryEditorOp::EditorInsertNewline)]
        }
        (_, Key::Backspace) => {
            vec![Operation::HistoryEditorLocal(HistoryEditorOp::EditorBackspace)]
        }
        (mods, Key::Char(c))
            if !mods.contains(Modifiers::CTRL) && !mods.contains(Modifiers::ALT) =>
        {
            vec![Operation::HistoryEditorLocal(HistoryEditorOp::EditorInsertChar(c))]
        }

        _ => vec![],
    }
}

// ---------------------------------------------------------------------------
// Rendering — list mode
// ---------------------------------------------------------------------------

fn render_list_mode(
    view: &GitHistoryEditorView,
    frame: &mut Frame,
    area: Rect,
    theme: &crate::theme::Theme,
) {
    let bg = theme.bg();
    let rows = Layout::default()
        .direction(Direction::Vertical)
        .constraints([
            Constraint::Length(1), // header
            Constraint::Min(1),    // commit list
            Constraint::Length(1), // hint / status
        ])
        .split(area);

    render_header(view, frame, rows[0], theme);
    render_commit_list(view, frame, rows[1], theme, false, None);
    render_hint(view, frame, rows[2], theme);

    // Clear background.
    frame.render_widget(Block::default().style(Style::default().bg(bg)), area);
    // Re-render on top (ratatui renders in order so we render background then content).
    render_header(view, frame, rows[0], theme);
    render_commit_list(view, frame, rows[1], theme, false, None);
    render_hint(view, frame, rows[2], theme);
}

fn render_header(
    view: &GitHistoryEditorView,
    frame: &mut Frame,
    area: Rect,
    theme: &crate::theme::Theme,
) {
    let bg = theme.bg();
    let accent = theme.accent();
    let fg_dim = theme.fg_dim();

    let undo_count = view.undo_stack.len();
    let undo_str = if undo_count > 0 {
        format!(" [ undo: {undo_count} ] ")
    } else {
        String::new()
    };

    let title = " Git History Editor ";
    let title_len = title.chars().count();
    let undo_len = undo_str.chars().count();
    let spacer_len = (area.width as usize)
        .saturating_sub(title_len)
        .saturating_sub(undo_len);
    let spacer = " ".repeat(spacer_len);

    let header_line = Line::from(vec![
        Span::styled(title, Style::default().fg(accent).add_modifier(Modifier::BOLD).bg(bg)),
        Span::styled(spacer, Style::default().bg(bg)),
        Span::styled(undo_str, Style::default().fg(fg_dim).bg(bg)),
    ]);

    frame.render_widget(
        Paragraph::new(header_line).style(Style::default().bg(bg)),
        area,
    );
}

fn render_commit_list(
    view: &GitHistoryEditorView,
    frame: &mut Frame,
    area: Rect,
    theme: &crate::theme::Theme,
    dimmed: bool,
    editor_cursor_row: Option<usize>,
) {
    let bg = if dimmed { theme.bg_inactive() } else { theme.bg() };
    let fg = if dimmed { theme.fg_dim() } else { theme.fg() };
    let fg_dim = theme.fg_dim();
    let sel_bg = theme.selection_bg();
    let sel_fg = theme.selection_fg();
    let accent = theme.accent();

    if view.loading {
        frame.render_widget(
            Paragraph::new(Span::styled(" Loading…", Style::default().fg(fg_dim).bg(bg))),
            area,
        );
        return;
    }

    if view.commits.is_empty() {
        frame.render_widget(
            Paragraph::new(Span::styled(" (no commits)", Style::default().fg(fg_dim).bg(bg))),
            area,
        );
        return;
    }

    let visible_h = area.height as usize;
    view.last_height.set(visible_h);

    let scroll = view.scroll.get();
    let visible = &view.commits[scroll..(scroll + visible_h).min(view.commits.len())];

    let mut ratatui_lines: Vec<Line> = Vec::with_capacity(visible_h);
    for (i, commit) in visible.iter().enumerate() {
        let abs_idx = scroll + i;
        let is_cursor = abs_idx == view.cursor && editor_cursor_row.is_none();
        let is_move_origin = view
            .pending_move_origin
            .map(|o| o == abs_idx)
            .unwrap_or(false);
        let is_active_in_editor = editor_cursor_row == Some(abs_idx);

        let item_bg = if is_cursor || is_active_in_editor {
            sel_bg
        } else {
            bg
        };
        let item_fg = if is_cursor || is_active_in_editor {
            sel_fg
        } else {
            fg
        };

        // Cursor indicator.
        let indicator = if is_cursor || is_active_in_editor {
            "▶ "
        } else {
            "  "
        };

        // Short oid (8 chars).
        let short_oid: String = commit.oid.chars().take(8).collect();

        // Message column: show badge when in pending-move or editor.
        let msg = if is_cursor && view.pending_move_origin.is_some() {
            format!("[MOVING] {}", truncate(&commit.summary, 28))
        } else if is_active_in_editor {
            match &view.mode {
                EditorMode::Reword => format!("[REWORDING] {}", truncate(&commit.summary, 24)),
                EditorMode::Squash { target_idx } if *target_idx == abs_idx => {
                    format!("[SQUASH TARGET] {}", truncate(&commit.summary, 20))
                }
                EditorMode::Squash { .. } => {
                    format!("[SQUASHING] {}", truncate(&commit.summary, 22))
                }
                _ => truncate(&commit.summary, 36),
            }
        } else {
            truncate(&commit.summary, 36)
        };

        // Move indicator suffix.
        let move_suffix = if is_move_origin && !is_cursor {
            " ↕"
        } else {
            ""
        };

        let oid_style = Style::default()
            .fg(if is_cursor { sel_fg } else { fg_dim })
            .bg(item_bg);
        let msg_style = Style::default()
            .fg(if is_cursor { accent } else { item_fg })
            .bg(item_bg)
            .add_modifier(if is_cursor {
                Modifier::BOLD
            } else {
                Modifier::empty()
            });
        let meta_style = Style::default().fg(fg_dim).bg(item_bg);

        let line = Line::from(vec![
            Span::styled(indicator, Style::default().fg(item_fg).bg(item_bg)),
            Span::styled(format!("{short_oid} "), oid_style),
            Span::styled(format!("{msg}{move_suffix}"), msg_style),
            Span::styled(
                format!("  {}  {}", commit.author_name, commit.date_relative),
                meta_style,
            ),
        ]);
        ratatui_lines.push(line);
    }

    // Pad with blank lines if fewer commits than visible height.
    while ratatui_lines.len() < visible_h {
        ratatui_lines.push(Line::from(Span::styled("", Style::default().bg(bg))));
    }

    frame.render_widget(
        Paragraph::new(ratatui_lines).style(Style::default().bg(bg)),
        area,
    );
}

fn render_hint(
    view: &GitHistoryEditorView,
    frame: &mut Frame,
    area: Rect,
    theme: &crate::theme::Theme,
) {
    let bg = theme.bg();
    let fg_dim = theme.fg_dim();

    let text = if let Some(ref msg) = view.status_msg {
        Span::styled(format!(" {msg}"), Style::default().fg(theme.level_warn()).bg(bg))
    } else if view.pending_move_origin.is_some() {
        Span::styled(
            "  u/d: move  Enter: confirm  Esc: cancel",
            Style::default().fg(fg_dim).bg(bg),
        )
    } else {
        Span::styled(
            "  r:reword  u/d:move  s:squash  del:drop  ctrl+z:undo",
            Style::default().fg(fg_dim).bg(bg),
        )
    };

    frame.render_widget(Paragraph::new(Line::from(text)).style(Style::default().bg(bg)), area);
}

// ---------------------------------------------------------------------------
// Rendering — editor mode
// ---------------------------------------------------------------------------

fn render_editor_mode(
    view: &GitHistoryEditorView,
    frame: &mut Frame,
    area: Rect,
    theme: &crate::theme::Theme,
) {
    let bg = theme.bg();
    let msg_bg = theme.bg_inactive();

    // Decide how to split the screen: commit list (top) + message editor (bottom).
    // Give the editor enough space for the message: at least 6 content rows
    // (plus 2 for separator+hint = 8 total), and up to half the screen so
    // multi-line commit bodies are fully visible.
    let msg_line_count = view.message_buf.lines().len() as u16;
    let desired_editor = (msg_line_count + 2).max(8).min(area.height / 2);
    let editor_height = desired_editor.max(8u16.min(area.height));
    let list_height = area.height.saturating_sub(editor_height);

    let rows = Layout::default()
        .direction(Direction::Vertical)
        .constraints([
            Constraint::Length(list_height),  // dimmed commit list
            Constraint::Length(1),            // separator / label
            Constraint::Min(1),               // message buffer
            Constraint::Length(1),            // action hint
        ])
        .split(area);

    // Dimmed commit list — highlight the row being edited.
    let editor_cursor_row = Some(view.cursor);
    frame.render_widget(Block::default().style(Style::default().bg(msg_bg)), rows[0]);
    render_commit_list(view, frame, rows[0], theme, true, editor_cursor_row);

    // Separator with label.
    let label = match &view.mode {
        EditorMode::Squash { .. } => " Squash Message ",
        _ => " Commit Message ",
    };
    frame.render_widget(
        Paragraph::new(Span::styled(
            label,
            Style::default()
                .fg(theme.accent())
                .add_modifier(Modifier::BOLD)
                .bg(bg),
        ))
        .block(
            Block::default()
                .borders(Borders::TOP)
                .border_type(BorderType::Plain)
                .style(Style::default().bg(bg)),
        ),
        rows[1],
    );

    // Message buffer.
    render_message_buffer(view, frame, rows[2], theme);

    // Action hint.
    frame.render_widget(
        Paragraph::new(Span::styled(
            "  ctrl+s: apply  Esc: cancel",
            Style::default().fg(theme.fg_dim()).bg(bg),
        ))
        .style(Style::default().bg(bg)),
        rows[3],
    );
}

fn render_message_buffer(
    view: &GitHistoryEditorView,
    frame: &mut Frame,
    area: Rect,
    theme: &crate::theme::Theme,
) {
    let bg = theme.bg_inactive();
    let fg = theme.fg_active();

    let lines = view.message_buf.lines();
    let cursor = view.message_buf.cursor();
    let area_h = area.height as usize;

    // Compute scroll offset so the cursor line is always visible.
    let scroll = if cursor.line < area_h {
        0
    } else {
        cursor.line - area_h + 1
    };

    let mut ratatui_lines: Vec<Line> = Vec::with_capacity(area_h);
    for (row_idx, line) in lines.iter().enumerate().skip(scroll).take(area_h) {
        if row_idx == cursor.line {
            // Embed block cursor on cursor row.
            let col = cursor.column.min(line.len());
            let before = format!("  {}", &line[..col]);
            let cursor_char = line[col..].chars().next().unwrap_or(' ');
            let after_start = col + cursor_char.len_utf8().min(line.len().saturating_sub(col));
            let after = if after_start < line.len() { &line[after_start..] } else { "" };

            ratatui_lines.push(Line::from(vec![
                Span::styled(before, Style::default().fg(fg).bg(bg)),
                Span::styled(
                    cursor_char.to_string(),
                    Style::default()
                        .fg(Color::Black)
                        .bg(Color::White)
                        .add_modifier(Modifier::BOLD),
                ),
                Span::styled(after.to_string(), Style::default().fg(fg).bg(bg)),
            ]));
        } else {
            ratatui_lines.push(Line::from(Span::styled(
                format!("  {line}"),
                Style::default().fg(fg).bg(bg),
            )));
        }
    }

    frame.render_widget(
        Paragraph::new(ratatui_lines).style(Style::default().bg(bg)),
        area,
    );
}

// ---------------------------------------------------------------------------
// Helpers
// ---------------------------------------------------------------------------

fn truncate(s: &str, max_chars: usize) -> String {
    let chars: Vec<char> = s.chars().collect();
    if chars.len() <= max_chars {
        s.to_owned()
    } else {
        let mut t: String = chars[..max_chars.saturating_sub(1)].iter().collect();
        t.push('…');
        t
    }
}

// ---------------------------------------------------------------------------
// Tests
// ---------------------------------------------------------------------------

#[cfg(test)]
mod tests {
    use super::*;
    use crate::operation::HistoryEditorOp;

    fn make_commit(oid: &str, summary: &str) -> CommitInfo {
        CommitInfo {
            oid: oid.to_owned(),
            summary: summary.to_owned(),
            message: summary.to_owned(),
            author_name: "Test".to_owned(),
            date_relative: "1d ago".to_owned(),
        }
    }

    fn settings() -> Settings {
        Settings::new(std::path::Path::new("/nonexistent/config.yaml"))
            .unwrap_or_else(|_| Settings::new(std::path::Path::new(".")).unwrap())
    }

    fn loaded_view() -> GitHistoryEditorView {
        let mut v = GitHistoryEditorView::new(PathBuf::from("/tmp/test_repo"));
        let commits = vec![
            make_commit("aaaaaa0000000000000000000000000000000000", "Commit A (newest)"),
            make_commit("bbbbbb0000000000000000000000000000000000", "Commit B"),
            make_commit("cccccc0000000000000000000000000000000000", "Commit C (oldest)"),
        ];
        v.handle_operation(
            &Operation::HistoryEditorLocal(HistoryEditorOp::CommitsLoaded(commits)),
            &settings(),
        );
        v
    }

    #[test]
    fn new_view_starts_loading() {
        let v = GitHistoryEditorView::new(PathBuf::from("/tmp/repo"));
        assert!(v.loading);
        assert!(v.commits.is_empty());
        assert_eq!(v.mode, EditorMode::List);
    }

    #[test]
    fn commits_loaded_clears_loading_flag() {
        let v = loaded_view();
        assert!(!v.loading);
        assert_eq!(v.commits.len(), 3);
    }

    #[test]
    fn navigate_down_moves_cursor() {
        let mut v = loaded_view();
        assert_eq!(v.cursor, 0);
        v.handle_operation(&Operation::NavigateDown, &settings());
        assert_eq!(v.cursor, 1);
    }

    #[test]
    fn navigate_up_does_not_go_below_zero() {
        let mut v = loaded_view();
        v.handle_operation(&Operation::NavigateUp, &settings());
        assert_eq!(v.cursor, 0);
    }

    #[test]
    fn start_reword_enters_reword_mode_with_prefilled_message() {
        let mut v = loaded_view();
        v.enter_reword_mode();
        assert_eq!(v.mode, EditorMode::Reword);
        assert_eq!(v.current_message(), "Commit A (newest)");
    }

    #[test]
    fn start_squash_enters_squash_mode_with_combined_message() {
        let mut v = loaded_view();
        v.cursor = 0;
        v.enter_squash_mode(1);
        assert!(matches!(v.mode, EditorMode::Squash { target_idx: 1 }));
        let msg = v.current_message();
        assert!(msg.contains("Commit A"), "expected A in msg: {msg}");
        assert!(msg.contains("Commit B"), "expected B in msg: {msg}");
    }

    #[test]
    fn swap_toward_older_moves_commit_down() {
        let mut v = loaded_view();
        v.cursor = 0;
        v.swap_toward_older();
        assert_eq!(v.commits[0].summary, "Commit B");
        assert_eq!(v.commits[1].summary, "Commit A (newest)");
        assert_eq!(v.cursor, 1);
    }

    #[test]
    fn swap_toward_newer_moves_commit_up() {
        let mut v = loaded_view();
        v.cursor = 1;
        v.swap_toward_newer();
        assert_eq!(v.commits[0].summary, "Commit B");
        assert_eq!(v.commits[1].summary, "Commit A (newest)");
        assert_eq!(v.cursor, 0);
    }

    #[test]
    fn operation_failed_sets_status_message() {
        let mut v = loaded_view();
        v.handle_operation(
            &Operation::HistoryEditorLocal(HistoryEditorOp::OperationFailed(
                "rebase failed".to_owned(),
            )),
            &settings(),
        );
        assert!(v.status_msg.as_deref().unwrap_or("").contains("rebase failed"));
    }

    #[test]
    fn operation_completed_refreshes_commit_list() {
        let mut v = loaded_view();
        let new_commits = vec![
            make_commit("dddddd0000000000000000000000000000000000", "Commit D"),
        ];
        v.handle_operation(
            &Operation::HistoryEditorLocal(HistoryEditorOp::OperationCompleted {
                commits: new_commits,
                pre_sha: None,
                description: String::new(),
            }),
            &settings(),
        );
        assert_eq!(v.commits.len(), 1);
        assert_eq!(v.commits[0].summary, "Commit D");
    }
}
