#![allow(dead_code)] // Phase 0: types defined now, consumed in Phase 1+

//! Core types for the lore-tui Elm architecture.
//!
//! - [`Msg`] — every user action and async result flows through this enum.
//! - [`Screen`] — navigation targets.
//! - [`EntityKey`] — safe cross-project entity identity.
//! - [`AppError`] — structured error display in the TUI.
//! - [`InputMode`] — controls key dispatch routing.

use std::fmt;

use chrono::{DateTime, Utc};
use ftui::Event;

// ---------------------------------------------------------------------------
// EntityKind
// ---------------------------------------------------------------------------

/// Distinguishes issue vs merge request in an [`EntityKey`].
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, PartialOrd, Ord)]
pub enum EntityKind {
    Issue,
    MergeRequest,
}

// ---------------------------------------------------------------------------
// EntityKey
// ---------------------------------------------------------------------------

/// Uniquely identifies an entity (issue or MR) across projects.
///
/// Bare `iid` is unsafe in multi-project datasets — equality requires
/// project_id + iid + kind.
#[derive(Debug, Clone, PartialEq, Eq, Hash)]
pub struct EntityKey {
    pub project_id: i64,
    pub iid: i64,
    pub kind: EntityKind,
}

impl EntityKey {
    #[must_use]
    pub fn issue(project_id: i64, iid: i64) -> Self {
        Self {
            project_id,
            iid,
            kind: EntityKind::Issue,
        }
    }

    #[must_use]
    pub fn mr(project_id: i64, iid: i64) -> Self {
        Self {
            project_id,
            iid,
            kind: EntityKind::MergeRequest,
        }
    }
}

impl fmt::Display for EntityKey {
    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
        let prefix = match self.kind {
            EntityKind::Issue => "#",
            EntityKind::MergeRequest => "!",
        };
        write!(f, "p{}:{}{}", self.project_id, prefix, self.iid)
    }
}

// ---------------------------------------------------------------------------
// Screen
// ---------------------------------------------------------------------------

/// Navigation targets within the TUI.
#[derive(Debug, Clone, PartialEq, Eq, Hash)]
pub enum Screen {
    Dashboard,
    IssueList,
    IssueDetail(EntityKey),
    MrList,
    MrDetail(EntityKey),
    Search,
    Timeline,
    Who,
    Trace,
    FileHistory,
    Sync,
    Stats,
    Doctor,
    Bootstrap,
}

impl Screen {
    /// Human-readable label for breadcrumbs and status bar.
    #[must_use]
    pub fn label(&self) -> &str {
        match self {
            Self::Dashboard => "Dashboard",
            Self::IssueList => "Issues",
            Self::IssueDetail(_) => "Issue",
            Self::MrList => "Merge Requests",
            Self::MrDetail(_) => "Merge Request",
            Self::Search => "Search",
            Self::Timeline => "Timeline",
            Self::Who => "Who",
            Self::Trace => "Trace",
            Self::FileHistory => "File History",
            Self::Sync => "Sync",
            Self::Stats => "Stats",
            Self::Doctor => "Doctor",
            Self::Bootstrap => "Bootstrap",
        }
    }

    /// Whether this screen shows a specific entity detail view.
    #[must_use]
    pub fn is_detail_or_entity(&self) -> bool {
        matches!(self, Self::IssueDetail(_) | Self::MrDetail(_))
    }
}

// ---------------------------------------------------------------------------
// AppError
// ---------------------------------------------------------------------------

/// Structured error types for user-facing display in the TUI.
#[derive(Debug, Clone)]
pub enum AppError {
    /// Database is busy (WAL contention).
    DbBusy,
    /// Database corruption detected.
    DbCorruption(String),
    /// GitLab rate-limited; retry after N seconds (if header present).
    NetworkRateLimited { retry_after_secs: Option<u64> },
    /// Network unavailable.
    NetworkUnavailable,
    /// GitLab authentication failed.
    AuthFailed,
    /// Data parsing error.
    ParseError(String),
    /// Internal / unexpected error.
    Internal(String),
}

