//! Log View - displays jj log output
//!
//! The main view of Tij, showing the change history.

mod input;
mod render;

use crate::model::Change;

// Re-export RebaseMode from model (canonical definition)
pub use crate::model::RebaseMode;

/// Input mode for Log View
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
pub enum InputMode {
    /// Normal navigation mode
    #[default]
    Normal,
    /// Text search input mode (for n/N navigation)
    SearchInput,
    /// Revset input mode (for jj filtering)
    RevsetInput,
    /// Describe input mode (editing change description)
    DescribeInput,
    /// Bookmark input mode (creating bookmark)
    BookmarkInput,
    /// Rebase mode selection (r/s/A/B single key)
    RebaseModeSelect,
    /// Rebase destination selection mode
    RebaseSelect,
    /// Squash destination selection mode
    SquashSelect,
    /// Compare revision selection mode (select second revision)
    CompareSelect,
    /// Interdiff revision selection mode (select second revision)
    InterdiffSelect,
    /// Bisect revision selection mode (select good revision)
    BisectSelect,
    /// Parallelize selection mode (select end of range)
    ParallelizeSelect,
    /// Rebase revset text input mode
    RebaseRevsetInput,
}

impl InputMode {
    pub fn input_bar_meta(self) -> Option<(&'static str, &'static str)> {
        match self {
            InputMode::SearchInput => Some(("Search: ", " / Search ")),
            InputMode::RevsetInput => Some(("Revset: ", " r Revset ")),
            InputMode::DescribeInput => Some(("Describe: ", " d Describe ")),
            InputMode::BookmarkInput => Some(("Bookmark: ", " b Bookmark ")),
            InputMode::RebaseRevsetInput => Some(("Revset: ", " Rebase Revset ")),
            // RebaseModeSelect/RebaseSelect/SquashSelect/CompareSelect/InterdiffSelect/ParallelizeSelect use status bar hints, not input bar
            InputMode::Normal
            | InputMode::RebaseModeSelect
            | InputMode::RebaseSelect
            | InputMode::SquashSelect
            | InputMode::CompareSelect
            | InputMode::InterdiffSelect
            | InputMode::BisectSelect
            | InputMode::ParallelizeSelect => None,
        }
    }
}

/// Source specification for rebase operation
#[derive(Debug, Clone, PartialEq, Eq)]
pub(crate) enum RebaseSource {
    /// Single selected change (change_id for highlight, commit_id for execution)
    Selected {
        change_id: String,
        commit_id: String,
    },
    /// Revset expression (passed directly to jj as-is)
    Revset(String),
}

