//! File History screen state — per-file MR timeline with rename tracking.
//!
//! Shows which MRs touched a file over time, resolving renames via BFS.
//! Users enter a file path, toggle options (follow renames, merged only,
//! show discussions), and browse a chronological MR list.

use crate::text_width::{next_char_boundary, prev_char_boundary};

// ---------------------------------------------------------------------------
// FileHistoryState
// ---------------------------------------------------------------------------

/// State for the File History screen.
#[derive(Debug, Default)]
pub struct FileHistoryState {
    /// User-entered file path.
    pub path_input: String,
    /// Cursor position within `path_input` (byte offset).
    pub path_cursor: usize,
    /// Whether the path input field has keyboard focus.
    pub path_focused: bool,

    /// The most recent result (None until first query).
    pub result: Option<FileHistoryResult>,

    /// Index of the currently selected MR in the result list.
    pub selected_mr_index: usize,
    /// Vertical scroll offset for the MR list.
    pub scroll_offset: u16,

    /// Whether to follow rename chains (default true).
    pub follow_renames: bool,
    /// Whether to show only merged MRs (default false).
    pub merged_only: bool,
    /// Whether to show inline discussion snippets (default false).
    pub show_discussions: bool,

    /// Cached list of known file paths for autocomplete.
    pub known_paths: Vec<String>,
    /// Filtered autocomplete matches for current input.
    pub autocomplete_matches: Vec<String>,
    /// Currently highlighted autocomplete suggestion index.
    pub autocomplete_index: usize,

    /// Monotonic generation counter for stale-response detection.
    pub generation: u64,
    /// Whether a query is currently in-flight.
    pub loading: bool,
}

// ---------------------------------------------------------------------------
// Result types (local to TUI — avoids coupling to CLI command structs)
// ---------------------------------------------------------------------------

/// Full result of a file-history query.
#[derive(Debug)]
pub struct FileHistoryResult {
    /// The queried file path.
    pub path: String,
    /// Resolved rename chain (may be just the original path).
    pub rename_chain: Vec<String>,
    /// Whether renames were actually followed.
    pub renames_followed: bool,
    /// MRs that touched any path in the rename chain.
    pub merge_requests: Vec<FileHistoryMr>,
    /// DiffNote discussion snippets on the file (when requested).
    pub discussions: Vec<FileDiscussion>,
    /// Total MR count (may exceed displayed count if limited).
    pub total_mrs: usize,
    /// Number of distinct file paths searched.
    pub paths_searched: usize,
}

/// A single MR that touched the file.
#[derive(Debug)]
pub struct FileHistoryMr {
    pub iid: i64,
    pub title: String,
    /// "merged", "opened", or "closed".
    pub state: String,
    pub author_username: String,
    /// "added", "modified", "deleted", or "renamed".
    pub change_type: String,
    pub merged_at_ms: Option<i64>,
    pub updated_at_ms: i64,
    pub merge_commit_sha: Option<String>,
}

/// A DiffNote discussion snippet on the file.
#[derive(Debug)]
pub struct FileDiscussion {
    pub discussion_id: String,
    pub author_username: String,
    pub body_snippet: String,
    pub path: String,
    pub created_at_ms: i64,
}

// ---------------------------------------------------------------------------
// State methods
// ---------------------------------------------------------------------------

impl FileHistoryState {
    /// Enter the screen: focus the path input.
    pub fn enter(&mut self) {
        self.path_focused = true;
        self.path_cursor = self.path_input.len();
    }

    /// Leave the screen: blur all inputs.
    pub fn leave(&mut self) {
        self.path_focused = false;
    }

    /// Whether any text input has focus.
    #[must_use]
    pub fn has_text_focus(&self) -> bool {
        self.path_focused
    }

    /// Blur all inputs.
    pub fn blur(&mut self) {
        self.path_focused = false;
    }

    /// Submit the current path (trigger a query).
    /// Returns the generation for stale detection.
    pub fn submit(&mut self) -> u64 {
        self.loading = true;
        self.bump_generation()
    }

    /// Apply query results if generation matches.
    pub fn apply_results(&mut self, generation: u64, result: FileHistoryResult) {
        if generation != self.generation {
            return; // Stale response — discard.
        }
        self.result = Some(result);
        self.loading = false;
        self.selected_mr_index = 0;
        self.scroll_offset = 0;
    }

    /// Toggle follow_renames. Returns new generation for re-query.
    pub fn toggle_follow_renames(&mut self) -> u64 {
        self.follow_renames = !self.follow_renames;
        self.bump_generation()
    }

    /// Toggle merged_only. Returns new generation for re-query.
    pub fn toggle_merged_only(&mut self) -> u64 {
        self.merged_only = !self.merged_only;
        self.bump_generation()
    }

    /// Toggle show_discussions. Returns new generation for re-query.
    pub fn toggle_show_discussions(&mut self) -> u64 {
        self.show_discussions = !self.show_discussions;
        self.bump_generation()
    }

    // --- Input field operations ---

    /// Insert a char at cursor.
    pub fn insert_char(&mut self, c: char) {
        if self.path_focused {
            self.path_input.insert(self.path_cursor, c);
            self.path_cursor += c.len_utf8();
        }
    }