impl fmt::Display for AppError {
    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
        match self {
            Self::DbBusy => write!(f, "Database is busy — another process holds the lock"),
            Self::DbCorruption(detail) => write!(f, "Database corruption: {detail}"),
            Self::NetworkRateLimited {
                retry_after_secs: Some(secs),
            } => write!(f, "Rate limited by GitLab — retry in {secs}s"),
            Self::NetworkRateLimited {
                retry_after_secs: None,
            } => write!(f, "Rate limited by GitLab — try again shortly"),
            Self::NetworkUnavailable => write!(f, "Network unavailable — working offline"),
            Self::AuthFailed => write!(f, "GitLab authentication failed — check your token"),
            Self::ParseError(detail) => write!(f, "Parse error: {detail}"),
            Self::Internal(detail) => write!(f, "Internal error: {detail}"),
        }
    }
}

// ---------------------------------------------------------------------------
// InputMode
// ---------------------------------------------------------------------------

/// Controls how keystrokes are routed through the key dispatch pipeline.
#[derive(Debug, Clone, Default)]
pub enum InputMode {
    /// Standard navigation mode — keys dispatch to screen-specific handlers.
    #[default]
    Normal,
    /// Text input focused (filter bar, search box).
    Text,
    /// Command palette is open.
    Palette,
    /// "g" prefix pressed — waiting for second key (500ms timeout).
    GoPrefix { started_at: DateTime<Utc> },
}

// ---------------------------------------------------------------------------
// Msg
// ---------------------------------------------------------------------------

/// Every user action and async result flows through this enum.
///
/// Generation fields (`generation: u64`) on async result variants enable
/// stale-response detection: if the generation doesn't match the current
/// request generation, the result is silently dropped.
#[derive(Debug)]
pub enum Msg {
    // --- Terminal events ---
    /// Raw terminal event (key, mouse, paste, focus, clipboard).
    RawEvent(Event),
    /// Periodic tick from runtime subscription.
    Tick,
    /// Terminal resized.
    Resize {
        width: u16,
        height: u16,
    },

    // --- Navigation ---
    /// Navigate to a specific screen.
    NavigateTo(Screen),
    /// Go back in navigation history.
    GoBack,
    /// Go forward in navigation history.
    GoForward,
    /// Jump to the dashboard.
    GoHome,
    /// Jump back N screens in history.
    JumpBack(usize),
    /// Jump forward N screens in history.
    JumpForward(usize),

    // --- Command palette ---
    OpenCommandPalette,
    CloseCommandPalette,
    CommandPaletteInput(String),
    CommandPaletteSelect(String),

    // --- Issue list ---
    IssueListLoaded {
        generation: u64,
        page: crate::state::issue_list::IssueListPage,
    },
    IssueListFilterChanged(String),
    IssueListSortChanged,
    IssueSelected(EntityKey),

    // --- MR list ---
    MrListLoaded {
        generation: u64,
        page: crate::state::mr_list::MrListPage,
    },
    MrListFilterChanged(String),
    MrSelected(EntityKey),

    // --- Issue detail ---
    IssueDetailLoaded {
        generation: u64,
        key: EntityKey,
        data: Box<crate::state::issue_detail::IssueDetailData>,
    },

    // --- MR detail ---
    MrDetailLoaded {
        generation: u64,
        key: EntityKey,
        data: Box<crate::state::mr_detail::MrDetailData>,
    },

    // --- Discussions (shared by issue + MR detail) ---
    DiscussionsLoaded {
        generation: u64,
        key: EntityKey,
        discussions: Vec<crate::view::common::discussion_tree::DiscussionNode>,
    },

    // --- Search ---
    SearchQueryChanged(String),
    SearchRequestStarted {
        generation: u64,
        query: String,
    },
    SearchExecuted {
        generation: u64,
        results: Vec<SearchResult>,
    },
    SearchResultSelected(EntityKey),
    SearchModeChanged,
    SearchCapabilitiesLoaded,