/// Actions that LogView can request from App
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum LogAction {
    /// No action needed
    None,
    /// Open diff view for the given change ID
    OpenDiff(String),
    /// Execute revset filter
    ExecuteRevset(String),
    /// Clear revset filter (reset to default)
    ClearRevset,
    /// Start describe input mode (App should fetch full description and call set_describe_input)
    StartDescribe(String),
    /// Update change description
    Describe { revision: String, message: String },
    /// Open external editor for describe (jj describe --edit)
    DescribeExternal(String),
    /// Edit a specific change (jj edit)
    Edit(String),
    /// Create a new empty change (jj new)
    NewChange,
    /// Create a new change with selected revision as parent (jj new <revision>)
    NewChangeFrom {
        revision: String,
        display_name: String,
    },
    /// User pressed C on @ - show info notification suggesting 'c'
    NewChangeFromCurrent,
    /// Squash source change into destination (jj squash --from --into)
    SquashInto { source: String, destination: String },
    /// Abandon a change (jj abandon)
    Abandon(String),
    /// Split a change (jj split, opens external editor)
    Split(String),
    /// Create a bookmark on a change
    CreateBookmark { revision: String, name: String },
    /// Start bookmark deletion (opens selection dialog)
    StartBookmarkDelete,
    /// Rebase source change to destination with specified mode
    Rebase {
        source: String,
        destination: String,
        mode: RebaseMode,
        skip_emptied: bool,
        use_revset: bool,
        simplify_parents: bool,
    },
    /// Absorb working copy changes into ancestor commits
    Absorb,
    /// Open resolve list view for a change
    OpenResolveList {
        revision: String,
        is_working_copy: bool,
    },
    /// Fetch from remote
    Fetch,
    /// Start push flow (opens dialog if bookmarks exist)
    StartPush,
    /// Start track flow (opens dialog if untracked remotes exist)
    StartTrack,
    /// Start bookmark jump flow (opens selection dialog)
    StartBookmarkJump,
    /// Compare two revisions (open diff --from --to)
    Compare { from: String, to: String },
    /// Entered compare mode (notification with from_id)
    StartCompare(String),
    /// Compare blocked: same revision selected
    CompareSameRevision,
    /// Interdiff two revisions (open interdiff --from --to)
    Interdiff { from: String, to: String },
    /// Entered interdiff mode (notification with from_id)
    StartInterdiff(String),
    /// Interdiff blocked: same revision selected
    InterdiffSameRevision,
    /// Open Bookmark View
    OpenBookmarkView,
    /// Open Tag View
    OpenTagView,
    /// Open Workspace View
    OpenWorkspaceView,
    /// Open Command History View
    OpenCommandHistory,
    /// Move @ to next child (jj next --edit)
    NextChange,
    /// Move @ to previous parent (jj prev --edit)
    PrevChange,
    /// Toggle reversed display order
    ToggleReversed,
    /// Duplicate a change (jj duplicate)
    Duplicate(String),
    /// Open external diff editor for a change (jj diffedit)
    DiffEdit(String),
    /// Open evolution log for a change (jj evolog)
    OpenEvolog(String),
    /// Revert a change (jj revert, creates reverse-diff commit)
    Revert(String),
    /// Simplify parents (remove redundant parent edges)
    SimplifyParents(String),
    /// Parallelize commits (convert linear chain to siblings)
    Parallelize { from: String, to: String },
    /// Fix (apply configured code formatters to revision and descendants)
    Fix { revision: String, change_id: String },
    /// Entered parallelize mode (notification with from_id)
    StartParallelize(String),
    /// Parallelize blocked: same revision selected
    ParallelizeSameRevision,
    /// Entered bisect mode (notification with bad_id)
    StartBisect(String),
    /// Bisect: good and bad revisions selected
    Bisect { good: String, bad: String },
    /// Bisect blocked: same revision selected
    BisectSameRevision,
    /// Arrange: interactively arrange the commit graph (jj arrange)
    Arrange,
    /// Advance the closest ancestor bookmark(s) to @
    AdvanceBookmark,
    /// Metaedit: edit metadata (author, change-id, timestamp)
    Metaedit {
        /// For UI display (notification)
        change_id: String,
        /// For jj command execution (safe against divergent changes)
        commit_id: String,
    },
    /// Open stack diff (jj show <revset>, jj 0.42+) for the selected change's
    /// stack. Carries change_id for the working copy, commit_id otherwise
    /// (same convention as OpenDiff — unambiguous for divergent changes).
    ShowStackDiff(String),
    /// Show Agent Trace records for the selected change (Phase 2).
    /// Carries both short IDs because trace matching needs change_id (jj
    /// anchors) AND commit_id (git anchors) — see `TraceIndex::records_for`.
    ShowTraces {
        change_id: String,
        commit_id: String,
    },
    /// Toggle the "AI changes only" filter (Agent Trace A2)
    ToggleAiFilter,
    /// Show the AI contribution summary for the loaded changes (Agent Trace A1)
    ShowAiSummary,
    /// Write the AI attribution Markdown report for the loaded changes (A8)
    ExportAiReport,
    /// Toggle the command echo bar (command transparency P2)
    ToggleCommandEcho,
    /// Notify the count of orphaned traces over the loaded changes (A7)
    ShowOrphans,
    /// Start the `jj run` flow: open the target-preset select dialog.
    /// `revision` (commit_id) builds the revset; `change_id` is shown in the
    /// dialog title only (it is consumed there — the follow-up callback carries
    /// the revision only, no dead field).
    RunStart { revision: String, change_id: String },
}

/// Identifiers for low-frequency Log commands reachable only via the palette
/// (Phase 48-C). Decouples palette dispatch from single-key bindings.
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum LogCommand {
    Interdiff,
    Compare,
    Duplicate,
    DiffEdit,
    Revert,
    SimplifyParents,
    Parallelize,
    Metaedit,
    Arrange,
    Bisect,
    Fix,
    ShowStackDiff,
    ShowTraces,
    ToggleAiFilter,
    AiSummary,
    AiReport,
    AiOrphans,
    ToggleCommandEcho,
    Run,
}

