#![allow(dead_code)] // Phase 2: consumed by LoreApp and view/issue_list

//! Issue list screen state.
//!
//! Uses keyset pagination with a snapshot fence for stable ordering
//! under concurrent sync writes. Filter changes reset the pagination
//! cursor and snapshot fence.

use std::hash::{Hash, Hasher};

// ---------------------------------------------------------------------------
// Cursor (keyset pagination boundary)
// ---------------------------------------------------------------------------

/// Keyset pagination cursor — (updated_at, iid) boundary.
///
/// The next page query uses `WHERE (updated_at, iid) < (cursor.updated_at, cursor.iid)`
/// to avoid OFFSET instability.
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct IssueCursor {
    pub updated_at: i64,
    pub iid: i64,
}

// ---------------------------------------------------------------------------
// Filter
// ---------------------------------------------------------------------------

/// Structured filter for issue list queries.
#[derive(Debug, Clone, Default, PartialEq, Eq)]
pub struct IssueFilter {
    pub state: Option<String>,
    pub author: Option<String>,
    pub assignee: Option<String>,
    pub label: Option<String>,
    pub milestone: Option<String>,
    pub status: Option<String>,
    pub free_text: Option<String>,
    pub project_id: Option<i64>,
}

impl IssueFilter {
    /// Compute a hash for change detection.
    pub fn hash_value(&self) -> u64 {
        let mut hasher = std::collections::hash_map::DefaultHasher::new();
        self.state.hash(&mut hasher);
        self.author.hash(&mut hasher);
        self.assignee.hash(&mut hasher);
        self.label.hash(&mut hasher);
        self.milestone.hash(&mut hasher);
        self.status.hash(&mut hasher);
        self.free_text.hash(&mut hasher);
        self.project_id.hash(&mut hasher);
        hasher.finish()
    }

    /// Whether any filter is active.
    pub fn is_active(&self) -> bool {
        self.state.is_some()
            || self.author.is_some()
            || self.assignee.is_some()
            || self.label.is_some()
            || self.milestone.is_some()
            || self.status.is_some()
            || self.free_text.is_some()
            || self.project_id.is_some()
    }
}

// ---------------------------------------------------------------------------
// Row
// ---------------------------------------------------------------------------

/// A single row in the issue list.
#[derive(Debug, Clone)]
pub struct IssueListRow {
    pub project_path: String,
    pub iid: i64,
    pub title: String,
    pub state: String,
    pub author: String,
    pub labels: Vec<String>,
    pub updated_at: i64,
}

// ---------------------------------------------------------------------------
// Page result
// ---------------------------------------------------------------------------

/// Result from a paginated issue list query.
#[derive(Debug, Clone)]
pub struct IssueListPage {
    pub rows: Vec<IssueListRow>,
    pub next_cursor: Option<IssueCursor>,
    pub total_count: u64,
}

// ---------------------------------------------------------------------------
// Sort
// ---------------------------------------------------------------------------

/// Fields available for sorting.
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
pub enum SortField {
    #[default]
    UpdatedAt,
    Iid,
    Title,
    State,
    Author,
}

/// Sort direction.
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
pub enum SortOrder {
    #[default]
    Desc,
    Asc,
}

// ---------------------------------------------------------------------------
// IssueListState
// ---------------------------------------------------------------------------

/// State for the issue list screen.
#[derive(Debug, Default)]
pub struct IssueListState {
    /// Current page of issue rows.
    pub rows: Vec<IssueListRow>,
    /// Total count of matching issues.
    pub total_count: u64,
    /// Selected row index (within current window).
    pub selected_index: usize,
    /// Scroll offset for the entity table.
    pub scroll_offset: usize,
    /// Cursor for the next page.
    pub next_cursor: Option<IssueCursor>,
    /// Whether a prefetch is in flight.
    pub prefetch_in_flight: bool,
    /// Current filter.
    pub filter: IssueFilter,
    /// Raw filter input text.
    pub filter_input: String,
    /// Whether the filter bar has focus.
    pub filter_focused: bool,
    /// Sort field.
    pub sort_field: SortField,
    /// Sort direction.
    pub sort_order: SortOrder,
    /// Snapshot fence: max updated_at from initial load.
    pub snapshot_fence: Option<i64>,
    /// Hash of the current filter for change detection.
    pub filter_hash: u64,
    /// Whether Quick Peek is visible.
    pub peek_visible: bool,
}

impl IssueListState {
    /// Reset pagination state (called when filter changes or on refresh).
    pub fn reset_pagination(&mut self) {
        self.rows.clear();
        self.next_cursor = None;
        self.selected_index = 0;
        self.scroll_offset = 0;
        self.snapshot_fence = None;
        self.total_count = 0;
        self.prefetch_in_flight = false;
    }

    /// Apply a new page of results.
    pub fn apply_page(&mut self, page: IssueListPage) {
        // Set snapshot fence on first page load.
        if self.snapshot_fence.is_none() {
            self.snapshot_fence = page.rows.first().map(|r| r.updated_at);
        }
        self.rows.extend(page.rows);
        self.next_cursor = page.next_cursor;
        self.total_count = page.total_count;
        self.prefetch_in_flight = false;
    }

