use std::path::PathBuf;

use crate::picker_action::AppAction;

use git2::{BranchType, ErrorCode, ObjectType};
use hjkl_buffer::View;
use hjkl_engine::{BufferEdit, Host, Options, Settings};

use super::{App, BufferSlot};
use crate::picker_sources::{FileSourceWithOpen, RgSourceWithOpen};
use crate::syntax::BufferId;

/// Window radius (in lines) around the cursor when snapshotting a buffer
/// for the picker preview. Bounds the per-frame tree-sitter parse cost
/// so huge buffers don't stall the picker.
const BUFFER_PREVIEW_WINDOW_RADIUS: usize = 250;

/// Snapshot a window of `buf` around the cursor as a `String`, returning
/// the content, the cursor row *within that window* (0-based), and the
/// original-buffer row of the first line in the window (`window_start`).
fn snapshot_buffer_window(buf: &hjkl_buffer::View) -> (String, usize, usize) {
    let cursor_row = buf.cursor().row;
    let total = buf.row_count();
    let start = cursor_row.saturating_sub(BUFFER_PREVIEW_WINDOW_RADIUS);
    let end = (cursor_row + BUFFER_PREVIEW_WINDOW_RADIUS).min(total);
    let mut content = String::with_capacity((end - start).saturating_mul(80));
    let preview_rope = buf.rope();
    for r in start..end {
        let line = hjkl_buffer::rope_line_str(&preview_rope, r);
        content.push_str(&line);
        content.push('\n');
    }
    (content, cursor_row - start, start)
}

impl App {
    /// Open the fuzzy file picker.
    pub(crate) fn open_picker(&mut self) {
        let cwd = std::env::current_dir().unwrap_or_else(|_| PathBuf::from("."));
        let source = Box::new(FileSourceWithOpen::new(cwd));
        self.picker = Some(crate::picker::Picker::new(source));
    }

    /// Open the buffer picker over the currently open slots (special-pane
    /// slots excluded).
    pub(crate) fn open_buffer_picker(&mut self) {
        // Collect only regular slots for the picker so the explorer buffer /
        // bottom qf-dock buffer never appear in `:buffers` / buffer picker
        // results.
        let real_slots: Vec<&crate::app::BufferSlot> = (0..self.slots.len())
            .filter(|&idx| !self.slot_is_special(idx))
            .map(|idx| &self.slots[idx])
            .collect();
        let source = Box::new(crate::picker::BufferSource::new(
            &real_slots,
            |s| {
                s.filename
                    .as_ref()
                    .and_then(|p| p.to_str())
                    .unwrap_or("[No Name]")
                    .to_owned()
            },
            |s| s.dirty,
            |s| snapshot_buffer_window(s.buffer()).0,
            |s| s.filename.clone(),
            |s| snapshot_buffer_window(s.buffer()).1,
            |s| snapshot_buffer_window(s.buffer()).2,
        ));
        self.picker = Some(crate::picker::Picker::new(source));
    }

    /// Open the ripgrep content-search picker, optionally prepopulating
    /// the query with `pattern`.
    pub(crate) fn open_grep_picker(&mut self, pattern: Option<&str>) {
        let cwd = std::env::current_dir().unwrap_or_else(|_| PathBuf::from("."));
        let source = Box::new(RgSourceWithOpen::new(cwd));
        self.picker = Some(match pattern {
            Some(p) if !p.is_empty() => crate::picker::Picker::new_with_query(source, p),
            _ => crate::picker::Picker::new(source),
        });
    }

    /// Open the git-log commit picker.
    pub(crate) fn open_git_log_picker(&mut self) {
        let cwd = std::env::current_dir().unwrap_or_else(|_| PathBuf::from("."));
        let source = Box::new(crate::picker_git::GitLogPicker::new(cwd));
        self.picker = Some(crate::picker::Picker::new(source));
    }