/// Log View state
#[derive(Debug, Default)]
pub struct LogView {
    /// List of changes to display (includes graph-only lines)
    pub changes: Vec<Change>,
    /// Currently selected index in `changes`
    pub selected_index: usize,
    /// Scroll offset for display
    pub scroll_offset: usize,
    /// Current input mode
    pub input_mode: InputMode,
    /// Input buffer for revset/search/bookmark (NOT used for describe anymore)
    pub input_buffer: String,
    /// Revset input history
    pub revset_history: Vec<String>,
    /// Current revset filter (None = default)
    pub current_revset: Option<String>,
    /// Last search query for n/N navigation
    pub(crate) last_search_query: Option<String>,
    /// Revision (commit_id) being edited (for DescribeInput/BookmarkInput mode)
    pub editing_revision: Option<String>,
    /// Indices of selectable changes (not graph-only, and AI-matched while the
    /// AI filter is on). Drives navigation and selection.
    selectable_indices: Vec<usize>,
    /// Current position in selectable_indices
    selection_cursor: usize,
    /// Agent Trace "AI changes only" filter (A2). When on, non-AI and
    /// graph-only rows are hidden.
    pub(crate) ai_filter: bool,
    /// Absolute `changes` indices to DRAW, in order. Filter off → every row
    /// (incl. graph); filter on → AI-matched non-graph rows only. The single
    /// source of truth shared by render / search / title-count.
    visible_indices: Vec<usize>,
    /// Source for rebase (set when entering RebaseSelect mode)
    pub(crate) rebase_source: Option<RebaseSource>,
    /// Current rebase mode (set during RebaseModeSelect)
    pub(crate) rebase_mode: RebaseMode,
    /// Source change for squash (change_id, commit_id)
    pub(crate) squash_source: Option<(String, String)>,
    /// "From" change for compare (change_id, commit_id)
    pub(crate) compare_from: Option<(String, String)>,
    /// "From" change for interdiff (change_id, commit_id)
    pub(crate) interdiff_from: Option<(String, String)>,
    /// "From" change for parallelize (change_id, commit_id)
    pub(crate) parallelize_from: Option<(String, String)>,
    /// "Bad" revision for bisect (change_id, short_change_id)
    pub(crate) bisect_bad: Option<(String, String)>,
    /// Whether to display log in reversed order (oldest first)
    pub(crate) reversed: bool,
    /// Whether to pass --skip-emptied on rebase (toggled with S in RebaseSelect)
    pub(crate) skip_emptied: bool,
    /// Whether to pass --simplify-parents on rebase (toggled with P in RebaseSelect)
    pub(crate) simplify_parents: bool,
    /// Whether the log results were truncated by --limit
    pub truncated: bool,
    /// Agent Trace AI badges, keyed by row commit_id
    /// (confirmed → `[AI]`, heuristic → `[AI?]`; both empty = no trace data)
    pub ai_badges: crate::trace::AiBadgeSets,
}

pub mod empty_text {
    pub const TITLE: &str = "No changes found.";
    pub const HINT: &str = "Hint: Try '/' with revset all()";
}

impl LogView {
    /// Create a new LogView
    pub fn new() -> Self {
        Self::default()
    }

    /// Set the changes to display
    ///
    /// Rebuilds the visible/selectable sets (honoring the AI filter) and
    /// resets selection to the first selectable change.
    pub fn set_changes(&mut self, changes: Vec<Change>) {
        self.changes = changes;
        self.selection_cursor = 0;
        self.rebuild_visible();
    }

    /// Set Agent Trace AI badges (recomputed by App after each log refresh).
    ///
    /// Rebuilds the visible/selectable sets too: badges are applied AFTER
    /// `set_changes` on `Ctrl+L` (refresh.rs → apply_trace_badges), so the AI
    /// filter must re-evaluate against the new badges here (A2).
    pub fn set_ai_badges(&mut self, badges: crate::trace::AiBadgeSets) {
        self.ai_badges = badges;
        self.rebuild_visible();
    }