    /// Check if filter changed and reset if needed.
    pub fn check_filter_change(&mut self) -> bool {
        let new_hash = self.filter.hash_value();
        if new_hash != self.filter_hash {
            self.filter_hash = new_hash;
            self.reset_pagination();
            true
        } else {
            false
        }
    }

    /// Whether the user has scrolled near the end of current data (80% threshold).
    pub fn should_prefetch(&self) -> bool {
        if self.prefetch_in_flight || self.next_cursor.is_none() {
            return false;
        }
        if self.rows.is_empty() {
            return false;
        }
        let threshold = (self.rows.len() * 4) / 5; // 80%
        self.selected_index >= threshold
    }
}

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

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

    fn sample_page(count: usize, has_next: bool) -> IssueListPage {
        let rows: Vec<IssueListRow> = (0..count)
            .map(|i| IssueListRow {
                project_path: "group/project".into(),
                iid: (count - i) as i64,
                title: format!("Issue {}", count - i),
                state: "opened".into(),
                author: "taylor".into(),
                labels: vec![],
                updated_at: 1_700_000_000_000 - (i as i64 * 60_000),
            })
            .collect();

        let next_cursor = if has_next {
            rows.last().map(|r| IssueCursor {
                updated_at: r.updated_at,
                iid: r.iid,
            })
        } else {
            None
        };

        IssueListPage {
            rows,
            next_cursor,
            total_count: if has_next {
                (count * 2) as u64
            } else {
                count as u64
            },
        }
    }

    #[test]
    fn test_apply_page_sets_snapshot_fence() {
        let mut state = IssueListState::default();
        let page = sample_page(5, false);
        state.apply_page(page);

        assert_eq!(state.rows.len(), 5);
        assert!(state.snapshot_fence.is_some());
        assert_eq!(state.snapshot_fence.unwrap(), 1_700_000_000_000);
    }

    #[test]
    fn test_apply_page_appends() {
        let mut state = IssueListState::default();
        state.apply_page(sample_page(5, true));
        assert_eq!(state.rows.len(), 5);

        state.apply_page(sample_page(3, false));
        assert_eq!(state.rows.len(), 8);
    }

    #[test]
    fn test_reset_pagination_clears_state() {
        let mut state = IssueListState::default();
        state.apply_page(sample_page(5, true));
        state.selected_index = 3;

        state.reset_pagination();

        assert!(state.rows.is_empty());
        assert_eq!(state.selected_index, 0);
        assert!(state.next_cursor.is_none());
        assert!(state.snapshot_fence.is_none());
    }

    #[test]
    fn test_check_filter_change_detects_change() {
        let mut state = IssueListState::default();
        state.filter_hash = state.filter.hash_value();

        state.filter.state = Some("opened".into());
        assert!(state.check_filter_change());
    }

    #[test]
    fn test_check_filter_change_no_change() {
        let mut state = IssueListState::default();
        state.filter_hash = state.filter.hash_value();
        assert!(!state.check_filter_change());
    }

    #[test]
    fn test_should_prefetch() {
        let mut state = IssueListState::default();
        state.apply_page(sample_page(10, true));

        state.selected_index = 4; // 40% — no prefetch
        assert!(!state.should_prefetch());

        state.selected_index = 8; // 80% — prefetch
        assert!(state.should_prefetch());
    }

    #[test]
    fn test_should_prefetch_no_next_page() {
        let mut state = IssueListState::default();
        state.apply_page(sample_page(10, false));
        state.selected_index = 9;
        assert!(!state.should_prefetch());
    }

    #[test]
    fn test_should_prefetch_already_in_flight() {
        let mut state = IssueListState::default();
        state.apply_page(sample_page(10, true));
        state.selected_index = 9;
        state.prefetch_in_flight = true;
        assert!(!state.should_prefetch());
    }

    #[test]
    fn test_issue_filter_is_active() {
        let empty = IssueFilter::default();
        assert!(!empty.is_active());

        let active = IssueFilter {
            state: Some("opened".into()),
            ..Default::default()
        };
        assert!(active.is_active());
    }

    #[test]
    fn test_issue_filter_hash_deterministic() {
        let f1 = IssueFilter {
            state: Some("opened".into()),
            author: Some("taylor".into()),
            ..Default::default()
        };
        let f2 = f1.clone();
        assert_eq!(f1.hash_value(), f2.hash_value());
    }

    #[test]
    fn test_issue_filter_hash_differs() {
        let f1 = IssueFilter {
            state: Some("opened".into()),
            ..Default::default()
        };
        let f2 = IssueFilter {
            state: Some("closed".into()),
            ..Default::default()
        };
        assert_ne!(f1.hash_value(), f2.hash_value());
    }

    #[test]
    fn test_snapshot_fence_not_overwritten_on_second_page() {
        let mut state = IssueListState::default();
        state.apply_page(sample_page(5, true));
        let fence = state.snapshot_fence;

        state.apply_page(sample_page(3, false));
        assert_eq!(
            state.snapshot_fence, fence,
            "Fence should not change on second page"
        );
    }
}
