//! Trace screen state — file → MR → issue chain drill-down.
//!
//! Users enter a file path, and the trace query resolves rename chains,
//! finds MRs that touched the file, links issues via entity_references,
//! and extracts DiffNote discussions. Each result chain can be
//! expanded/collapsed independently.

use std::collections::HashSet;

use lore::core::trace::TraceResult;

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

// ---------------------------------------------------------------------------
// TraceState
// ---------------------------------------------------------------------------

/// State for the Trace screen.
#[derive(Debug, Default)]
pub struct TraceState {
    /// User-entered file path (with optional :line suffix).
    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,

    /// Parsed line filter from `:N` suffix (stored but not yet used for highlighting).
    pub line_filter: Option<u32>,

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

    /// Index of the currently selected chain in the trace result.
    pub selected_chain_index: usize,
    /// Set of chain indices that are currently expanded.
    pub expanded_chains: HashSet<usize>,

    /// Whether to follow rename chains in the query (default true).
    pub follow_renames: bool,
    /// Whether to include DiffNote discussions (default true).
    pub include_discussions: bool,

    /// Vertical scroll offset for the chain list.
    pub scroll_offset: u16,

    /// 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,

    /// Generation counter for stale response guard.
    pub generation: u64,
    /// Whether a query is in flight.
    pub loading: bool,
}

impl TraceState {
    /// Initialize defaults for a fresh Trace screen entry.
    pub fn enter(&mut self) {
        self.path_focused = true;
        self.follow_renames = true;
        self.include_discussions = true;
    }

    /// Clean up when leaving the Trace screen.
    pub fn leave(&mut self) {
        self.path_focused = false;
    }

    /// Submit the current path input as a trace query.
    ///
    /// Bumps generation, parses the :line suffix, and returns the
    /// new generation if the path is non-empty.
    pub fn submit(&mut self) -> Option<u64> {
        let trimmed = self.path_input.trim();
        if trimmed.is_empty() {
            return None;
        }

        let (path, line) = lore::cli::commands::trace::parse_trace_path(trimmed);
        self.path_input = path;
        self.path_cursor = self.path_input.len();
        self.line_filter = line;

        self.generation += 1;
        self.loading = true;
        self.selected_chain_index = 0;
        self.expanded_chains.clear();
        self.scroll_offset = 0;
        self.path_focused = false;
        self.autocomplete_matches.clear();

        Some(self.generation)
    }

    /// Apply a trace result, guarded by generation counter.
    pub fn apply_result(&mut self, generation: u64, result: TraceResult) {
        if generation != self.generation {
            return; // Stale response — discard.
        }
        self.result = Some(result);
        self.loading = false;
    }

    /// Toggle the expand/collapse state of the selected chain.
    pub fn toggle_expand(&mut self) {
        if self.expanded_chains.contains(&self.selected_chain_index) {
            self.expanded_chains.remove(&self.selected_chain_index);
        } else {
            self.expanded_chains.insert(self.selected_chain_index);
        }
    }

    /// Toggle follow_renames and bump generation (triggers re-fetch).
    pub fn toggle_follow_renames(&mut self) -> Option<u64> {
        self.follow_renames = !self.follow_renames;
        self.requery()
    }

    /// Toggle include_discussions and bump generation (triggers re-fetch).
    pub fn toggle_include_discussions(&mut self) -> Option<u64> {
        self.include_discussions = !self.include_discussions;
        self.requery()
    }

    /// Re-query with current settings if path is non-empty.
    fn requery(&mut self) -> Option<u64> {
        if self.path_input.trim().is_empty() {
            return None;
        }
        self.generation += 1;
        self.loading = true;
        self.selected_chain_index = 0;
        self.expanded_chains.clear();
        self.scroll_offset = 0;
        Some(self.generation)
    }

    /// Select the previous chain.
    pub fn select_prev(&mut self) {
        if self.selected_chain_index > 0 {
            self.selected_chain_index -= 1;
            self.ensure_visible();
        }
    }

    /// Select the next chain.
    pub fn select_next(&mut self) {
        let max = self.chain_count().saturating_sub(1);
        if self.selected_chain_index < max {
            self.selected_chain_index += 1;
            self.ensure_visible();
        }
    }

    /// Number of trace chains in the current result.
    fn chain_count(&self) -> usize {
        self.result.as_ref().map_or(0, |r| r.trace_chains.len())
    }

    /// Ensure the selected chain is visible within the scroll viewport.
    fn ensure_visible(&mut self) {
        let idx = self.selected_chain_index as u16;
        if idx < self.scroll_offset {
            self.scroll_offset = idx;
        }
        // Rough viewport — exact height adjusted in render.
    }

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

    /// Remove focus from all text inputs.
    pub fn blur(&mut self) {
        self.path_focused = false;
        self.autocomplete_matches.clear();
    }