    /// Whether a change carries an AI badge (confirmed or heuristic).
    fn is_ai_change(&self, change: &Change) -> bool {
        let cid = change.commit_id.as_str();
        self.ai_badges.confirmed.contains(cid) || self.ai_badges.heuristic.contains(cid)
    }

    /// Recompute `visible_indices` and `selectable_indices` from `changes`,
    /// `ai_filter`, and `ai_badges`; re-pin the selection to a visible row.
    ///
    /// Invariant: when `selectable_indices` is non-empty, `selected_index`
    /// points at a selectable (and, under the filter, AI-matched) row. When it
    /// is empty, `selected_index` is left as-is — callers must guard via
    /// `selected_change()` / the visible-empty checks (no sentinel value).
    fn rebuild_visible(&mut self) {
        self.selectable_indices = self
            .changes
            .iter()
            .enumerate()
            .filter(|(_, c)| !c.is_graph_only && (!self.ai_filter || self.is_ai_change(c)))
            .map(|(i, _)| i)
            .collect();

        self.visible_indices = if self.ai_filter {
            // Filtered: graph lines hidden too → visible == selectable
            self.selectable_indices.clone()
        } else {
            // Unfiltered: draw every row (graph included) = legacy behavior
            (0..self.changes.len()).collect()
        };

        if self.selectable_indices.is_empty() {
            self.selection_cursor = 0;
            // selected_index left as-is; guarded by selected_change()
        } else {
            self.selection_cursor = self.selection_cursor.min(self.selectable_indices.len() - 1);
            self.selected_index = self.selectable_indices[self.selection_cursor];
        }
    }

    /// Toggle the AI-only filter. Returns the number of visible (selectable)
    /// changes after toggling — the caller decides whether to keep it on.
    pub fn toggle_ai_filter(&mut self) {
        self.ai_filter = !self.ai_filter;
        self.scroll_offset = 0;
        self.rebuild_visible();
    }

    /// Force the AI filter off (used by the empty-guard when there's nothing
    /// to show). Idempotent.
    pub fn set_ai_filter_off(&mut self) {
        if self.ai_filter {
            self.ai_filter = false;
            self.rebuild_visible();
        }
    }

    /// True while the filter is on but nothing matches (empty AI view).
    pub fn ai_filter_empty(&self) -> bool {
        self.ai_filter && self.selectable_indices.is_empty()
    }

    /// Number of selectable (visible, AI-matched while filtered) changes.
    pub fn visible_change_count(&self) -> usize {
        self.selectable_indices.len()
    }

    /// Absolute `changes` indices to draw, in order (render/scroll source).
    pub(crate) fn visible_indices(&self) -> &[usize] {
        &self.visible_indices
    }

    /// Get the currently selected change.
    ///
    /// Returns `None` while the AI filter is on and the selection is not a
    /// visible (AI-matched) row — including the empty case — so change-started
    /// actions never operate on a hidden `changes[..]` entry (A2).
    pub fn selected_change(&self) -> Option<&Change> {
        if self.ai_filter && !self.selectable_indices.contains(&self.selected_index) {
            return None;
        }
        self.changes.get(self.selected_index)
    }

    /// Move selection up (skips graph-only lines)
    pub fn move_up(&mut self) {
        if self.selection_cursor > 0 {
            self.selection_cursor -= 1;
            self.selected_index = self.selectable_indices[self.selection_cursor];
        }
    }

    /// Move selection down (skips graph-only lines)
    pub fn move_down(&mut self) {
        if self.selection_cursor < self.selectable_indices.len().saturating_sub(1) {
            self.selection_cursor += 1;
            self.selected_index = self.selectable_indices[self.selection_cursor];
        }
    }

    /// Move to top (first selectable change)
    pub fn move_to_top(&mut self) {
        self.selection_cursor = 0;
        self.selected_index = self.selectable_indices.first().copied().unwrap_or(0);
    }

    /// Move to bottom (last selectable change)
    pub fn move_to_bottom(&mut self) {
        if let Some(&last) = self.selectable_indices.last() {
            self.selection_cursor = self.selectable_indices.len().saturating_sub(1);
            self.selected_index = last;
        }
    }

    /// Start text search input mode
    pub fn start_search_input(&mut self) {
        self.input_mode = InputMode::SearchInput;
        self.input_buffer.clear();
    }

