//! Picker state and navigation logic.

use std::collections::HashSet;
use std::path::{Path, PathBuf};
use std::time::SystemTime;

use crate::discover;
use crate::fuzzy::Fuzzy;
use crate::refresh::{self, Thresholds};
use crate::worktree::{self, Upstream, Worktree};

use super::preview::Cache;
use super::theme::Theme;

/// Result of a single event pump in the picker's main loop.
pub enum Outcome {
    /// Keep drawing.
    Continue,
    /// User cancelled; picker should exit with `Canceled`.
    Cancel,
    /// User selected a worktree; picker should print this path.
    Select(PathBuf),
}

/// Whether the picker is listing one repo or many.
///
/// Controls layout (cross-repo gets the extra REPO column) and the
/// filter haystack (cross-repo includes the repo name).
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum Mode {
    /// Single repo: no REPO column.
    SingleRepo,
    /// Multiple repos merged from `projects.roots`.
    CrossRepo,
}

/// A single row in the picker.
#[derive(Clone)]
pub struct Entry {
    /// Enclosing repo's name, or `None` in single-repo mode.
    pub repo: Option<String>,
    /// The worktree being listed.
    pub worktree: Worktree,
    /// Pre-computed dirty-file count.
    pub dirty: usize,
    /// Pre-computed upstream tracking, if any.
    pub upstream: Option<Upstream>,
    /// `FETCH_HEAD` mtime of the enclosing repo, or `None` if never fetched.
    pub fetched_at: Option<SystemTime>,
}

/// Full picker state.
///
/// Mutated by the event-dispatch functions in [`super::events`] and
/// rendered by [`super::render::draw`].
pub struct App {
    /// All entries the user can pick from (unfiltered).
    pub entries: Vec<Entry>,
    /// Indices into [`Self::entries`] currently visible after filtering.
    pub visible: Vec<usize>,
    /// Index into [`Self::visible`] (not [`Self::entries`]) of the row
    /// the user has highlighted.
    pub selected: usize,
    /// Layout mode (see [`Mode`]).
    pub mode: Mode,
    /// Whether the `/` filter input is currently capturing keystrokes.
    pub filter_active: bool,
    /// Whether the `?` help overlay is shown.
    pub help_visible: bool,
    /// Fuzzy-scorer state (`nucleo_matcher`-backed).
    pub fuzzy: Fuzzy,
    /// Lazy preview cache (commits + diffstat per worktree path).
    pub preview: Cache,
    /// Resolved colour palette.
    pub theme: Theme,
    /// Resolved freshness thresholds for the FETCHED column.
    pub thresholds: Thresholds,
}

impl App {
    #[must_use]
    pub fn new(entries: Vec<Entry>, mode: Mode, theme: Theme, thresholds: Thresholds) -> Self {
        let visible = (0..entries.len()).collect();
        Self {
            entries,
            visible,
            selected: 0,
            mode,
            filter_active: false,
            help_visible: false,
            fuzzy: Fuzzy::new(),
            preview: Cache::default(),
            theme,
            thresholds,
        }
    }

    pub fn toggle_help(&mut self) {
        self.help_visible = !self.help_visible;
    }

    #[must_use]
    pub fn selected_entry(&self) -> Option<&Entry> {
        self.visible
            .get(self.selected)
            .and_then(|ix| self.entries.get(*ix))
    }

    #[must_use]
    pub fn selected_path(&self) -> Option<PathBuf> {
        self.selected_entry().map(|e| e.worktree.path.clone())
    }

    pub fn move_up(&mut self) {
        if self.selected > 0 {
            self.selected -= 1;
        }
    }

    pub fn move_down(&mut self) {
        if !self.visible.is_empty() && self.selected + 1 < self.visible.len() {
            self.selected += 1;
        }
    }

    pub fn move_first(&mut self) {
        self.selected = 0;
    }

    pub fn move_last(&mut self) {
        if !self.visible.is_empty() {
            self.selected = self.visible.len() - 1;
        }
    }

    pub fn page_up(&mut self, size: usize) {
        self.selected = self.selected.saturating_sub(size.max(1));
    }

    pub fn page_down(&mut self, size: usize) {
        if self.visible.is_empty() {
            return;
        }
        let max = self.visible.len() - 1;
        self.selected = (self.selected + size.max(1)).min(max);
    }

    /// Re-fetches the highlighted row's repo and updates `fetched_at`
    /// for every Entry that belongs to it.
    ///
    /// Blocking. Tier 1 ships sync; Tier 3 will run this in a
    /// background thread with a spinner.
    pub fn refresh_current(&mut self) {
        let Some(entry) = self.selected_entry().cloned() else {
            return;
        };
        let Some(anchor) = anchor_for_entry_path(&entry.worktree.path) else {
            return;
        };
        if worktree::fetch(&anchor, true).is_ok() {
            self.update_fetched_at_for_anchor(&anchor);
        }
    }

    /// Re-fetches every distinct repo represented in the visible list,
    /// in parallel where applicable, and updates `fetched_at` on each
    /// affected Entry.
    pub fn refresh_all(&mut self) {
        let mut anchors: Vec<PathBuf> = Vec::new();
        let mut seen: HashSet<PathBuf> = HashSet::new();
        for ix in &self.visible {
            if let Some(e) = self.entries.get(*ix)
                && let Some(anchor) = anchor_for_entry_path(&e.worktree.path)
                && seen.insert(anchor.clone())
            {
                anchors.push(anchor);
            }
        }
        for anchor in &anchors {
            let _ = worktree::fetch(anchor, true);
        }
        for anchor in &anchors {
            self.update_fetched_at_for_anchor(anchor);
        }
    }