    /// Open the git-branch picker.
    pub(crate) fn open_git_branch_picker(&mut self) {
        let cwd = std::env::current_dir().unwrap_or_else(|_| PathBuf::from("."));
        let source = Box::new(crate::picker_git::GitBranchPicker::new(cwd));
        self.picker = Some(crate::picker::Picker::new(source));
    }

    /// Open the git file-history picker for the current buffer's path.
    pub(crate) fn open_git_file_history_picker(&mut self) {
        let Some(filename) = self.active().filename.clone() else {
            self.bus.error("git: current buffer has no path");
            return;
        };

        // Resolve relative path inside the repo workdir.
        let cwd = std::env::current_dir().unwrap_or_else(|_| PathBuf::from("."));
        let abs = if filename.is_absolute() {
            filename
        } else {
            cwd.join(&filename)
        };

        // Discover repo to obtain workdir.
        let Ok(repo) = git2::Repository::discover(&abs) else {
            self.bus.error("git: not in a git repo");
            return;
        };

        let workdir = match repo.workdir() {
            Some(w) => w.to_path_buf(),
            None => {
                self.bus.error("git: bare repo — no workdir");
                return;
            }
        };

        let rel_path = match abs.strip_prefix(&workdir) {
            Ok(r) => r.to_path_buf(),
            Err(_) => {
                self.bus
                    .error("git: current buffer is outside the repo workdir");
                return;
            }
        };

        let source = Box::new(crate::picker_git::GitFileHistoryPicker::new(
            workdir, rel_path,
        ));
        self.picker = Some(crate::picker::Picker::new(source));
    }

    /// Open the git-tags picker.
    pub(crate) fn open_git_tags_picker(&mut self) {
        let cwd = std::env::current_dir().unwrap_or_else(|_| PathBuf::from("."));
        let source = Box::new(crate::picker_git::GitTagsPicker::new(cwd));
        self.picker = Some(crate::picker::Picker::new(source));
    }

    /// Open the git-remotes picker.
    pub(crate) fn open_git_remotes_picker(&mut self) {
        let cwd = std::env::current_dir().unwrap_or_else(|_| PathBuf::from("."));
        let source = Box::new(crate::picker_git::GitRemotesPicker::new(cwd));
        self.picker = Some(crate::picker::Picker::new(source));
    }

    /// Open the git-stash picker.
    pub(crate) fn open_git_stash_picker(&mut self) {
        let cwd = std::env::current_dir().unwrap_or_else(|_| PathBuf::from("."));
        let source = Box::new(crate::picker_git::GitStashPicker::new(cwd));
        self.picker = Some(crate::picker::Picker::new(source));
    }

    /// Open the git-status fuzzy picker.
    pub(crate) fn open_git_status_picker(&mut self) {
        let cwd = std::env::current_dir().unwrap_or_else(|_| PathBuf::from("."));
        let source = Box::new(crate::picker_git::GitStatusPicker::new(cwd));
        self.picker = Some(crate::picker::Picker::new(source));
    }

    /// Dismiss the picker and release the preview-pane parse it kept warm.
    ///
    /// Every picker teardown goes through here so the retained tree never
    /// outlives the picker that produced it.
    pub(crate) fn close_picker(&mut self) {
        self.picker = None;
        self.clear_preview_cache();
    }

    /// Accept the currently highlighted picker item — same as pressing Enter.
    ///
    /// Dismisses the picker and dispatches the item's action.
    pub(crate) fn picker_accept(&mut self) {
        let event = match self.picker.as_mut() {
            Some(p) => p.accept(),
            None => return,
        };
        self.close_picker();
        if let crate::picker::PickerEvent::Select(action) = event {
            self.dispatch_picker_action(action);
        }
    }