    /// Start revset input mode
    pub fn start_revset_input(&mut self) {
        self.input_mode = InputMode::RevsetInput;
        self.input_buffer.clear();
    }

    /// Cancel input mode
    pub fn cancel_input(&mut self) {
        self.input_mode = InputMode::Normal;
        self.input_buffer.clear();
        self.editing_revision = None;
    }

    /// Set describe input mode with the description text (single-line only)
    ///
    /// Called by App after verifying the description is single-line.
    /// Multi-line descriptions are blocked at the App layer (directed to Ctrl+E).
    pub fn set_describe_input(&mut self, revision: String, description: String) {
        self.editing_revision = Some(revision);
        self.input_buffer = description;
        self.input_mode = InputMode::DescribeInput;
    }

    /// Start bookmark input mode for the selected change
    pub fn start_bookmark_input(&mut self) {
        // Clone commit_id first to avoid borrow conflict
        let commit_id = self.selected_change().map(|c| c.commit_id.to_string());

        if let Some(commit_id) = commit_id {
            self.editing_revision = Some(commit_id);
            self.input_buffer.clear();
            self.input_mode = InputMode::BookmarkInput;
        }
    }

    /// Start rebase mode selection (r/s/A/B single key)
    ///
    /// Saves the source change and enters RebaseModeSelect mode.
    /// Returns true if mode was entered, false if no change is selected.
    pub fn start_rebase_mode_select(&mut self) -> bool {
        let source = self
            .selected_change()
            .map(|c| (c.change_id.to_string(), c.commit_id.to_string()));

        if let Some((change_id, commit_id)) = source {
            self.rebase_source = Some(RebaseSource::Selected {
                change_id,
                commit_id,
            });
            self.skip_emptied = false;
            self.simplify_parents = false;
            self.input_mode = InputMode::RebaseModeSelect;
            true
        } else {
            false
        }
    }

    /// Cancel rebase mode selection
    pub fn cancel_rebase_mode_select(&mut self) {
        self.rebase_source = None;
        self.rebase_mode = RebaseMode::default();
        self.skip_emptied = false;
        self.simplify_parents = false;
        self.input_mode = InputMode::Normal;
    }

    /// Start rebase destination selection mode
    ///
    /// Returns true if mode was entered, false if no change is selected.
    pub fn start_rebase_select(&mut self) -> bool {
        // Clone change data first to avoid borrow conflict
        let source = self
            .selected_change()
            .map(|c| (c.change_id.to_string(), c.commit_id.to_string()));

        if let Some((change_id, commit_id)) = source {
            self.rebase_source = Some(RebaseSource::Selected {
                change_id,
                commit_id,
            });
            self.input_mode = InputMode::RebaseSelect;
            true
        } else {
            false
        }
    }

    /// Cancel rebase selection mode
    pub fn cancel_rebase_select(&mut self) {
        self.rebase_source = None;
        self.rebase_mode = RebaseMode::default();
        self.skip_emptied = false;
        self.simplify_parents = false;
        self.input_mode = InputMode::Normal;
    }

    /// Start squash destination selection mode
    ///
    /// Returns true if mode was entered, false if no change is selected.
    pub fn start_squash_select(&mut self) -> bool {
        let source = self
            .selected_change()
            .map(|c| (c.change_id.to_string(), c.commit_id.to_string()));

        if let Some(pair) = source {
            self.squash_source = Some(pair);
            self.input_mode = InputMode::SquashSelect;
            true
        } else {
            false
        }
    }

    /// Cancel squash selection mode
    pub fn cancel_squash_select(&mut self) {
        self.squash_source = None;
        self.input_mode = InputMode::Normal;
    }

    /// Start compare revision selection mode
    ///
    /// The currently selected change becomes the "from" revision.
    /// The user then selects the "to" revision.
    /// Returns true if mode was entered, false if no change is selected.
    pub fn start_compare_select(&mut self) -> bool {
        let source = self
            .selected_change()
            .map(|c| (c.change_id.to_string(), c.commit_id.to_string()));

        if let Some(pair) = source {
            self.compare_from = Some(pair);
            self.input_mode = InputMode::CompareSelect;
            true
        } else {
            false
        }
    }

