//! Model trait impl and key dispatch for LoreApp.

use chrono::TimeDelta;
use ftui::{Cmd, Event, Frame, KeyCode, KeyEvent, Model, Modifiers};

use crate::crash_context::CrashEvent;
use crate::message::{InputMode, Msg, Screen};
use crate::state::LoadState;
use crate::task_supervisor::TaskKey;

use super::LoreApp;

/// Timeout for the g-prefix key sequence.
const GO_PREFIX_TIMEOUT: TimeDelta = TimeDelta::milliseconds(500);

impl LoreApp {
    // -----------------------------------------------------------------------
    // Key dispatch
    // -----------------------------------------------------------------------

    /// Normalize terminal key variants for cross-terminal consistency.
    fn normalize_key(key: &mut KeyEvent) {
        // BackTab -> Shift+Tab canonical form.
        if key.code == KeyCode::BackTab {
            key.code = KeyCode::Tab;
            key.modifiers |= Modifiers::SHIFT;
        }
    }

    /// 5-stage key dispatch pipeline.
    ///
    /// Returns the Cmd to execute (Quit, None, or a task command).
    pub(crate) fn interpret_key(&mut self, mut key: KeyEvent) -> Cmd<Msg> {
        Self::normalize_key(&mut key);

        let screen = self.navigation.current().clone();

        // Record key press in crash context.
        self.crash_context.push(CrashEvent::KeyPress {
            key: format!("{:?}", key.code),
            mode: format!("{:?}", self.input_mode),
            screen: screen.label().to_string(),
        });

        // --- Stage 1: Quit check ---
        // Ctrl+C always quits regardless of mode.
        if key.code == KeyCode::Char('c') && key.modifiers.contains(Modifiers::CTRL) {
            return Cmd::quit();
        }

        // --- Stage 2: InputMode routing ---
        match &self.input_mode {
            InputMode::Text => {
                return self.handle_text_mode_key(&key, &screen);
            }
            InputMode::Palette => {
                return self.handle_palette_mode_key(&key, &screen);
            }
            InputMode::GoPrefix { started_at } => {
                let elapsed = self.clock.now().signed_duration_since(*started_at);
                if elapsed > GO_PREFIX_TIMEOUT {
                    // Timeout expired — cancel prefix and re-process as normal.
                    self.input_mode = InputMode::Normal;
                } else {
                    return self.handle_go_prefix_key(&key, &screen);
                }
            }
            InputMode::Normal => {}
        }

        // --- Stage 3: Global shortcuts (Normal mode) ---
        // 'q' quits.
        if key.code == KeyCode::Char('q') && key.modifiers == Modifiers::NONE {
            return Cmd::quit();
        }

        // 'g' starts prefix sequence.
        if self
            .command_registry
            .is_sequence_starter(&key.code, &key.modifiers)
        {
            self.input_mode = InputMode::GoPrefix {
                started_at: self.clock.now(),
            };
            return Cmd::none();
        }

        // Registry-based single-key lookup.
        if let Some(cmd_def) =
            self.command_registry
                .lookup_key(&key.code, &key.modifiers, &screen, &self.input_mode)
        {
            return self.execute_command(cmd_def.id, &screen);
        }

        // --- Stage 4: Screen-local keys ---
        // Delegated to AppState::interpret_screen_key in future phases.

        // --- Stage 5: Fallback (unhandled) ---
        Cmd::none()
    }

    /// Handle keys in Text input mode.
    ///
    /// Only Esc and Ctrl+P pass through; everything else is consumed by
    /// the focused text widget (handled in future phases).
    fn handle_text_mode_key(&mut self, key: &KeyEvent, screen: &Screen) -> Cmd<Msg> {
        // Esc blurs the text input.
        if key.code == KeyCode::Escape {
            self.state.blur_text_focus();
            self.input_mode = InputMode::Normal;
            return Cmd::none();
        }

        // Ctrl+P opens palette even in text mode.
        if let Some(cmd_def) =
            self.command_registry
                .lookup_key(&key.code, &key.modifiers, screen, &InputMode::Text)
        {
            return self.execute_command(cmd_def.id, screen);
        }

        // All other keys consumed by text widget (future).
        Cmd::none()
    }