    /// Open the currently focused picker row in a horizontal split.
    ///
    /// Gets the path from the focused row, dismisses the picker, then runs
    /// `:split <path>`.
    pub(crate) fn picker_open_in_split(&mut self) {
        let Some(path) = self
            .picker
            .as_ref()
            .and_then(|p| p.path_for_visible_row(p.selected))
        else {
            self.bus.warn("picker: no path for selected item");
            return;
        };
        self.close_picker();
        // Split a regular window, not a special pane (explorer/cmdline).
        self.focus_editor_window_for_open();
        // Escape `%`/`#` so dispatch_ex doesn't expand them to other filenames.
        let s = super::ex_dispatch::escape_ex_path(&path.to_string_lossy());
        self.dispatch_ex(&format!("split {s}"));
    }

    /// Open the currently focused picker row in a vertical split.
    pub(crate) fn picker_open_in_vsplit(&mut self) {
        let Some(path) = self
            .picker
            .as_ref()
            .and_then(|p| p.path_for_visible_row(p.selected))
        else {
            self.bus.warn("picker: no path for selected item");
            return;
        };
        self.close_picker();
        // Split a regular window, not a special pane (explorer/cmdline).
        self.focus_editor_window_for_open();
        // Escape `%`/`#` so dispatch_ex doesn't expand them to other filenames.
        let s = super::ex_dispatch::escape_ex_path(&path.to_string_lossy());
        self.dispatch_ex(&format!("vsplit {s}"));
    }

    /// Open the currently focused picker row in a new tab.
    pub(crate) fn picker_open_in_tab(&mut self) {
        let Some(path) = self
            .picker
            .as_ref()
            .and_then(|p| p.path_for_visible_row(p.selected))
        else {
            self.bus.warn("picker: no path for selected item");
            return;
        };
        self.close_picker();
        // `tabnew` creates its own fresh window, but leave the CURRENT
        // tab's focus on a regular window too so returning to this tab
        // doesn't strand focus on a hijack-prone special pane.
        self.focus_editor_window_for_open();
        // Escape `%`/`#` so dispatch_ex doesn't expand them to other filenames.
        let s = super::ex_dispatch::escape_ex_path(&path.to_string_lossy());
        self.dispatch_ex(&format!("tabnew {s}"));
    }

    /// Copy the path of the currently focused picker row to the system clipboard.
    pub(crate) fn picker_copy_path(&mut self) {
        let Some(path) = self
            .picker
            .as_ref()
            .and_then(|p| p.path_for_visible_row(p.selected))
        else {
            self.bus.warn("picker: no path for selected item");
            return;
        };
        let s = path.to_string_lossy().to_string();
        self.active_editor_mut()
            .host_mut()
            .write_clipboard(s.clone());
        self.bus.info(format!("copied: {s}"));
    }

    pub(crate) fn handle_picker_key(&mut self, key: crossterm::event::KeyEvent) {
        let event = match self.picker.as_mut() {
            Some(p) => hjkl_picker_tui::handle_key(p, key),
            None => return,
        };
        match event {
            crate::picker::PickerEvent::None => {}
            crate::picker::PickerEvent::Cancel => {
                self.close_picker();
            }
            crate::picker::PickerEvent::Select(action) => {
                self.close_picker();
                self.dispatch_picker_action(action);
            }
        }
    }

