#![allow(dead_code)] // Phase 1: consumed by LoreApp in bd-6pmy

//! Top-level state composition for the TUI.
//!
//! Each screen has its own state struct. State is preserved when
//! navigating away — screens are never cleared on pop.
//!
//! [`LoadState`] enables stale-while-revalidate: screens show the last
//! available data during a refresh, with a spinner indicating the load.
//!
//! [`ScreenIntent`] is the pure return type from state handlers — they
//! never spawn async tasks directly. The intent is interpreted by
//! [`LoreApp`](crate::app::LoreApp) which dispatches through the
//! [`TaskSupervisor`](crate::task_supervisor::TaskSupervisor).

pub mod bootstrap;
pub mod command_palette;
pub mod dashboard;
pub mod doctor;
pub mod file_history;
pub mod issue_detail;
pub mod issue_list;
pub mod mr_detail;
pub mod mr_list;
pub mod scope_picker;
pub mod search;
pub mod stats;
pub mod sync;
pub mod sync_delta_ledger;
pub mod timeline;
pub mod trace;
pub mod who;

use std::collections::{HashMap, HashSet};

use crate::message::Screen;

// Re-export screen states for convenience.
pub use bootstrap::BootstrapState;
pub use command_palette::CommandPaletteState;
pub use dashboard::DashboardState;
pub use doctor::DoctorState;
pub use file_history::FileHistoryState;
pub use issue_detail::IssueDetailState;
pub use issue_list::IssueListState;
pub use mr_detail::MrDetailState;
pub use mr_list::MrListState;
pub use scope_picker::ScopePickerState;
pub use search::SearchState;
pub use stats::StatsState;
pub use sync::SyncState;
pub use timeline::TimelineState;
pub use trace::TraceState;
pub use who::WhoState;

// ---------------------------------------------------------------------------
// LoadState
// ---------------------------------------------------------------------------

/// Loading state for a screen's data.
///
/// Enables stale-while-revalidate: screens render their last data while
/// showing a spinner when `Refreshing`.
#[derive(Debug, Clone, PartialEq, Eq, Default)]
pub enum LoadState {
    /// No load in progress, data is current (or screen was never loaded).
    #[default]
    Idle,
    /// First load — no data to show yet, display a full-screen spinner.
    LoadingInitial,
    /// Background refresh — show existing data with a spinner indicator.
    Refreshing,
    /// Load failed — display the error alongside any stale data.
    Error(String),
}

impl LoadState {
    /// Whether data is currently being loaded.
    #[must_use]
    pub fn is_loading(&self) -> bool {
        matches!(self, Self::LoadingInitial | Self::Refreshing)
    }
}

// ---------------------------------------------------------------------------
// ScreenLoadStateMap
// ---------------------------------------------------------------------------

/// Tracks per-screen load state.
///
/// Returns [`LoadState::Idle`] for screens that haven't been tracked.
/// Automatically removes entries set to `Idle` to prevent unbounded growth.
#[derive(Debug, Default)]
pub struct ScreenLoadStateMap {
    map: HashMap<Screen, LoadState>,
    /// Screens that have had a load state set at least once.
    visited: HashSet<Screen>,
}

impl ScreenLoadStateMap {
    /// Get the load state for a screen (defaults to `Idle`).
    #[must_use]
    pub fn get(&self, screen: &Screen) -> &LoadState {
        static IDLE: LoadState = LoadState::Idle;
        self.map.get(screen).unwrap_or(&IDLE)
    }

    /// Set the load state for a screen.
    ///
    /// Setting to `Idle` removes the entry to prevent map growth.
    pub fn set(&mut self, screen: Screen, state: LoadState) {
        self.visited.insert(screen.clone());
        if state == LoadState::Idle {
            self.map.remove(&screen);
        } else {
            self.map.insert(screen, state);
        }
    }

    /// Whether this screen has ever had a load initiated.
    #[must_use]
    pub fn was_visited(&self, screen: &Screen) -> bool {
        self.visited.contains(screen)
    }

    /// Whether any screen is currently loading.
    #[must_use]
    pub fn any_loading(&self) -> bool {
        self.map.values().any(LoadState::is_loading)
    }
}

// ---------------------------------------------------------------------------
// ScreenIntent
// ---------------------------------------------------------------------------

/// Pure return type from screen state handlers.
///
/// State handlers must never spawn async work directly — they return
/// an intent that [`LoreApp`] interprets and dispatches through the
/// [`TaskSupervisor`].
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum ScreenIntent {
    /// No action needed.
    None,
    /// Navigate to a new screen.
    Navigate(Screen),
    /// Screen data needs re-querying (e.g., filter changed).
    RequeryNeeded(Screen),
}

// ---------------------------------------------------------------------------
// ScopeContext
// ---------------------------------------------------------------------------

/// Global scope filters applied across all screens.
///
/// When a project filter is active, all data queries scope to that
/// project. The TUI shows the active scope in the status bar.
#[derive(Debug, Default)]
pub struct ScopeContext {
    /// Active project filter (project_id).
    pub project_id: Option<i64>,
    /// Human-readable project name for display.
    pub project_name: Option<String>,
}

// ---------------------------------------------------------------------------
// AppState
// ---------------------------------------------------------------------------

/// Top-level state composition for the TUI.
///
/// Each field holds one screen's state. State is preserved when
/// navigating away and restored on return.
#[derive(Debug, Default)]
pub struct AppState {
    // Per-screen states.
    pub bootstrap: BootstrapState,
    pub dashboard: DashboardState,
    pub doctor: DoctorState,
    pub issue_list: IssueListState,
    pub issue_detail: IssueDetailState,
    pub mr_list: MrListState,
    pub mr_detail: MrDetailState,
    pub search: SearchState,
    pub stats: StatsState,
    pub timeline: TimelineState,
    pub who: WhoState,
    pub trace: TraceState,
    pub file_history: FileHistoryState,
    pub sync: SyncState,
    pub command_palette: CommandPaletteState,
    pub scope_picker: ScopePickerState,