    /// Focus the path input.
    pub fn focus_input(&mut self) {
        self.path_focused = true;
        self.update_autocomplete();
    }

    // --- Text editing helpers ---

    /// Insert a character at the cursor position (byte offset).
    pub fn insert_char(&mut self, ch: char) {
        self.path_input.insert(self.path_cursor, ch);
        self.path_cursor += ch.len_utf8();
        self.update_autocomplete();
    }

    /// Delete the character before the cursor (byte offset).
    pub fn delete_char_before_cursor(&mut self) {
        if self.path_cursor == 0 {
            return;
        }
        let prev = prev_char_boundary(&self.path_input, self.path_cursor);
        self.path_input.drain(prev..self.path_cursor);
        self.path_cursor = prev;
        self.update_autocomplete();
    }

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

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

    // --- Autocomplete ---

    /// Update autocomplete matches based on current input.
    pub fn update_autocomplete(&mut self) {
        let input_lower = self.path_input.to_lowercase();
        if input_lower.is_empty() {
            self.autocomplete_matches.clear();
            self.autocomplete_index = 0;
            return;
        }

        self.autocomplete_matches = self
            .known_paths
            .iter()
            .filter(|p| p.to_lowercase().contains(&input_lower))
            .take(10) // Limit visible suggestions.
            .cloned()
            .collect();
        self.autocomplete_index = 0;
    }

    /// Cycle to the next autocomplete suggestion.
    pub fn autocomplete_next(&mut self) {
        if self.autocomplete_matches.is_empty() {
            return;
        }
        self.autocomplete_index = (self.autocomplete_index + 1) % self.autocomplete_matches.len();
    }