    pub(crate) fn dispatch_picker_action(&mut self, action: crate::picker::PickerAction) {
        let boxed = match action {
            crate::picker::PickerAction::Custom(b) => b,
            crate::picker::PickerAction::None => return,
        };
        let app_action = match boxed.downcast::<AppAction>() {
            Ok(a) => *a,
            Err(_) => {
                self.bus.warn("picker: unknown action type");
                return;
            }
        };
        match app_action {
            AppAction::OpenPath(path) => {
                // Route the file to a regular editor window — picking from
                // a special pane (explorer focused) must not hijack it.
                self.focus_editor_window_for_open();
                let s = path.to_string_lossy().to_string();
                self.do_edit(&s, false);
            }
            AppAction::SwitchSlot(idx) => {
                if idx < self.slots.len() {
                    self.focus_editor_window_for_open();
                    self.switch_to(idx);
                }
            }
            AppAction::OpenPathAtLine(path, line) => {
                self.focus_editor_window_for_open();
                let s = path.to_string_lossy().to_string();
                self.do_edit(&s, false);
                // goto_line is 1-based and clamps to buffer length.
                if line > 0 {
                    self.active_editor_mut().goto_line(line as usize);
                    self.sync_after_engine_mutation();
                }
            }
            AppAction::ShowCommit(sha) => self.do_show_commit(&sha),
            AppAction::CheckoutBranch(name) => self.do_checkout_branch(&name),
            AppAction::CheckoutTag(name) => self.do_checkout_tag(&name),
            AppAction::FetchRemote(name) => self.do_fetch_remote(&name),
            AppAction::StashApply(idx) => self.do_stash_apply(idx),
            AppAction::StashPop(idx) => self.do_stash_pop(idx),
            AppAction::StashDrop(idx) => self.do_stash_drop(idx),
            AppAction::JumpToRowCol(row, col) => {
                self.active_editor_mut().jump_cursor(row, col);
                self.sync_after_engine_mutation();
            }
            AppAction::ApplyCodeAction(idx) => {
                // Take the action from pending_code_actions by index.
                if idx < self.pending_code_actions.len() {
                    let action = self.pending_code_actions.remove(idx);
                    self.pending_code_actions.clear();
                    let encoding = self.pending_code_actions_encoding;
                    self.apply_code_action_or_command(action, encoding);
                } else {
                    self.bus.error("E: code action index out of range");
                }
            }
            AppAction::AnvilInstall(name) => {
                self.anvil_install(&name);
            }
            AppAction::AnvilNoOp(_name) => {
                // Tool already installed — no-op from picker <CR>.
                // TODO: could show the install log popup here.
            }
        }
    }

    /// Open the anvil tool picker.
    pub(crate) fn open_anvil_picker(&mut self) {
        let Some(registry) = self.anvil_registry.as_ref() else {
            self.bus.error("anvil: registry not available");
            return;
        };
        let source = Box::new(crate::picker_sources::AnvilPickerSource::from_registry(
            registry,
        ));
        self.picker = Some(crate::picker::Picker::new(source));
    }