    /// Handle keys in Palette mode.
    fn handle_palette_mode_key(&mut self, key: &KeyEvent, screen: &Screen) -> Cmd<Msg> {
        match key.code {
            KeyCode::Escape => {
                self.state.command_palette.close();
                self.input_mode = InputMode::Normal;
                Cmd::none()
            }
            KeyCode::Enter => {
                if let Some(cmd_id) = self.state.command_palette.selected_command_id() {
                    self.state.command_palette.close();
                    self.input_mode = InputMode::Normal;
                    self.execute_command(cmd_id, screen)
                } else {
                    Cmd::none()
                }
            }
            KeyCode::Up => {
                self.state.command_palette.select_prev();
                Cmd::none()
            }
            KeyCode::Down => {
                self.state.command_palette.select_next();
                Cmd::none()
            }
            KeyCode::Backspace => {
                self.state
                    .command_palette
                    .delete_back(&self.command_registry, screen);
                Cmd::none()
            }
            KeyCode::Char(c) => {
                self.state
                    .command_palette
                    .insert_char(c, &self.command_registry, screen);
                Cmd::none()
            }
            _ => Cmd::none(),
        }
    }

    /// Handle the second key of a g-prefix sequence.
    fn handle_go_prefix_key(&mut self, key: &KeyEvent, screen: &Screen) -> Cmd<Msg> {
        self.input_mode = InputMode::Normal;

        if let Some(cmd_def) = self.command_registry.complete_sequence(
            &KeyCode::Char('g'),
            &Modifiers::NONE,
            &key.code,
            &key.modifiers,
            screen,
        ) {
            return self.execute_command(cmd_def.id, screen);
        }

        // Invalid second key — cancel prefix silently.
        Cmd::none()
    }

    /// Execute a command by ID.
    fn execute_command(&mut self, id: &str, screen: &Screen) -> Cmd<Msg> {
        match id {
            "quit" => Cmd::quit(),
            "go_back" => {
                self.navigation.pop();
                Cmd::none()
            }
            "show_help" => {
                self.state.show_help = !self.state.show_help;
                Cmd::none()
            }
            "command_palette" => {
                self.input_mode = InputMode::Palette;
                let screen = self.navigation.current().clone();
                self.state
                    .command_palette
                    .open(&self.command_registry, &screen);
                Cmd::none()
            }
            "open_in_browser" => {
                // Will dispatch OpenInBrowser msg in future phase.
                Cmd::none()
            }
            "show_cli" => {
                // Will show CLI equivalent in future phase.
                Cmd::none()
            }
            "go_home" => self.navigate_to(Screen::Dashboard),
            "go_issues" => self.navigate_to(Screen::IssueList),
            "go_mrs" => self.navigate_to(Screen::MrList),
            "go_search" => self.navigate_to(Screen::Search),
            "go_timeline" => self.navigate_to(Screen::Timeline),
            "go_who" => self.navigate_to(Screen::Who),
            "go_file_history" => self.navigate_to(Screen::FileHistory),
            "go_trace" => self.navigate_to(Screen::Trace),
            "go_doctor" => self.navigate_to(Screen::Doctor),
            "go_stats" => self.navigate_to(Screen::Stats),
            "go_sync" => {
                if screen == &Screen::Bootstrap {
                    self.state.bootstrap.sync_started = true;
                    Cmd::none()
                } else {
                    self.navigate_to(Screen::Sync)
                }
            }
            "jump_back" => {
                self.navigation.jump_back();
                Cmd::none()
            }
            "jump_forward" => {
                self.navigation.jump_forward();
                Cmd::none()
            }
            "toggle_scope" => {
                if self.state.scope_picker.visible {
                    self.state.scope_picker.close();
                    Cmd::none()
                } else {
                    // Fetch projects and open picker asynchronously.
                    Cmd::task(move || {
                        // The actual DB query runs in the task; for now, open
                        // immediately with cached projects if available.
                        Msg::ScopeProjectsLoaded { projects: vec![] }
                    })
                }
            }
            "move_down" | "move_up" | "select_item" | "focus_filter" | "scroll_to_top" => {
                // Screen-specific actions — delegated in future phases.
                Cmd::none()
            }
            _ => Cmd::none(),
        }
    }