    /// Delete the char before cursor.
    pub fn delete_char_before_cursor(&mut self) {
        if self.path_focused && self.path_cursor > 0 {
            let prev = prev_char_boundary(&self.path_input, self.path_cursor);
            self.path_input.drain(prev..self.path_cursor);
            self.path_cursor = prev;
        }
    }

    /// Move cursor left.
    pub fn cursor_left(&mut self) {
        if self.path_focused && self.path_cursor > 0 {
            self.path_cursor = prev_char_boundary(&self.path_input, self.path_cursor);
        }
    }

    /// Move cursor right.
    pub fn cursor_right(&mut self) {
        if self.path_focused && self.path_cursor < self.path_input.len() {
            self.path_cursor = next_char_boundary(&self.path_input, self.path_cursor);
        }
    }

    // --- Selection navigation ---

    /// Move selection up.
    pub fn select_prev(&mut self) {
        self.selected_mr_index = self.selected_mr_index.saturating_sub(1);
    }

    /// Move selection down (bounded by result count).
    pub fn select_next(&mut self, result_count: usize) {
        if result_count > 0 {
            self.selected_mr_index = (self.selected_mr_index + 1).min(result_count - 1);
        }
    }

    /// Ensure the selected row is visible within the viewport.
    pub fn ensure_visible(&mut self, viewport_height: usize) {
        if viewport_height == 0 {
            return;
        }
        let offset = self.scroll_offset as usize;
        if self.selected_mr_index < offset {
            self.scroll_offset = self.selected_mr_index as u16;
        } else if self.selected_mr_index >= offset + viewport_height {
            self.scroll_offset = (self.selected_mr_index - viewport_height + 1) as u16;
        }
    }

    // --- Internal ---

    fn bump_generation(&mut self) -> u64 {
        self.generation += 1;
        self.generation
    }
}

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

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

    #[test]
    fn test_default_state() {
        let state = FileHistoryState::default();
        assert!(state.path_input.is_empty());
        assert!(!state.path_focused);
        assert!(state.result.is_none());
        assert!(!state.follow_renames); // Default false, toggled on by user
        assert!(!state.merged_only);
        assert!(!state.show_discussions);
        assert_eq!(state.generation, 0);
    }

    #[test]
    fn test_enter_focuses_path() {
        let mut state = FileHistoryState {
            path_input: "src/lib.rs".into(),
            ..FileHistoryState::default()
        };
        state.enter();
        assert!(state.path_focused);
        assert_eq!(state.path_cursor, 10);
    }

    #[test]
    fn test_submit_bumps_generation() {
        let mut state = FileHistoryState::default();
        let generation = state.submit();
        assert_eq!(generation, 1);
        assert!(state.loading);
    }

    #[test]
    fn test_stale_response_discarded() {
        let mut state = FileHistoryState::default();
        let stale_gen = state.submit();
        // Bump again (user toggled an option).
        let _new_gen = state.toggle_merged_only();
        // Stale result arrives.
        state.apply_results(
            stale_gen,
            FileHistoryResult {
                path: "src/lib.rs".into(),
                rename_chain: vec!["src/lib.rs".into()],
                renames_followed: false,
                merge_requests: vec![],
                discussions: vec![],
                total_mrs: 0,
                paths_searched: 1,
            },
        );
        assert!(state.result.is_none()); // Discarded.
    }

    #[test]
    fn test_toggle_options_bump_generation() {
        let mut state = FileHistoryState::default();
        let g1 = state.toggle_follow_renames();
        assert_eq!(g1, 1);
        assert!(state.follow_renames);

        let g2 = state.toggle_merged_only();
        assert_eq!(g2, 2);
        assert!(state.merged_only);

        let g3 = state.toggle_show_discussions();
        assert_eq!(g3, 3);
        assert!(state.show_discussions);
    }

    #[test]
    fn test_insert_and_delete_char() {
        let mut state = FileHistoryState {
            path_focused: true,
            ..FileHistoryState::default()
        };
        state.insert_char('s');
        state.insert_char('r');
        state.insert_char('c');
        assert_eq!(state.path_input, "src");
        assert_eq!(state.path_cursor, 3);

        state.delete_char_before_cursor();
        assert_eq!(state.path_input, "sr");
        assert_eq!(state.path_cursor, 2);
    }

    #[test]
    fn test_select_prev_next() {
        let mut state = FileHistoryState::default();
        state.select_next(5);
        assert_eq!(state.selected_mr_index, 1);
        state.select_next(5);
        assert_eq!(state.selected_mr_index, 2);
        state.select_prev();
        assert_eq!(state.selected_mr_index, 1);
        state.select_prev();
        assert_eq!(state.selected_mr_index, 0);
        state.select_prev(); // Should not underflow.
        assert_eq!(state.selected_mr_index, 0);
    }

    #[test]
    fn test_ensure_visible() {
        let mut state = FileHistoryState {
            selected_mr_index: 15,
            ..FileHistoryState::default()
        };
        state.ensure_visible(5);
        assert_eq!(state.scroll_offset, 11); // 15 - 5 + 1
    }
}