    pub(crate) fn do_checkout_branch(&mut self, name: &str) {
        let cwd = std::env::current_dir().unwrap_or_else(|_| PathBuf::from("."));
        let Ok(repo) = git2::Repository::discover(&cwd) else {
            self.bus.error("git: not in a repo");
            return;
        };

        // Try local first, then remote.
        let local_result = repo.find_branch(name, BranchType::Local);
        let (branch, is_remote) = match local_result {
            Ok(b) => (b, false),
            Err(ref e) if e.code() == ErrorCode::NotFound => {
                match repo.find_branch(name, BranchType::Remote) {
                    Ok(b) => (b, true),
                    Err(_) => {
                        self.bus.error(format!("git: branch '{name}' not found"));
                        return;
                    }
                }
            }
            Err(e) => {
                self.bus.error(format!("git: {e}"));
                return;
            }
        };

        let target_obj = match branch.get().peel(git2::ObjectType::Commit) {
            Ok(o) => o,
            Err(e) => {
                self.bus.error(format!("git: {e}"));
                return;
            }
        };
        let target_oid = target_obj.id();

        let tree = match branch.get().peel_to_tree() {
            Ok(t) => t,
            Err(e) => {
                self.bus.error(format!("git: {e}"));
                return;
            }
        };

        // Pre-flight: paths that switching would touch (diff HEAD tree vs target tree)
        // intersected with dirty paths in workdir/index. Refuse with friendly list
        // instead of letting libgit2's safe() return opaque Conflict (-13).
        let head_tree = repo.head().ok().and_then(|h| h.peel_to_tree().ok());
        let touched: std::collections::HashSet<String> = match head_tree.as_ref() {
            Some(ht) => match repo.diff_tree_to_tree(Some(ht), Some(&tree), None) {
                Ok(diff) => {
                    let mut set = std::collections::HashSet::new();
                    diff.foreach(
                        &mut |delta, _| {
                            if let Some(p) = delta.new_file().path().and_then(|p| p.to_str()) {
                                set.insert(p.to_string());
                            }
                            if let Some(p) = delta.old_file().path().and_then(|p| p.to_str()) {
                                set.insert(p.to_string());
                            }
                            true
                        },
                        None,
                        None,
                        None,
                    )
                    .ok();
                    set
                }
                Err(_) => std::collections::HashSet::new(),
            },
            None => std::collections::HashSet::new(),
        };

        let mut so = git2::StatusOptions::new();
        so.include_untracked(false).include_ignored(false);
        let dirty: Vec<String> = match repo.statuses(Some(&mut so)) {
            Ok(statuses) => statuses
                .iter()
                .filter(|s| !s.status().is_empty())
                .filter_map(|s| s.path().ok().map(|p| p.to_string()))
                .filter(|p| touched.contains(p))
                .collect(),
            Err(_) => Vec::new(),
        };

        if !dirty.is_empty() {
            let preview: Vec<&str> = dirty.iter().take(3).map(String::as_str).collect();
            let suffix = if dirty.len() > 3 {
                format!(", +{} more", dirty.len() - 3)
            } else {
                String::new()
            };
            self.bus.warn(format!(
                "git: uncommitted changes in {}{} — stash or commit first",
                preview.join(", "),
                suffix,
            ));
            return;
        }

        let mut cb = git2::build::CheckoutBuilder::new();
        cb.safe();
        if let Err(e) = repo.checkout_tree(tree.as_object(), Some(&mut cb)) {
            self.bus.error(format!("git: checkout failed: {e}"));
            return;
        }

        if is_remote {
            // Detached HEAD for remote branch checkouts.
            if let Err(e) = repo.set_head_detached(target_oid) {
                self.bus.error(format!("git: {e}"));
                return;
            }
        } else {
            let refname = format!("refs/heads/{name}");
            if let Err(e) = repo.set_head(&refname) {
                self.bus.error(format!("git: {e}"));
                return;
            }
        }

        self.bus.info(format!("checked out {name}"));
        // Reload non-dirty buffers whose disk file changed during checkout.
        self.checktime_all();
    }

    pub(crate) fn do_checkout_tag(&mut self, name: &str) {
        let cwd = std::env::current_dir().unwrap_or_else(|_| PathBuf::from("."));
        let Ok(repo) = git2::Repository::discover(&cwd) else {
            self.bus.error("git: not in a repo");
            return;
        };

        let refname = format!("refs/tags/{name}");
        let Ok(tag_ref) = repo.find_reference(&refname) else {
            self.bus.error(format!("git: tag '{name}' not found"));
            return;
        };

        let target_obj = match tag_ref.peel(ObjectType::Commit) {
            Ok(o) => o,
            Err(e) => {
                self.bus.error(format!("git: {e}"));
                return;
            }
        };
        let target_oid = target_obj.id();

        let tree = match target_obj.peel_to_tree() {
            Ok(t) => t,
            Err(e) => {
                self.bus.error(format!("git: {e}"));
                return;
            }
        };

        // Pre-flight conflict check: same pattern as do_checkout_branch.
        let head_tree = repo.head().ok().and_then(|h| h.peel_to_tree().ok());
        let touched: std::collections::HashSet<String> = match head_tree.as_ref() {
            Some(ht) => match repo.diff_tree_to_tree(Some(ht), Some(&tree), None) {
                Ok(diff) => {
                    let mut set = std::collections::HashSet::new();
                    diff.foreach(
                        &mut |delta, _| {
                            if let Some(p) = delta.new_file().path().and_then(|p| p.to_str()) {
                                set.insert(p.to_string());
                            }
                            if let Some(p) = delta.old_file().path().and_then(|p| p.to_str()) {
                                set.insert(p.to_string());
                            }
                            true
                        },
                        None,
                        None,
                        None,
                    )
                    .ok();
                    set
                }
                Err(_) => std::collections::HashSet::new(),
            },
            None => std::collections::HashSet::new(),
        };

        let mut so = git2::StatusOptions::new();
        so.include_untracked(false).include_ignored(false);
        let dirty: Vec<String> = match repo.statuses(Some(&mut so)) {
            Ok(statuses) => statuses
                .iter()
                .filter(|s| !s.status().is_empty())
                .filter_map(|s| s.path().ok().map(|p| p.to_string()))
                .filter(|p| touched.contains(p))
                .collect(),
            Err(_) => Vec::new(),
        };

        if !dirty.is_empty() {
            let preview: Vec<&str> = dirty.iter().take(3).map(String::as_str).collect();
            let suffix = if dirty.len() > 3 {
                format!(", +{} more", dirty.len() - 3)
            } else {
                String::new()
            };
            self.bus.warn(format!(
                "git: uncommitted changes in {}{} — stash or commit first",
                preview.join(", "),
                suffix,
            ));
            return;
        }

        let mut cb = git2::build::CheckoutBuilder::new();
        cb.safe();
        if let Err(e) = repo.checkout_tree(tree.as_object(), Some(&mut cb)) {
            self.bus.error(format!("git: checkout failed: {e}"));
            return;
        }

        if let Err(e) = repo.set_head_detached(target_oid) {
            self.bus.error(format!("git: {e}"));
            return;
        }

        self.bus
            .info(format!("checked out tag {name} (detached HEAD)"));
        self.checktime_all();
    }