    // -----------------------------------------------------------------------
    // Navigation helpers
    // -----------------------------------------------------------------------

    /// Navigate to a screen, pushing the nav stack and starting a data load.
    fn navigate_to(&mut self, screen: Screen) -> Cmd<Msg> {
        let screen_label = screen.label().to_string();
        let current_label = self.navigation.current().label().to_string();

        self.crash_context.push(CrashEvent::StateTransition {
            from: current_label,
            to: screen_label,
        });

        self.navigation.push(screen.clone());

        // First visit → full-screen spinner; revisit → corner spinner over stale data.
        let load_state = if self.state.load_state.was_visited(&screen) {
            LoadState::Refreshing
        } else {
            LoadState::LoadingInitial
        };
        self.state.set_loading(screen.clone(), load_state);

        // Spawn supervised task for data loading (placeholder — actual DB
        // query dispatch comes in Phase 2 screen implementations).
        let _handle = self.supervisor.submit(TaskKey::LoadScreen(screen));

        Cmd::none()
    }

    // -----------------------------------------------------------------------
    // Message dispatch (non-key)
    // -----------------------------------------------------------------------

    /// Handle non-key messages.
    pub(crate) fn handle_msg(&mut self, msg: Msg) -> Cmd<Msg> {
        // Record in crash context.
        self.crash_context.push(CrashEvent::MsgDispatched {
            msg_name: msg.variant_name().to_string(),
            screen: self.navigation.current().label().to_string(),
        });

        match msg {
            Msg::Quit => Cmd::quit(),

            // --- Navigation ---
            Msg::NavigateTo(screen) => self.navigate_to(screen),
            Msg::GoBack => {
                self.navigation.pop();
                Cmd::none()
            }
            Msg::GoForward => {
                self.navigation.go_forward();
                Cmd::none()
            }
            Msg::GoHome => self.navigate_to(Screen::Dashboard),
            Msg::JumpBack(_) => {
                self.navigation.jump_back();
                Cmd::none()
            }
            Msg::JumpForward(_) => {
                self.navigation.jump_forward();
                Cmd::none()
            }

            // --- Error ---
            Msg::Error(err) => {
                self.state.set_error(err.to_string());
                Cmd::none()
            }

            // --- Help / UI ---
            Msg::ShowHelp => {
                self.state.show_help = !self.state.show_help;
                Cmd::none()
            }
            Msg::BlurTextInput => {
                self.state.blur_text_focus();
                self.input_mode = InputMode::Normal;
                Cmd::none()
            }

            // --- Terminal ---
            Msg::Resize { width, height } => {
                self.state.terminal_size = (width, height);
                Cmd::none()
            }
            Msg::Tick => Cmd::none(),

            // --- Loaded results (stale guard) ---
            Msg::IssueListLoaded { generation, page } => {
                if self
                    .supervisor
                    .is_current(&TaskKey::LoadScreen(Screen::IssueList), generation)
                {
                    self.state.issue_list.apply_page(page);
                    self.state.set_loading(Screen::IssueList, LoadState::Idle);
                    self.supervisor
                        .complete(&TaskKey::LoadScreen(Screen::IssueList), generation);
                }
                Cmd::none()
            }
            Msg::MrListLoaded { generation, page } => {
                if self
                    .supervisor
                    .is_current(&TaskKey::LoadScreen(Screen::MrList), generation)
                {
                    self.state.mr_list.apply_page(page);
                    self.state.set_loading(Screen::MrList, LoadState::Idle);
                    self.supervisor
                        .complete(&TaskKey::LoadScreen(Screen::MrList), generation);
                }
                Cmd::none()
            }
            Msg::DashboardLoaded { generation, data } => {
                if self
                    .supervisor
                    .is_current(&TaskKey::LoadScreen(Screen::Dashboard), generation)
                {
                    self.state.dashboard.update(*data);
                    self.state.set_loading(Screen::Dashboard, LoadState::Idle);
                    self.supervisor
                        .complete(&TaskKey::LoadScreen(Screen::Dashboard), generation);
                }
                Cmd::none()
            }

            // --- Issue detail ---
            Msg::IssueDetailLoaded {
                generation,
                key,
                data,
            } => {
                let screen = Screen::IssueDetail(key.clone());
                if self
                    .supervisor
                    .is_current(&TaskKey::LoadScreen(screen.clone()), generation)
                {
                    self.state.issue_detail.apply_metadata(*data);
                    self.state.set_loading(screen.clone(), LoadState::Idle);
                    self.supervisor
                        .complete(&TaskKey::LoadScreen(screen), generation);
                }
                Cmd::none()
            }
            Msg::DiscussionsLoaded {
                generation: _,
                key,
                discussions,
            } => {
                // Progressive hydration: the parent detail task already called
                // supervisor.complete(), so is_current() would return false.
                // Instead, check that the detail state still expects this key.
                match key.kind {
                    crate::message::EntityKind::Issue => {
                        if self.state.issue_detail.current_key.as_ref() == Some(&key) {
                            self.state.issue_detail.apply_discussions(discussions);
                        }
                    }
                    crate::message::EntityKind::MergeRequest => {
                        if self.state.mr_detail.current_key.as_ref() == Some(&key) {
                            self.state.mr_detail.apply_discussions(discussions);
                        }
                    }
                }
                Cmd::none()
            }

            // --- MR detail ---
            Msg::MrDetailLoaded {
                generation,
                key,
                data,
            } => {
                let screen = Screen::MrDetail(key.clone());
                if self
                    .supervisor
                    .is_current(&TaskKey::LoadScreen(screen.clone()), generation)
                {
                    self.state.mr_detail.apply_metadata(*data);
                    self.state.set_loading(screen.clone(), LoadState::Idle);
                    self.supervisor
                        .complete(&TaskKey::LoadScreen(screen), generation);
                }
                Cmd::none()
            }

            // --- Sync lifecycle ---
            Msg::SyncStarted => {
                self.state.sync.start();
                if *self.navigation.current() == Screen::Bootstrap {
                    self.state.bootstrap.sync_started = true;
                }
                Cmd::none()
            }
            Msg::SyncProgress {
                stage,
                current,
                total,
            } => {
                self.state.sync.update_progress(&stage, current, total);
                Cmd::none()
            }
            Msg::SyncProgressBatch { stage, batch_size } => {
                self.state.sync.update_batch(&stage, batch_size);
                Cmd::none()
            }
            Msg::SyncLogLine(line) => {
                self.state.sync.add_log_line(line);
                Cmd::none()
            }
            Msg::SyncBackpressureDrop => {
                // Silently drop — the coalescer already handles throttling.
                Cmd::none()
            }
            Msg::SyncCompleted { elapsed_ms } => {
                self.state.sync.complete(elapsed_ms);

                // If we came from Bootstrap, replace nav history with Dashboard.
                if *self.navigation.current() == Screen::Bootstrap {
                    self.state.bootstrap.sync_started = false;
                    self.navigation.reset_to(Screen::Dashboard);

                    // Trigger a fresh dashboard load without preserving Bootstrap in history.
                    let dashboard = Screen::Dashboard;
                    let load_state = if self.state.load_state.was_visited(&dashboard) {
                        LoadState::Refreshing
                    } else {
                        LoadState::LoadingInitial
                    };
                    self.state.set_loading(dashboard.clone(), load_state);
                    let _handle = self.supervisor.submit(TaskKey::LoadScreen(dashboard));
                }
                Cmd::none()
            }
            Msg::SyncCancelled => {
                self.state.sync.cancel();
                Cmd::none()
            }
            Msg::SyncFailed(err) => {
                self.state.sync.fail(err);
                Cmd::none()
            }
            Msg::SyncStreamStats { bytes, items } => {
                self.state.sync.update_stream_stats(bytes, items);
                Cmd::none()
            }

            // --- Who screen ---
            Msg::WhoResultLoaded { generation, result } => {
                if self
                    .supervisor
                    .is_current(&TaskKey::LoadScreen(Screen::Who), generation)
                {
                    self.state.who.apply_results(generation, *result);
                    self.state.set_loading(Screen::Who, LoadState::Idle);
                    self.supervisor
                        .complete(&TaskKey::LoadScreen(Screen::Who), generation);
                }
                Cmd::none()
            }
            Msg::WhoModeChanged => {
                // Mode tab changed — view will re-render from state.
                Cmd::none()
            }

            // --- File History screen ---
            Msg::FileHistoryLoaded { generation, result } => {
                if self
                    .supervisor
                    .is_current(&TaskKey::LoadScreen(Screen::FileHistory), generation)
                {
                    self.state.file_history.apply_results(generation, *result);
                    self.state.set_loading(Screen::FileHistory, LoadState::Idle);
                    self.supervisor
                        .complete(&TaskKey::LoadScreen(Screen::FileHistory), generation);
                }
                Cmd::none()
            }
            Msg::FileHistoryKnownPathsLoaded { paths } => {
                self.state.file_history.known_paths = paths;
                Cmd::none()
            }

            // --- Trace screen ---
            Msg::TraceResultLoaded { generation, result } => {
                if self
                    .supervisor
                    .is_current(&TaskKey::LoadScreen(Screen::Trace), generation)
                {
                    self.state.trace.apply_result(generation, *result);
                    self.state.set_loading(Screen::Trace, LoadState::Idle);
                    self.supervisor
                        .complete(&TaskKey::LoadScreen(Screen::Trace), generation);
                }
                Cmd::none()
            }
            Msg::TraceKnownPathsLoaded { paths } => {
                self.state.trace.known_paths = paths;
                Cmd::none()
            }

            // --- Doctor ---
            Msg::DoctorLoaded { checks } => {
                self.state.doctor.apply_checks(checks);
                self.state.set_loading(Screen::Doctor, LoadState::Idle);
                Cmd::none()
            }

            // --- Stats ---
            Msg::StatsLoaded { data } => {
                self.state.stats.apply_data(data);
                self.state.set_loading(Screen::Stats, LoadState::Idle);
                Cmd::none()
            }

            // --- Timeline ---
            Msg::TimelineLoaded { generation, events } => {
                if self
                    .supervisor
                    .is_current(&TaskKey::LoadScreen(Screen::Timeline), generation)
                {
                    self.state.timeline.apply_results(generation, events);
                    self.state.set_loading(Screen::Timeline, LoadState::Idle);
                    self.supervisor
                        .complete(&TaskKey::LoadScreen(Screen::Timeline), generation);
                }
                Cmd::none()
            }

            // --- Search ---
            Msg::SearchExecuted {
                generation,
                results,
            } => {
                if self
                    .supervisor
                    .is_current(&TaskKey::LoadScreen(Screen::Search), generation)
                {
                    self.state.search.apply_results(generation, results);
                    self.state.set_loading(Screen::Search, LoadState::Idle);
                    self.supervisor
                        .complete(&TaskKey::LoadScreen(Screen::Search), generation);
                }
                Cmd::none()
            }

            // --- Scope ---
            Msg::ScopeProjectsLoaded { projects } => {
                self.state
                    .scope_picker
                    .open(projects, &self.state.global_scope);
                Cmd::none()
            }

            // All other message variants: no-op for now.
            // Future phases will fill these in as screens are implemented.
            _ => Cmd::none(),
        }
    }
}

impl Model for LoreApp {
    type Message = Msg;

    fn init(&mut self) -> Cmd<Self::Message> {
        // Install crash context panic hook.
        crate::crash_context::CrashContext::install_panic_hook(&self.crash_context);
        crate::crash_context::CrashContext::prune_crash_files();

        // Navigate to dashboard (will trigger data load in future phase).
        Cmd::none()
    }

    fn update(&mut self, msg: Self::Message) -> Cmd<Self::Message> {
        // Route raw key events through the 5-stage pipeline.
        if let Msg::RawEvent(Event::Key(key)) = msg {
            return self.interpret_key(key);
        }

        // Everything else goes through message dispatch.
        self.handle_msg(msg)
    }

    fn view(&self, frame: &mut Frame) {
        crate::view::render_screen(frame, self);
    }
}