    // Cross-cutting state.
    pub global_scope: ScopeContext,
    pub load_state: ScreenLoadStateMap,
    pub error_toast: Option<String>,
    pub show_help: bool,
    pub terminal_size: (u16, u16),
}

impl AppState {
    /// Set a screen's load state.
    pub fn set_loading(&mut self, screen: Screen, state: LoadState) {
        self.load_state.set(screen, state);
    }

    /// Set the global error toast.
    pub fn set_error(&mut self, msg: String) {
        self.error_toast = Some(msg);
    }

    /// Clear the global error toast.
    pub fn clear_error(&mut self) {
        self.error_toast = None;
    }

    /// Whether any text input is currently focused.
    #[must_use]
    pub fn has_text_focus(&self) -> bool {
        self.issue_list.filter_focused
            || self.mr_list.filter_focused
            || self.search.query_focused
            || self.command_palette.query_focused
            || self.who.has_text_focus()
            || self.trace.has_text_focus()
            || self.file_history.has_text_focus()
    }

    /// Remove focus from all text inputs.
    pub fn blur_text_focus(&mut self) {
        self.issue_list.filter_focused = false;
        self.mr_list.filter_focused = false;
        self.search.query_focused = false;
        self.command_palette.query_focused = false;
        self.who.blur();
        self.trace.blur();
        self.file_history.blur();
    }
}

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

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

    #[test]
    fn test_load_state_default_idle() {
        let map = ScreenLoadStateMap::default();
        assert_eq!(*map.get(&Screen::Dashboard), LoadState::Idle);
        assert_eq!(*map.get(&Screen::IssueList), LoadState::Idle);
    }

    #[test]
    fn test_load_state_set_and_get() {
        let mut map = ScreenLoadStateMap::default();
        map.set(Screen::Dashboard, LoadState::LoadingInitial);
        assert_eq!(*map.get(&Screen::Dashboard), LoadState::LoadingInitial);
        assert_eq!(*map.get(&Screen::IssueList), LoadState::Idle);
    }

    #[test]
    fn test_load_state_set_idle_removes_entry() {
        let mut map = ScreenLoadStateMap::default();
        map.set(Screen::Dashboard, LoadState::Refreshing);
        assert_eq!(map.map.len(), 1);

        map.set(Screen::Dashboard, LoadState::Idle);
        assert_eq!(map.map.len(), 0);
        assert_eq!(*map.get(&Screen::Dashboard), LoadState::Idle);
    }

    #[test]
    fn test_any_loading() {
        let mut map = ScreenLoadStateMap::default();
        assert!(!map.any_loading());

        map.set(Screen::Dashboard, LoadState::LoadingInitial);
        assert!(map.any_loading());

        map.set(Screen::Dashboard, LoadState::Error("oops".into()));
        assert!(!map.any_loading());
    }

    #[test]
    fn test_load_state_is_loading() {
        assert!(!LoadState::Idle.is_loading());
        assert!(LoadState::LoadingInitial.is_loading());
        assert!(LoadState::Refreshing.is_loading());
        assert!(!LoadState::Error("x".into()).is_loading());
    }

    #[test]
    fn test_app_state_default_compiles() {
        let state = AppState::default();
        assert!(!state.show_help);
        assert!(state.error_toast.is_none());
        assert_eq!(state.terminal_size, (0, 0));
    }

    #[test]
    fn test_app_state_set_error_and_clear() {
        let mut state = AppState::default();
        state.set_error("db busy".into());
        assert_eq!(state.error_toast.as_deref(), Some("db busy"));

        state.clear_error();
        assert!(state.error_toast.is_none());
    }

    #[test]
    fn test_app_state_has_text_focus() {
        let mut state = AppState::default();
        assert!(!state.has_text_focus());

        state.search.query_focused = true;
        assert!(state.has_text_focus());
    }

    #[test]
    fn test_app_state_blur_text_focus() {
        let mut state = AppState::default();
        state.issue_list.filter_focused = true;
        state.mr_list.filter_focused = true;
        state.search.query_focused = true;
        state.command_palette.query_focused = true;

        state.blur_text_focus();

        assert!(!state.has_text_focus());
        assert!(!state.issue_list.filter_focused);
        assert!(!state.mr_list.filter_focused);
        assert!(!state.search.query_focused);
        assert!(!state.command_palette.query_focused);
    }

    #[test]
    fn test_app_state_set_loading() {
        let mut state = AppState::default();
        state.set_loading(Screen::IssueList, LoadState::Refreshing);
        assert_eq!(
            *state.load_state.get(&Screen::IssueList),
            LoadState::Refreshing
        );
    }

    #[test]
    fn test_screen_intent_variants() {
        let none = ScreenIntent::None;
        let nav = ScreenIntent::Navigate(Screen::IssueList);
        let requery = ScreenIntent::RequeryNeeded(Screen::Search);

        assert_eq!(none, ScreenIntent::None);
        assert_eq!(nav, ScreenIntent::Navigate(Screen::IssueList));
        assert_eq!(requery, ScreenIntent::RequeryNeeded(Screen::Search));
    }

    #[test]
    fn test_scope_context_default() {
        let scope = ScopeContext::default();
        assert!(scope.project_id.is_none());
        assert!(scope.project_name.is_none());
    }
}