    pub(crate) fn do_fetch_remote(&mut self, name: &str) {
        let cwd = std::env::current_dir().unwrap_or_else(|_| PathBuf::from("."));
        let Ok(repo) = git2::Repository::discover(&cwd) else {
            self.bus.error("git: not in a repo");
            return;
        };

        let Ok(mut remote) = repo.find_remote(name) else {
            self.bus.error(format!("git: remote '{name}' not found"));
            return;
        };

        match remote.fetch(&[] as &[&str], None, None) {
            Ok(()) => {
                self.bus.info(format!("fetched {name}"));
            }
            Err(e) => {
                self.bus.error(format!("git: fetch {name} failed — {e}"));
            }
        }
    }

    pub(crate) fn do_stash_apply(&mut self, idx: usize) {
        let cwd = std::env::current_dir().unwrap_or_else(|_| PathBuf::from("."));
        let Ok(mut repo) = git2::Repository::discover(&cwd) else {
            self.bus.error("git: not in a repo");
            return;
        };
        let mut opts = git2::StashApplyOptions::new();
        match repo.stash_apply(idx, Some(&mut opts)) {
            Ok(()) => {
                self.bus.info(format!("applied stash@{{{idx}}}"));
                self.checktime_all();
            }
            Err(e) => {
                self.bus.error(format!("stash apply conflict — {e}"));
            }
        }
    }

    pub(crate) fn do_stash_pop(&mut self, idx: usize) {
        let cwd = std::env::current_dir().unwrap_or_else(|_| PathBuf::from("."));
        let Ok(mut repo) = git2::Repository::discover(&cwd) else {
            self.bus.error("git: not in a repo");
            return;
        };
        let mut opts = git2::StashApplyOptions::new();
        match repo.stash_pop(idx, Some(&mut opts)) {
            Ok(()) => {
                self.bus.info(format!("popped stash@{{{idx}}}"));
                self.checktime_all();
            }
            Err(e) => {
                self.bus.error(format!("stash pop conflict — {e}"));
            }
        }
    }

    pub(crate) fn do_stash_drop(&mut self, idx: usize) {
        let cwd = std::env::current_dir().unwrap_or_else(|_| PathBuf::from("."));
        let Ok(mut repo) = git2::Repository::discover(&cwd) else {
            self.bus.error("git: not in a repo");
            return;
        };
        match repo.stash_drop(idx) {
            Ok(()) => {
                self.bus.info(format!("dropped stash@{{{idx}}}"));
            }
            Err(e) => {
                self.bus.error(format!("git: stash drop failed — {e}"));
            }
        }
    }