    /// Cancel compare selection mode
    pub fn cancel_compare_select(&mut self) {
        self.compare_from = None;
        self.input_mode = InputMode::Normal;
    }

    /// Start interdiff revision selection mode
    ///
    /// The currently selected change becomes the "from" revision.
    /// The user then selects the "to" revision.
    /// Returns true if mode was entered, false if no change is selected.
    pub fn start_interdiff_select(&mut self) -> bool {
        let source = self
            .selected_change()
            .map(|c| (c.change_id.to_string(), c.commit_id.to_string()));

        if let Some(pair) = source {
            self.interdiff_from = Some(pair);
            self.input_mode = InputMode::InterdiffSelect;
            true
        } else {
            false
        }
    }

    /// Cancel interdiff selection mode
    pub fn cancel_interdiff_select(&mut self) {
        self.interdiff_from = None;
        self.input_mode = InputMode::Normal;
    }

    /// Start bisect revision selection mode
    ///
    /// The currently selected change becomes the "bad" revision.
    /// The user then selects the "good" revision.
    /// Returns true if mode was entered, false if no change is selected.
    pub fn start_bisect_select(&mut self) -> bool {
        let source = self
            .selected_change()
            .map(|c| (c.change_id.to_string(), c.change_id.short().to_string()));

        if let Some(pair) = source {
            self.bisect_bad = Some(pair);
            self.input_mode = InputMode::BisectSelect;
            true
        } else {
            false
        }
    }

    /// Cancel bisect selection mode
    pub fn cancel_bisect_select(&mut self) {
        self.bisect_bad = None;
        self.input_mode = InputMode::Normal;
    }

    /// Start parallelize selection mode
    ///
    /// The currently selected change becomes the "from" revision.
    /// The user then selects the "to" revision to define the range.
    /// Returns true if mode was entered, false if no change is selected.
    pub fn start_parallelize_select(&mut self) -> bool {
        let source = self
            .selected_change()
            .map(|c| (c.change_id.to_string(), c.commit_id.to_string()));

        if let Some(pair) = source {
            self.parallelize_from = Some(pair);
            self.input_mode = InputMode::ParallelizeSelect;
            true
        } else {
            false
        }
    }

    /// Cancel parallelize selection mode
    pub fn cancel_parallelize_select(&mut self) {
        self.parallelize_from = None;
        self.input_mode = InputMode::Normal;
    }

    /// Select a change by its change_id (exact match)
    ///
    /// Returns true if the change was found and selected, false otherwise.
    /// The scroll_offset will be updated during next render via calculate_scroll_offset().
    pub fn select_change_by_id(&mut self, change_id: &str) -> bool {
        // Find the change in the selectable indices
        for (cursor, &idx) in self.selectable_indices.iter().enumerate() {
            if let Some(change) = self.changes.get(idx)
                && change.change_id == change_id
            {
                self.selection_cursor = cursor;
                self.selected_index = idx;
                return true;
            }
        }
        false
    }

    /// Get the current selection cursor position (index into selectable_indices)
    pub fn selected_selectable_index(&self) -> usize {
        self.selection_cursor
    }

    /// Select the working copy (@) change
    ///
    /// Searches for the change with `is_working_copy == true` and moves
    /// the cursor to it. Used after `jj next`/`jj prev` to follow @.
    /// Returns true if working copy was found and selected.
    pub fn select_working_copy(&mut self) -> bool {
        for (cursor, &idx) in self.selectable_indices.iter().enumerate() {
            if let Some(change) = self.changes.get(idx)
                && change.is_working_copy
            {
                self.selection_cursor = cursor;
                self.selected_index = idx;
                return true;
            }
        }
        false
    }

    /// Select a change by prefix match on change_id
    ///
    /// Used when the caller has a potentially shorter change_id (e.g., from
    /// `jj file annotate` which uses `shortest()` format instead of `short(8)`).
    pub fn select_change_by_prefix(&mut self, prefix: &str) -> bool {
        for (cursor, &idx) in self.selectable_indices.iter().enumerate() {
            if let Some(change) = self.changes.get(idx)
                && change.change_id.starts_with(prefix)
            {
                self.selection_cursor = cursor;
                self.selected_index = idx;
                return true;
            }
        }
        false
    }
}

#[cfg(test)]
mod tests;
