use std::path::PathBuf;
use std::time::Instant;

use crate::picker_action::AppAction;

use git2::{BranchType, ErrorCode, ObjectType};
use hjkl_buffer::Buffer;
use hjkl_engine::{BufferEdit, Editor, Host, Options};
use hjkl_engine_tui::EditorRatatuiExt;

use super::{App, BufferSlot, DiskState, STATUS_LINE_HEIGHT};
use crate::host::TuiHost;
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::Buffer) -> (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.
    pub(crate) fn open_buffer_picker(&mut self) {
        let source = Box::new(crate::picker::BufferSource::new(
            &self.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.editor.buffer()).0,
            |s| s.filename.clone(),
            |s| snapshot_buffer_window(s.editor.buffer()).1,
            |s| snapshot_buffer_window(s.editor.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 filename = match self.active().filename.clone() {
            Some(p) => p,
            None => {
                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.clone()
        } else {
            cwd.join(&filename)
        };

        // Discover repo to obtain workdir.
        let repo = match git2::Repository::discover(&abs) {
            Ok(r) => r,
            Err(_) => {
                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));
    }

    /// 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.picker = None;
        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 path = match self
            .picker
            .as_ref()
            .and_then(|p| p.path_for_visible_row(p.selected))
        {
            Some(p) => p,
            None => {
                self.bus.warn("picker: no path for selected item");
                return;
            }
        };
        self.picker = None;
        let s = path.to_string_lossy().to_string();
        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 path = match self
            .picker
            .as_ref()
            .and_then(|p| p.path_for_visible_row(p.selected))
        {
            Some(p) => p,
            None => {
                self.bus.warn("picker: no path for selected item");
                return;
            }
        };
        self.picker = None;
        let s = path.to_string_lossy().to_string();
        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 path = match self
            .picker
            .as_ref()
            .and_then(|p| p.path_for_visible_row(p.selected))
        {
            Some(p) => p,
            None => {
                self.bus.warn("picker: no path for selected item");
                return;
            }
        };
        self.picker = None;
        let s = path.to_string_lossy().to_string();
        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 path = match self
            .picker
            .as_ref()
            .and_then(|p| p.path_for_visible_row(p.selected))
        {
            Some(p) => p,
            None => {
                self.bus.warn("picker: no path for selected item");
                return;
            }
        };
        let s = path.to_string_lossy().to_string();
        self.active_mut()
            .editor
            .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.picker = None;
            }
            crate::picker::PickerEvent::Select(action) => {
                self.picker = None;
                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) => {
                let s = path.to_string_lossy().to_string();
                self.do_edit(&s, false);
            }
            AppAction::SwitchSlot(idx) => {
                if idx < self.slots.len() {
                    self.switch_to(idx);
                }
            }
            AppAction::OpenPathAtLine(path, line) => {
                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_mut().editor.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_mut().editor.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();
                    self.apply_code_action_or_command(action);
                } else {
                    self.bus.error("E: code action index out of range");
                }
            }
            AppAction::AnvilInstall(name) => {
                self.anvil_install(&name.clone());
            }
            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 registry = match self.anvil_registry.as_ref() {
            Some(r) => r,
            None => {
                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 repo = match git2::Repository::discover(&cwd) {
            Ok(r) => r,
            Err(_) => {
                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 repo = match git2::Repository::discover(&cwd) {
            Ok(r) => r,
            Err(_) => {
                self.bus.error("git: not in a repo");
                return;
            }
        };

        let refname = format!("refs/tags/{name}");
        let tag_ref = match repo.find_reference(&refname) {
            Ok(r) => r,
            Err(_) => {
                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 repo = match git2::Repository::discover(&cwd) {
            Ok(r) => r,
            Err(_) => {
                self.bus.error("git: not in a repo");
                return;
            }
        };

        let mut remote = match repo.find_remote(name) {
            Ok(r) => r,
            Err(_) => {
                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 mut repo = match git2::Repository::discover(&cwd) {
            Ok(r) => r,
            Err(_) => {
                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 mut repo = match git2::Repository::discover(&cwd) {
            Ok(r) => r,
            Err(_) => {
                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 mut repo = match git2::Repository::discover(&cwd) {
            Ok(r) => r,
            Err(_) => {
                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 repo = match git2::Repository::discover(
            std::env::current_dir().unwrap_or_else(|_| PathBuf::from(".")),
        ) {
            Ok(r) => r,
            Err(_) => {
                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(
            &mut self.syntax,
            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;
                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(
    syntax: &mut crate::syntax::SyntaxLayer,
    buffer_id: BufferId,
    content: &str,
    config: &hjkl_app::config::Config,
) -> Result<BufferSlot, String> {
    let mut buffer = Buffer::new();
    let content = content.strip_suffix('\n').unwrap_or(content);
    BufferEdit::replace_all(&mut buffer, content);

    let host = TuiHost::new();
    let opts = Options {
        expandtab: config.editor.expandtab,
        tabstop: config.editor.tab_width as u32,
        shiftwidth: config.editor.tab_width as u32,
        softtabstop: config.editor.tab_width as u32,
        readonly: true,
        ..Options::default()
    };
    let mut editor = Editor::new(buffer, host, opts);
    if let Ok(size) = crossterm::terminal::size() {
        let vp = editor.host_mut().viewport_mut();
        vp.width = size.0;
        vp.height = size.1.saturating_sub(STATUS_LINE_HEIGHT);
    }
    let _ = editor.take_content_edits();
    let _ = editor.take_content_reset();

    let (vp_top, vp_height) = {
        let vp = editor.host().viewport();
        (vp.top_row, vp.height as usize)
    };
    if let Some(out) = syntax.render_viewport(buffer_id, editor.buffer(), vp_top, vp_height) {
        editor.install_ratatui_syntax_spans(out.spans);
    }

    let mut slot = BufferSlot {
        buffer_id,
        editor,
        filename: None,
        dirty: false,
        is_new_file: false,
        is_untracked: false,
        diag_signs: Vec::new(),
        diag_signs_lsp: Vec::new(),
        lsp_diags: Vec::new(),
        last_lsp_dirty_gen: None,
        git_signs: Vec::new(),
        last_git_dirty_gen: None,
        last_git_refresh_at: Instant::now(),
        saved_hash: 0,
        saved_len: 0,
        signature_cache: None,
        disk_mtime: None,
        disk_len: None,
        disk_state: DiskState::Synced,
        swap_path: None,
        last_swap_dirty_gen: None,
    };
    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(),
            _ => {}
        }
    }
}