    pub(crate) fn do_show_commit(&mut self, sha: &str) {
        let Ok(repo) = git2::Repository::discover(
            std::env::current_dir().unwrap_or_else(|_| PathBuf::from(".")),
        ) else {
            self.bus.error("git: not in a repo");
            return;
        };
        let oid = match git2::Oid::from_str(sha) {
            Ok(o) => o,
            Err(e) => {
                self.bus.error(format!("git: bad sha: {e}"));
                return;
            }
        };
        let commit = match repo.find_commit(oid) {
            Ok(c) => c,
            Err(e) => {
                self.bus.error(format!("git: {e}"));
                return;
            }
        };
        let content = crate::picker_git::render_commit(&repo, &commit);
        let short_sha = &sha[..7.min(sha.len())];
        match build_scratch_slot(self.next_buffer_id, &content, &self.config) {
            Ok(slot) => {
                self.next_buffer_id += 1;
                self.slots.push(slot);
                let new_idx = self.slots.len() - 1;
                // The commit view is an ordinary buffer — route it to a
                // regular window, never the explorer/cmdline pane.
                self.focus_editor_window_for_open();
                self.switch_to(new_idx);
                self.bus.info(format!("showing commit {short_sha}"));
            }
            Err(e) => {
                self.bus.error(e);
            }
        }
    }
}

/// Build a scratch [`BufferSlot`] pre-loaded with `content`. Mirrors
/// `build_slot`'s file-read path but injects content directly instead of
/// reading from disk, avoiding a file round-trip for ephemeral commit views.
fn build_scratch_slot(
    buffer_id: BufferId,
    content: &str,
    config: &hjkl_app::config::Config,
) -> Result<BufferSlot, String> {
    let mut buffer = View::new();
    let content = content.strip_suffix('\n').unwrap_or(content);
    BufferEdit::replace_all(&mut buffer, content);
    // No manual pre-render here (#151 Stage 2b — see `build_slot`'s doc):
    // `switch_to` (the only caller's next step) runs `recompute_and_install`
    // synchronously before the first paint.

    let mut opts = Options::default();
    config.options.apply_to_options(&mut opts);
    // This slot is a read-only preview regardless of what the config says.
    opts.readonly = true;
    let mut settings = Settings::default();
    settings.apply_options(&opts);
    config.options.apply_to_settings(&mut settings);

    let mut slot = BufferSlot::new(buffer_id, buffer, settings);
    slot.snapshot_saved();
    Ok(slot)
}

impl App {
    /// Dispatch a git-picker [`crate::keymap_actions::AppAction`].
    ///
    /// Handles variants:
    ///   - GitStatus, GitLog, GitBranch, GitFileHistory
    ///   - GitStashes, GitTags, GitRemotes
    ///
    /// All arms open a fuzzy picker populated from git data. The git/picker
    /// boundary is intentional: these variants live here because their
    /// implementations (`open_git_*_picker`) are in this file.
    pub(crate) fn dispatch_git_action(&mut self, action: crate::keymap_actions::AppAction) {
        use crate::keymap_actions::AppAction;
        match action {
            AppAction::GitStatus => self.open_git_status_picker(),
            AppAction::GitLog => self.open_git_log_picker(),
            AppAction::GitBranch => self.open_git_branch_picker(),
            AppAction::GitFileHistory => self.open_git_file_history_picker(),
            AppAction::GitStashes => self.open_git_stash_picker(),
            AppAction::GitTags => self.open_git_tags_picker(),
            AppAction::GitRemotes => self.open_git_remotes_picker(),
            AppAction::GitBlameLine => self.toggle_blame_column(),
            _ => {}
        }
    }
}