    // --- Timeline ---
    TimelineLoaded {
        generation: u64,
        events: Vec<TimelineEvent>,
    },
    TimelineEntitySelected(EntityKey),

    // --- Who (people) ---
    WhoResultLoaded {
        generation: u64,
        result: Box<WhoResult>,
    },
    WhoModeChanged,

    // --- Trace ---
    TraceResultLoaded {
        generation: u64,
        result: Box<lore::core::trace::TraceResult>,
    },
    TraceKnownPathsLoaded {
        paths: Vec<String>,
    },

    // --- File History ---
    FileHistoryLoaded {
        generation: u64,
        result: Box<crate::state::file_history::FileHistoryResult>,
    },
    FileHistoryKnownPathsLoaded {
        paths: Vec<String>,
    },

    // --- Scope ---
    /// Projects loaded for the scope picker.
    ScopeProjectsLoaded {
        projects: Vec<crate::scope::ProjectInfo>,
    },

    // --- Doctor ---
    DoctorLoaded {
        checks: Vec<crate::state::doctor::HealthCheck>,
    },

    // --- Stats ---
    StatsLoaded {
        data: crate::state::stats::StatsData,
    },

    // --- Sync ---
    SyncStarted,
    SyncProgress {
        stage: String,
        current: u64,
        total: u64,
    },
    SyncProgressBatch {
        stage: String,
        batch_size: u64,
    },
    SyncLogLine(String),
    SyncBackpressureDrop,
    SyncCompleted {
        elapsed_ms: u64,
    },
    SyncCancelled,
    SyncFailed(String),
    SyncStreamStats {
        bytes: u64,
        items: u64,
    },

    // --- Search debounce ---
    SearchDebounceArmed {
        generation: u64,
    },
    SearchDebounceFired {
        generation: u64,
    },

    // --- Dashboard ---
    DashboardLoaded {
        generation: u64,
        data: Box<crate::state::dashboard::DashboardData>,
    },

    // --- Global actions ---
    Error(AppError),
    ShowHelp,
    ShowCliEquivalent,
    OpenInBrowser,
    BlurTextInput,
    ScrollToTopCurrentScreen,
    Quit,
}