    fn update_fetched_at_for_anchor(&mut self, anchor: &Path) {
        let now = refresh::fetched_at(anchor);
        for e in &mut self.entries {
            if anchor_for_entry_path(&e.worktree.path).as_deref() == Some(anchor) {
                e.fetched_at = now;
            }
        }
    }

    pub fn open_filter(&mut self) {
        self.filter_active = true;
    }

    pub fn close_filter(&mut self, clear: bool) {
        self.filter_active = false;
        if clear {
            self.fuzzy.set_query("");
            self.rebuild_visible();
        }
    }

    pub fn filter_push(&mut self, c: char) {
        let mut q = self.fuzzy.query().to_string();
        q.push(c);
        self.fuzzy.set_query(&q);
        self.rebuild_visible();
    }

    pub fn filter_pop(&mut self) {
        let mut q = self.fuzzy.query().to_string();
        if q.pop().is_some() {
            self.fuzzy.set_query(&q);
            self.rebuild_visible();
        }
    }

    fn rebuild_visible(&mut self) {
        let query_empty = self.fuzzy.query().is_empty();
        if query_empty {
            self.visible = (0..self.entries.len()).collect();
        } else {
            let mut scored: Vec<(usize, u32)> = self
                .entries
                .iter()
                .enumerate()
                .filter_map(|(ix, e)| self.fuzzy.score(&haystack(e)).map(|s| (ix, s)))
                .collect();
            scored.sort_by_key(|b| std::cmp::Reverse(b.1));
            self.visible = scored.into_iter().map(|(ix, _)| ix).collect();
        }
        self.selected = 0;
    }
}

fn haystack(e: &Entry) -> String {
    let repo = e.repo.as_deref().unwrap_or("");
    let branch = e.worktree.branch.as_deref().unwrap_or("");
    format!("{}/{} {}", repo, e.worktree.name, branch)
}

/// Resolves the bare-clone or `.git` anchor for the repo enclosing a
/// worktree path. Walks ancestors until a directory looking like a repo
/// root is found.
fn anchor_for_entry_path(worktree_path: &Path) -> Option<PathBuf> {
    if let Some(a) = discover::anchor_for(worktree_path) {
        return Some(a);
    }
    for dir in worktree_path.ancestors().skip(1) {
        if let Some(a) = discover::anchor_for(dir) {
            return Some(a);
        }
    }
    None
}

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

    fn entry(repo: &str, name: &str) -> Entry {
        Entry {
            repo: Some(repo.into()),
            worktree: Worktree {
                path: Path::new("/tmp").join(name),
                name: name.into(),
                branch: Some(name.into()),
                head: None,
                bare: false,
                locked: false,
                locked_reason: None,
                prunable: false,
                prunable_reason: None,
            },
            dirty: 0,
            upstream: None,
            fetched_at: None,
        }
    }

    fn app(n: usize) -> App {
        App::new(
            (0..n).map(|i| entry("r", &format!("w{i}"))).collect(),
            Mode::SingleRepo,
            Theme::resolve(super::super::theme::ThemeKind::Plain, true),
            Thresholds::default(),
        )
    }

    #[test]
    fn empty_list_has_no_selection() {
        let a = App::new(
            Vec::new(),
            Mode::SingleRepo,
            Theme::resolve(super::super::theme::ThemeKind::Plain, true),
            Thresholds::default(),
        );
        assert!(a.selected_path().is_none());
    }

    #[test]
    fn move_down_clamps_at_end() {
        let mut a = app(3);
        for _ in 0..5 {
            a.move_down();
        }
        assert_eq!(a.selected, 2);
    }

    #[test]
    fn move_up_clamps_at_zero() {
        let mut a = app(3);
        a.move_down();
        for _ in 0..5 {
            a.move_up();
        }
        assert_eq!(a.selected, 0);
    }

    #[test]
    fn page_down_respects_bounds() {
        let mut a = app(5);
        a.page_down(10);
        assert_eq!(a.selected, 4);
    }

    #[test]
    fn first_last_seek() {
        let mut a = app(5);
        a.move_last();
        assert_eq!(a.selected, 4);
        a.move_first();
        assert_eq!(a.selected, 0);
    }

    #[test]
    fn filter_narrows_visible() {
        let mut a = App::new(
            vec![
                entry("r", "feat-auth"),
                entry("r", "feat-billing"),
                entry("r", "bugfix"),
            ],
            Mode::SingleRepo,
            Theme::resolve(super::super::theme::ThemeKind::Plain, true),
            Thresholds::default(),
        );
        a.filter_push('f');
        a.filter_push('e');
        a.filter_push('a');
        a.filter_push('t');
        assert_eq!(a.visible.len(), 2);
    }

    #[test]
    fn filter_empty_shows_all() {
        let mut a = app(3);
        a.filter_push('x');
        assert_eq!(a.visible.len(), 0);
        a.filter_pop();
        assert_eq!(a.visible.len(), 3);
    }

    #[test]
    fn close_filter_with_clear() {
        let mut a = app(3);
        a.open_filter();
        a.filter_push('x');
        a.close_filter(true);
        assert!(!a.filter_active);
        assert_eq!(a.visible.len(), 3);
    }
}