    /// Accept the current autocomplete suggestion into the path input.
    pub fn accept_autocomplete(&mut self) {
        if let Some(match_) = self.autocomplete_matches.get(self.autocomplete_index) {
            self.path_input = match_.clone();
            self.path_cursor = self.path_input.len();
            self.autocomplete_matches.clear();
        }
    }
}

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

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

    #[test]
    fn test_trace_state_default() {
        let state = TraceState::default();
        assert!(state.path_input.is_empty());
        assert!(!state.path_focused);
        assert!(!state.follow_renames); // Default false, enter() sets true.
        assert!(state.result.is_none());
        assert_eq!(state.generation, 0);
    }

    #[test]
    fn test_trace_state_enter_sets_defaults() {
        let mut state = TraceState::default();
        state.enter();
        assert!(state.path_focused);
        assert!(state.follow_renames);
        assert!(state.include_discussions);
    }

    #[test]
    fn test_submit_empty_returns_none() {
        let mut state = TraceState::default();
        assert!(state.submit().is_none());
        assert_eq!(state.generation, 0);
    }

    #[test]
    fn test_submit_with_path_bumps_generation() {
        let mut state = TraceState {
            path_input: "src/main.rs".into(),
            ..TraceState::default()
        };
        let generation = state.submit();
        assert_eq!(generation, Some(1));
        assert_eq!(state.generation, 1);
        assert!(state.loading);
        assert!(!state.path_focused);
    }

    #[test]
    fn test_submit_parses_line_suffix() {
        let mut state = TraceState {
            path_input: "src/main.rs:42".into(),
            ..TraceState::default()
        };
        state.submit();
        assert_eq!(state.path_input, "src/main.rs");
        assert_eq!(state.line_filter, Some(42));
    }

    #[test]
    fn test_apply_result_matching_generation() {
        let mut state = TraceState {
            path_input: "src/lib.rs".into(),
            ..TraceState::default()
        };
        state.submit(); // generation = 1

        let result = TraceResult {
            path: "src/lib.rs".into(),
            resolved_paths: vec![],
            renames_followed: false,
            trace_chains: vec![],
            total_chains: 0,
        };

        state.apply_result(1, result);
        assert!(state.result.is_some());
        assert!(!state.loading);
    }

    #[test]
    fn test_apply_result_stale_generation_discarded() {
        let mut state = TraceState {
            path_input: "src/lib.rs".into(),
            ..TraceState::default()
        };
        state.submit(); // generation = 1
        state.path_input = "src/other.rs".into();
        state.submit(); // generation = 2

        let stale_result = TraceResult {
            path: "src/lib.rs".into(),
            resolved_paths: vec![],
            renames_followed: false,
            trace_chains: vec![],
            total_chains: 0,
        };

        state.apply_result(1, stale_result); // Stale — should be discarded.
        assert!(state.result.is_none());
        assert!(state.loading); // Still loading.
    }

    #[test]
    fn test_toggle_expand() {
        let mut state = TraceState {
            selected_chain_index: 2,
            ..TraceState::default()
        };

        state.toggle_expand();
        assert!(state.expanded_chains.contains(&2));

        state.toggle_expand();
        assert!(!state.expanded_chains.contains(&2));
    }

    #[test]
    fn test_toggle_follow_renames_requeues() {
        let mut state = TraceState {
            path_input: "src/main.rs".into(),
            path_focused: true,
            follow_renames: true,
            include_discussions: true,
            ..TraceState::default()
        };
        assert!(state.follow_renames);

        let generation = state.toggle_follow_renames();
        assert!(!state.follow_renames);
        assert_eq!(generation, Some(1));
        assert!(state.loading);
    }

    #[test]
    fn test_toggle_include_discussions_requeues() {
        let mut state = TraceState {
            path_input: "src/main.rs".into(),
            path_focused: true,
            follow_renames: true,
            include_discussions: true,
            ..TraceState::default()
        };
        assert!(state.include_discussions);

        let generation = state.toggle_include_discussions();
        assert!(!state.include_discussions);
        assert_eq!(generation, Some(1));
    }

    #[test]
    fn test_select_prev_next() {
        let mut state = TraceState {
            result: Some(TraceResult {
                path: "x".into(),
                resolved_paths: vec![],
                renames_followed: false,
                trace_chains: vec![
                    lore::core::trace::TraceChain {
                        mr_iid: 1,
                        mr_title: "a".into(),
                        mr_state: "merged".into(),
                        mr_author: "x".into(),
                        change_type: "modified".into(),
                        merged_at_iso: None,
                        updated_at_iso: "2024-01-01".into(),
                        web_url: None,
                        issues: vec![],
                        discussions: vec![],
                    },
                    lore::core::trace::TraceChain {
                        mr_iid: 2,
                        mr_title: "b".into(),
                        mr_state: "merged".into(),
                        mr_author: "y".into(),
                        change_type: "added".into(),
                        merged_at_iso: None,
                        updated_at_iso: "2024-01-02".into(),
                        web_url: None,
                        issues: vec![],
                        discussions: vec![],
                    },
                ],
                total_chains: 2,
            }),
            ..TraceState::default()
        };

        assert_eq!(state.selected_chain_index, 0);
        state.select_next();
        assert_eq!(state.selected_chain_index, 1);
        state.select_next(); // Clamped at max.
        assert_eq!(state.selected_chain_index, 1);
        state.select_prev();
        assert_eq!(state.selected_chain_index, 0);
        state.select_prev(); // Clamped at 0.
        assert_eq!(state.selected_chain_index, 0);
    }

    #[test]
    fn test_insert_char_and_delete() {
        let mut state = TraceState::default();
        state.insert_char('a');
        state.insert_char('b');
        state.insert_char('c');
        assert_eq!(state.path_input, "abc");
        assert_eq!(state.path_cursor, 3);

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

    #[test]
    fn test_autocomplete_filters() {
        let mut state = TraceState {
            known_paths: vec!["src/a.rs".into(), "src/b.rs".into(), "lib/c.rs".into()],
            path_input: "src/".into(),
            ..TraceState::default()
        };
        state.update_autocomplete();
        assert_eq!(state.autocomplete_matches.len(), 2);
        assert!(state.autocomplete_matches.contains(&"src/a.rs".to_string()));
        assert!(state.autocomplete_matches.contains(&"src/b.rs".to_string()));
    }

    #[test]
    fn test_autocomplete_next_cycles() {
        let mut state = TraceState {
            known_paths: vec!["a.rs".into(), "ab.rs".into()],
            path_input: "a".into(),
            ..TraceState::default()
        };
        state.update_autocomplete();
        assert_eq!(state.autocomplete_matches.len(), 2);
        assert_eq!(state.autocomplete_index, 0);

        state.autocomplete_next();
        assert_eq!(state.autocomplete_index, 1);

        state.autocomplete_next();
        assert_eq!(state.autocomplete_index, 0); // Wrapped.
    }

    #[test]
    fn test_accept_autocomplete() {
        let mut state = TraceState {
            known_paths: vec!["src/main.rs".into()],
            path_input: "src/".into(),
            ..TraceState::default()
        };
        state.update_autocomplete();
        assert_eq!(state.autocomplete_matches.len(), 1);

        state.accept_autocomplete();
        assert_eq!(state.path_input, "src/main.rs");
        assert!(state.autocomplete_matches.is_empty());
    }

    #[test]
    fn test_has_text_focus() {
        let state = TraceState::default();
        assert!(!state.has_text_focus());
        let state = TraceState {
            path_focused: true,
            ..TraceState::default()
        };
        assert!(state.has_text_focus());
    }

    #[test]
    fn test_blur_clears_focus_and_autocomplete() {
        let mut state = TraceState {
            path_focused: true,
            autocomplete_matches: vec!["a".into()],
            ..TraceState::default()
        };

        state.blur();
        assert!(!state.path_focused);
        assert!(state.autocomplete_matches.is_empty());
    }
}