impl Msg {
    /// Return the variant name as a static string without formatting payload.
    ///
    /// Used by crash context to cheaply record which message was dispatched.
    pub fn variant_name(&self) -> &'static str {
        match self {
            Self::RawEvent(_) => "RawEvent",
            Self::Tick => "Tick",
            Self::Resize { .. } => "Resize",
            Self::NavigateTo(_) => "NavigateTo",
            Self::GoBack => "GoBack",
            Self::GoForward => "GoForward",
            Self::GoHome => "GoHome",
            Self::JumpBack(_) => "JumpBack",
            Self::JumpForward(_) => "JumpForward",
            Self::OpenCommandPalette => "OpenCommandPalette",
            Self::CloseCommandPalette => "CloseCommandPalette",
            Self::CommandPaletteInput(_) => "CommandPaletteInput",
            Self::CommandPaletteSelect(_) => "CommandPaletteSelect",
            Self::IssueListLoaded { .. } => "IssueListLoaded",
            Self::IssueListFilterChanged(_) => "IssueListFilterChanged",
            Self::IssueListSortChanged => "IssueListSortChanged",
            Self::IssueSelected(_) => "IssueSelected",
            Self::MrListLoaded { .. } => "MrListLoaded",
            Self::MrListFilterChanged(_) => "MrListFilterChanged",
            Self::MrSelected(_) => "MrSelected",
            Self::IssueDetailLoaded { .. } => "IssueDetailLoaded",
            Self::MrDetailLoaded { .. } => "MrDetailLoaded",
            Self::DiscussionsLoaded { .. } => "DiscussionsLoaded",
            Self::SearchQueryChanged(_) => "SearchQueryChanged",
            Self::SearchRequestStarted { .. } => "SearchRequestStarted",
            Self::SearchExecuted { .. } => "SearchExecuted",
            Self::SearchResultSelected(_) => "SearchResultSelected",
            Self::SearchModeChanged => "SearchModeChanged",
            Self::SearchCapabilitiesLoaded => "SearchCapabilitiesLoaded",
            Self::TimelineLoaded { .. } => "TimelineLoaded",
            Self::TimelineEntitySelected(_) => "TimelineEntitySelected",
            Self::WhoResultLoaded { .. } => "WhoResultLoaded",
            Self::WhoModeChanged => "WhoModeChanged",
            Self::TraceResultLoaded { .. } => "TraceResultLoaded",
            Self::TraceKnownPathsLoaded { .. } => "TraceKnownPathsLoaded",
            Self::FileHistoryLoaded { .. } => "FileHistoryLoaded",
            Self::FileHistoryKnownPathsLoaded { .. } => "FileHistoryKnownPathsLoaded",
            Self::ScopeProjectsLoaded { .. } => "ScopeProjectsLoaded",
            Self::DoctorLoaded { .. } => "DoctorLoaded",
            Self::StatsLoaded { .. } => "StatsLoaded",
            Self::SyncStarted => "SyncStarted",
            Self::SyncProgress { .. } => "SyncProgress",
            Self::SyncProgressBatch { .. } => "SyncProgressBatch",
            Self::SyncLogLine(_) => "SyncLogLine",
            Self::SyncBackpressureDrop => "SyncBackpressureDrop",
            Self::SyncCompleted { .. } => "SyncCompleted",
            Self::SyncCancelled => "SyncCancelled",
            Self::SyncFailed(_) => "SyncFailed",
            Self::SyncStreamStats { .. } => "SyncStreamStats",
            Self::SearchDebounceArmed { .. } => "SearchDebounceArmed",
            Self::SearchDebounceFired { .. } => "SearchDebounceFired",
            Self::DashboardLoaded { .. } => "DashboardLoaded",
            Self::Error(_) => "Error",
            Self::ShowHelp => "ShowHelp",
            Self::ShowCliEquivalent => "ShowCliEquivalent",
            Self::OpenInBrowser => "OpenInBrowser",
            Self::BlurTextInput => "BlurTextInput",
            Self::ScrollToTopCurrentScreen => "ScrollToTopCurrentScreen",
            Self::Quit => "Quit",
        }
    }
}

/// Convert terminal events into messages.
///
/// FrankenTUI requires `From<Event>` on the message type so the runtime
/// can inject terminal events into the model's update loop.
impl From<Event> for Msg {
    fn from(event: Event) -> Self {
        match event {
            Event::Resize { width, height } => Self::Resize { width, height },
            Event::Tick => Self::Tick,
            other => Self::RawEvent(other),
        }
    }
}

// ---------------------------------------------------------------------------
// Placeholder data types (will be fleshed out in Phase 1+)
// ---------------------------------------------------------------------------

/// Placeholder for issue detail payload.
#[derive(Debug, Clone)]
pub struct IssueDetail {
    pub key: EntityKey,
    pub title: String,
    pub description: String,
}

/// Placeholder for MR detail payload.
#[derive(Debug, Clone)]
pub struct MrDetail {
    pub key: EntityKey,
    pub title: String,
    pub description: String,
}

/// Placeholder for a discussion thread.
#[derive(Debug, Clone)]
pub struct Discussion {
    pub id: String,
    pub notes: Vec<String>,
}

// ---------------------------------------------------------------------------
// SearchMode
// ---------------------------------------------------------------------------

/// Search mode determines which backend index is used.
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Default)]
pub enum SearchMode {
    /// FTS5 only — fast, always available if documents are indexed.
    #[default]
    Lexical,
    /// FTS5 + vector RRF merge — best quality when embeddings exist.
    Hybrid,
    /// Vector-only cosine similarity — requires Ollama embeddings.
    Semantic,
}

impl SearchMode {
    /// Short label for the mode indicator in the query bar.
    #[must_use]
    pub fn label(self) -> &'static str {
        match self {
            Self::Lexical => "FTS",
            Self::Hybrid => "Hybrid",
            Self::Semantic => "Vec",
        }
    }

    /// Cycle to the next mode, wrapping around.
    #[must_use]
    pub fn next(self) -> Self {
        match self {
            Self::Lexical => Self::Hybrid,
            Self::Hybrid => Self::Semantic,
            Self::Semantic => Self::Lexical,
        }
    }
}

impl std::fmt::Display for SearchMode {
    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
        f.write_str(self.label())
    }
}

/// A search result from the local database.
#[derive(Debug, Clone)]
pub struct SearchResult {
    pub key: EntityKey,
    pub title: String,
    pub score: f64,
    pub snippet: String,
    pub project_path: String,
}

// ---------------------------------------------------------------------------
// TimelineEventKind
// ---------------------------------------------------------------------------

/// Event kind for color coding in the TUI timeline.
///
/// Derived from raw resource event tables in the local database.
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
pub enum TimelineEventKind {
    /// Entity was created.
    Created,
    /// State changed (opened/closed/reopened/locked).
    StateChanged,
    /// Label added to entity.
    LabelAdded,
    /// Label removed from entity.
    LabelRemoved,
    /// Milestone set on entity.
    MilestoneSet,
    /// Milestone removed from entity.
    MilestoneRemoved,
    /// Merge request was merged.
    Merged,
}

impl TimelineEventKind {
    /// Short display label for the event kind badge.
    #[must_use]
    pub fn label(self) -> &'static str {
        match self {
            Self::Created => "Created",
            Self::StateChanged => "State",
            Self::LabelAdded => "+Label",
            Self::LabelRemoved => "-Label",
            Self::MilestoneSet => "+Mile",
            Self::MilestoneRemoved => "-Mile",
            Self::Merged => "Merged",
        }
    }
}

// ---------------------------------------------------------------------------
// TimelineEvent
// ---------------------------------------------------------------------------

/// A timeline event for TUI display.
///
/// Produced by [`crate::action::fetch_timeline_events`] from raw
/// resource event tables. Contains enough data for the view to
/// render color-coded events with navigable entity references.
#[derive(Debug, Clone)]
pub struct TimelineEvent {
    /// Epoch milliseconds (UTC).
    pub timestamp_ms: i64,
    /// Entity this event belongs to (for navigation).
    pub entity_key: EntityKey,
    /// Event kind for color coding.
    pub event_kind: TimelineEventKind,
    /// Human-readable summary (e.g., "State changed to closed").
    pub summary: String,
    /// Optional detail text (e.g., label name, new state value).
    pub detail: Option<String>,
    /// Who performed the action.
    pub actor: Option<String>,
    /// Project path for display (e.g., "group/project").
    pub project_path: String,
}

// WhoResult is re-exported from the lore core crate.
pub use lore::core::who_types::WhoResult;

// DashboardData moved to crate::state::dashboard (enriched with
// EntityCounts, ProjectSyncInfo, RecentActivityItem, LastSyncInfo).

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

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

    #[test]
    fn test_entity_key_equality() {
        assert_eq!(EntityKey::issue(1, 42), EntityKey::issue(1, 42));
        assert_ne!(EntityKey::issue(1, 42), EntityKey::mr(1, 42));
    }

    #[test]
    fn test_entity_key_different_projects() {
        assert_ne!(EntityKey::issue(1, 42), EntityKey::issue(2, 42));
    }

    #[test]
    fn test_entity_key_display() {
        assert_eq!(EntityKey::issue(5, 123).to_string(), "p5:#123");
        assert_eq!(EntityKey::mr(5, 456).to_string(), "p5:!456");
    }

    #[test]
    fn test_entity_key_hash_is_usable_in_collections() {
        use std::collections::HashSet;
        let mut set = HashSet::new();
        set.insert(EntityKey::issue(1, 1));
        set.insert(EntityKey::issue(1, 1)); // duplicate
        set.insert(EntityKey::mr(1, 1));
        assert_eq!(set.len(), 2);
    }

    #[test]
    fn test_screen_labels() {
        assert_eq!(Screen::Dashboard.label(), "Dashboard");
        assert_eq!(Screen::IssueList.label(), "Issues");
        assert_eq!(Screen::MrList.label(), "Merge Requests");
        assert_eq!(Screen::Search.label(), "Search");
    }

    #[test]
    fn test_screen_is_detail_or_entity() {
        assert!(Screen::IssueDetail(EntityKey::issue(1, 1)).is_detail_or_entity());
        assert!(Screen::MrDetail(EntityKey::mr(1, 1)).is_detail_or_entity());
        assert!(!Screen::Dashboard.is_detail_or_entity());
        assert!(!Screen::IssueList.is_detail_or_entity());
        assert!(!Screen::Search.is_detail_or_entity());
    }

    #[test]
    fn test_app_error_display() {
        let err = AppError::DbBusy;
        assert!(err.to_string().contains("busy"));

        let err = AppError::NetworkRateLimited {
            retry_after_secs: Some(30),
        };
        assert!(err.to_string().contains("30s"));

        let err = AppError::NetworkRateLimited {
            retry_after_secs: None,
        };
        assert!(err.to_string().contains("shortly"));

        let err = AppError::AuthFailed;
        assert!(err.to_string().contains("token"));
    }

    #[test]
    fn test_input_mode_default_is_normal() {
        assert!(matches!(InputMode::default(), InputMode::Normal));
    }

    #[test]
    fn test_msg_from_event_resize() {
        let event = Event::Resize {
            width: 80,
            height: 24,
        };
        let msg = Msg::from(event);
        assert!(matches!(
            msg,
            Msg::Resize {
                width: 80,
                height: 24
            }
        ));
    }

    #[test]
    fn test_msg_from_event_tick() {
        let msg = Msg::from(Event::Tick);
        assert!(matches!(msg, Msg::Tick));
    }

    #[test]
    fn test_msg_from_event_focus_wraps_raw() {
        let msg = Msg::from(Event::Focus(true));
        assert!(matches!(msg, Msg::RawEvent(Event::Focus(true))));
    }

    #[test]
    fn test_search_mode_labels() {
        assert_eq!(SearchMode::Lexical.label(), "FTS");
        assert_eq!(SearchMode::Hybrid.label(), "Hybrid");
        assert_eq!(SearchMode::Semantic.label(), "Vec");
    }

    #[test]
    fn test_search_mode_next_cycles() {
        assert_eq!(SearchMode::Lexical.next(), SearchMode::Hybrid);
        assert_eq!(SearchMode::Hybrid.next(), SearchMode::Semantic);
        assert_eq!(SearchMode::Semantic.next(), SearchMode::Lexical);
    }

    #[test]
    fn test_search_mode_display() {
        assert_eq!(format!("{}", SearchMode::Lexical), "FTS");
        assert_eq!(format!("{}", SearchMode::Hybrid), "Hybrid");
        assert_eq!(format!("{}", SearchMode::Semantic), "Vec");
    }

    #[test]
    fn test_search_mode_default_is_lexical() {
        assert_eq!(SearchMode::default(), SearchMode::Lexical);
    }

    // -- TimelineEventKind tests --

    #[test]
    fn test_timeline_event_kind_labels() {
        assert_eq!(TimelineEventKind::Created.label(), "Created");
        assert_eq!(TimelineEventKind::StateChanged.label(), "State");
        assert_eq!(TimelineEventKind::LabelAdded.label(), "+Label");
        assert_eq!(TimelineEventKind::LabelRemoved.label(), "-Label");
        assert_eq!(TimelineEventKind::MilestoneSet.label(), "+Mile");
        assert_eq!(TimelineEventKind::MilestoneRemoved.label(), "-Mile");
        assert_eq!(TimelineEventKind::Merged.label(), "Merged");
    }

    #[test]
    fn test_timeline_event_kind_equality() {
        assert_eq!(TimelineEventKind::Created, TimelineEventKind::Created);
        assert_ne!(TimelineEventKind::Created, TimelineEventKind::Merged);
    }
}
