//! LSP-related action handlers.
//!
//! This module contains handlers for LSP actions that require complex logic,
//! such as restarting LSP servers and managing server lifecycle.

use super::Editor;
use crate::input::commands::Suggestion;
use crate::model::event::BufferId;
use crate::view::prompt::{Prompt, PromptType};
use rust_i18n::t;

impl Editor {
    /// Handle the LspRestart action.
    ///
    /// For a single-server config, restarts immediately (no prompt).
    /// For multiple servers, shows a prompt to select which server(s) to restart.
    pub fn handle_lsp_restart(&mut self) {
        // Get the language and file path from the active buffer
        let buffer_id = self.active_buffer();
        let Some(state) = self
            .windows
            .get(&self.active_window)
            .map(|w| &w.buffers)
            .expect("active window present")
            .get(&buffer_id)
        else {
            return;
        };
        let language = state.language.clone();
        let file_path = self
            .active_window()
            .buffer_metadata
            .get(&buffer_id)
            .and_then(|meta| meta.file_path().cloned());

        // Get configured servers for this language
        let configs: Vec<_> = self
            .lsp()
            .as_ref()
            .and_then(|lsp| lsp.get_configs(&language))
            .map(|c| c.to_vec())
            .unwrap_or_default();

        if configs.is_empty() {
            self.set_status_message(t!("lsp.no_server_configured").to_string());
            return;
        }

        // Single server: restart immediately without a prompt (backward compat)
        if configs.len() == 1 {
            let __active_id = self.active_window;
            let Some(lsp) = self.windows.get_mut(&__active_id).map(|w| &mut w.lsp) else {
                self.set_status_message(t!("lsp.no_manager").to_string());
                return;
            };

            let (success, message) = lsp.manual_restart(&language, file_path.as_deref());
            self.active_window_mut().status_message = Some(message);

            if success {
                self.reopen_buffers_for_language(&language);
            }
            return;
        }

        // Multiple servers: show a prompt
        let mut suggestions: Vec<Suggestion> = Vec::new();

        // Default option: restart all enabled servers
        let enabled_names: Vec<_> = configs
            .iter()
            .filter(|c| c.enabled && !c.command.is_empty())
            .map(|c| c.display_name())
            .collect();
        let all_description = if enabled_names.is_empty() {
            Some("No enabled servers".to_string())
        } else {
            Some(enabled_names.join(", "))
        };
        suggestions.push(Suggestion {
            description_spans: None,
            text: format!("{} (all enabled)", language),
            description: all_description,
            value: Some(language.clone()),
            disabled: enabled_names.is_empty(),
            keybinding: None,
            source: None,
        });

        // Individual server options
        for config in &configs {
            if config.command.is_empty() {
                continue;
            }
            let name = config.display_name();
            let status = if config.enabled { "" } else { " [disabled]" };
            suggestions.push(Suggestion {
                description_spans: None,
                text: format!("{}/{}{}", language, name, status),
                description: Some(format!("Command: {}", config.command)),
                value: Some(format!("{}/{}", language, name)),
                disabled: false,
                keybinding: None,
                source: None,
            });
        }

        // Start prompt with suggestions
        self.active_window_mut().prompt = Some(Prompt::with_suggestions(
            "Restart LSP server: ".to_string(),
            PromptType::RestartLspServer,
            suggestions.clone(),
        ));

        // Configure initial selection
        if let Some(prompt) = self.active_window_mut().prompt.as_mut() {
            prompt.selected_suggestion = Some(0);
        }
    }

    /// Send didOpen notifications for all buffers of a given language to any
    /// server handles that haven't received them yet.
    ///
    /// Called after an LSP server starts or restarts so it immediately knows
    /// about every open file (rather than waiting for the next user edit).
    pub(crate) fn reopen_buffers_for_language(&mut self, language: &str) {
        // Collect buffer info first to avoid borrow conflicts
        // Use buffer's stored language rather than detecting from path
        let buffers_for_language: Vec<_> = self
            .buffers()
            .iter()
            .filter_map(|(buf_id, state)| {
                if state.language == language {
                    self.active_window()
                        .buffer_metadata
                        .get(buf_id)
                        .and_then(|meta| meta.file_path().map(|p| (*buf_id, p.clone())))
                } else {
                    None
                }
            })
            .collect();

        let enable_inlay_hints = self.config.editor.enable_inlay_hints;

        for (buffer_id, buf_path) in buffers_for_language {
            let Some(state) = self
                .windows
                .get(&self.active_window)
                .map(|w| &w.buffers)
                .expect("active window present")
                .get(&buffer_id)
            else {
                continue;
            };

            let Some(content) = state.buffer.to_string() else {
                continue; // Skip buffers that aren't fully loaded
            };

            let Some(uri) = super::types::file_path_to_lsp_uri_with_translation(
                &buf_path,
                self.authority().path_translation.as_ref(),
            ) else {
                continue;
            };

            let lang_id = state.language.clone();
            let line_count = state.buffer.line_count().unwrap_or(1000);
            let buffer_version = state.buffer.version();

            let __active_id = self.active_window;

            if let Some(__win) = self.windows.get_mut(&__active_id) {
                let lsp = &mut __win.lsp;
                // Respect auto_start setting for this user action
                use crate::services::lsp::manager::LspSpawnResult;
                if lsp.try_spawn(&lang_id, Some(&buf_path)) != LspSpawnResult::Spawned {
                    continue;
                }

                // Collect handles that need didOpen (not yet tracked in
                // lsp_opened_with for this buffer).
                let opened_with = __win
                    .buffer_metadata
                    .get(&buffer_id)
                    .map(|m| m.lsp_opened_with.clone())
                    .unwrap_or_default();

                let handles_needing_open: Vec<(String, u64)> = lsp
                    .get_handles(&lang_id)
                    .into_iter()
                    .filter(|sh| !opened_with.contains(&sh.handle.id()))
                    .map(|sh| (sh.name.clone(), sh.handle.id()))
                    .collect();

                // Send didOpen to each handle that hasn't seen this buffer yet
                for (name, handle_id) in handles_needing_open {
                    let sh = lsp
                        .get_handles_mut(&lang_id)
                        .into_iter()
                        .find(|s| s.handle.id() == handle_id);

                    if let Some(sh) = sh {
                        if let Err(e) =
                            sh.handle
                                .did_open(uri.clone(), content.clone(), lang_id.clone())
                        {
                            tracing::warn!("LSP did_open to '{}' failed: {}", name, e);
                        } else if let Some(metadata) = __win.buffer_metadata.get_mut(&buffer_id) {
                            metadata.lsp_opened_with.insert(handle_id);
                        }
                    }
                }
            }

            // Kick off inlay hints for this buffer right after (re)opening.
            // Servers that emit a `serverQuiescent` notification (e.g.
            // rust-analyzer) will refresh these later once indexing is
            // done, but servers that don't would otherwise never get a
            // hints request unless the user edits the buffer.
            if enable_inlay_hints {
                let __active_id = self.active_window;
                if let Some(__win) = self.windows.get_mut(&__active_id) {
                    let __next_id = &mut __win.next_lsp_request_id;
                    let __pending = &mut __win.pending_inlay_hints_requests;
                    {
                        let lsp = &mut __win.lsp;
                        if let Some(sh) = lsp
                            .handle_for_feature_mut(&lang_id, crate::types::LspFeature::InlayHints)
                        {
                            let request_id = *__next_id;
                            *__next_id += 1;
                            let last_line = line_count.saturating_sub(1) as u32;
                            if let Err(e) = sh.handle.inlay_hints(
                                request_id,
                                uri.clone(),
                                0,
                                0,
                                last_line,
                                10000,
                            ) {
                                tracing::debug!(
                                    "Failed to request inlay hints for {}: {}",
                                    uri.as_str(),
                                    e
                                );
                            } else {
                                __pending.insert(
                                    request_id,
                                    super::InlayHintsRequest {
                                        buffer_id,
                                        version: buffer_version,
                                    },
                                );
                            }
                        }
                    }
                }
            }
        }
    }

    /// Handle the LspStop action.
    ///
    /// Shows a prompt to select which LSP server to stop, with suggestions
    /// for all currently running servers.
    pub fn handle_lsp_stop(&mut self) {
        let running_languages: Vec<String> = self
            .lsp()
            .as_ref()
            .map(|lsp| lsp.running_servers())
            .unwrap_or_default();

        if running_languages.is_empty() {
            self.set_status_message(t!("lsp.no_servers_running").to_string());
            return;
        }

        // Build suggestions showing server names when multiple servers per language
        let mut suggestions: Vec<Suggestion> = Vec::new();
        for lang in &running_languages {
            let server_names: Vec<String> = self
                .lsp()
                .as_ref()
                .map(|lsp| lsp.server_names_for_language(lang))
                .unwrap_or_default();

            if server_names.len() > 1 {
                // Multiple servers: show each individually
                for name in &server_names {
                    let description = Some(format!("Server: {}", name));
                    suggestions.push(Suggestion {
                        description_spans: None,
                        text: format!("{}/{}", lang, name),
                        description,
                        // Value carries "language/server_name" so the handler
                        // knows exactly which server to stop.
                        value: Some(format!("{}/{}", lang, name)),
                        disabled: false,
                        keybinding: None,
                        source: None,
                    });
                }
            } else {
                // Single server: show language only (value = just language)
                let description = self
                    .lsp()
                    .as_ref()
                    .and_then(|lsp| lsp.get_config(lang))
                    .filter(|c| !c.command.is_empty())
                    .map(|c| format!("Command: {}", c.command));

                suggestions.push(Suggestion {
                    description_spans: None,
                    text: lang.clone(),
                    description,
                    value: Some(lang.clone()),
                    disabled: false,
                    keybinding: None,
                    source: None,
                });
            }
        }

        // Start prompt with suggestions
        self.active_window_mut().prompt = Some(Prompt::with_suggestions(
            "Stop LSP server: ".to_string(),
            PromptType::StopLspServer,
            suggestions.clone(),
        ));

        // Configure initial selection
        if let Some(prompt) = self.active_window_mut().prompt.as_mut() {
            if suggestions.len() == 1 {
                // If only one entry, pre-fill the input with it
                prompt.input = suggestions[0].text.clone();
                prompt.cursor_pos = prompt.input.len();
                prompt.selected_suggestion = Some(0);
            } else if !prompt.suggestions.is_empty() {
                // Auto-select first suggestion
                prompt.selected_suggestion = Some(0);
            }
        }
    }

    /// Handle the LspToggleForBuffer action.
    ///
    /// Toggles LSP on/off for the current buffer only.
    /// Requires an LSP server to be configured for the current buffer's language.
    pub fn handle_lsp_toggle_for_buffer(&mut self) {
        let buffer_id = self.active_buffer();

        // Get the buffer's language to check if LSP is configured
        let language = {
            let Some(state) = self
                .windows
                .get(&self.active_window)
                .map(|w| &w.buffers)
                .expect("active window present")
                .get(&buffer_id)
            else {
                return;
            };
            state.language.clone()
        };

        // Check if LSP is configured for this language
        let lsp_configured = self
            .lsp()
            .as_ref()
            .and_then(|lsp| lsp.get_config(&language))
            .is_some();

        if !lsp_configured {
            self.set_status_message(t!("lsp.no_server_configured").to_string());
            return;
        }

        // Check current LSP state
        let (was_enabled, file_path) = {
            let Some(metadata) = self.active_window().buffer_metadata.get(&buffer_id) else {
                return;
            };
            (metadata.lsp_enabled, metadata.file_path().cloned())
        };

        if was_enabled {
            self.disable_lsp_for_buffer(buffer_id);
        } else {
            self.enable_lsp_for_buffer(buffer_id, &language, file_path);
        }
    }

    /// Handle an action from the LSP status details popup.
    ///
    /// Action keys have the format:
    /// - `restart:<language>/<server_name>` — restart a specific server
    /// - `start:<language>` — start LSP server(s) for a language
    /// - `stop:<language>/<server_name>` — stop a specific server
    /// - `log:<language>` — open the LSP log file for the language
    /// - `dismiss:<language>` — hide the pill for this language (dim style)
    /// - `enable:<language>` — restore a dismissed language's pill
    /// - `autostart:<language>/<server_name>` — flip auto_start=true for
    ///   the named server in config, save, and start it now
    /// - `cancel_popup` — no-op here; the row exists purely so the
    ///   user has an on-screen "Dismiss" affordance (close is handled
    ///   upstream in `handle_popup_confirm` before this is called)
    pub fn handle_lsp_status_action(&mut self, action_key: &str) {
        // Plugin-contributed rows in the LSP-Servers popup carry an
        // action_key like `plugin:{plugin_id}|{item_id}`. The popup
        // builder injects these from
        // `Window::lsp_menu_contributions`; selecting one fires the
        // standard `action_popup_result` hook so the contributing
        // plugin can react. The `popup_id` is the fixed string
        // "lsp_status" so a plugin can disambiguate from popups it
        // owns directly via `editor.showActionPopup`.
        if let Some(rest) = action_key.strip_prefix("plugin:") {
            let (plugin_id, item_id) = rest.split_once('|').unwrap_or((rest, ""));
            self.plugin_manager.read().unwrap().run_hook(
                "action_popup_result",
                crate::services::plugins::hooks::HookArgs::ActionPopupResult {
                    popup_id: "lsp_status".to_string(),
                    action_id: format!("{}|{}", plugin_id, item_id),
                },
            );
            return;
        }
        if action_key == "cancel_popup" {
            // Popup is already closed by `handle_popup_confirm`; the
            // row only exists to give the user an on-screen surface
            // that documents the Esc shortcut. Nothing to do here.
            return;
        }
        if let Some(target) = action_key.strip_prefix("autostart:") {
            // Persist `auto_start = true` in config so the server
            // starts automatically on future file opens, then kick it
            // off right away for the current session. Mirrors the
            // persisting half of the stop-server prompt path (see
            // `handle_stop_lsp_server` which sets auto_start=false).
            if let Some((language, server_name)) = target.split_once('/') {
                if let Some(lsp_configs) = self.config_mut().lsp.get_mut(language) {
                    for c in lsp_configs.as_mut_slice() {
                        if c.display_name() == server_name {
                            c.auto_start = true;
                        }
                    }
                    if let Err(e) = self.save_config() {
                        tracing::warn!(
                            "Failed to save config after enabling LSP auto-start: {}",
                            e
                        );
                    } else {
                        let config_path = self.dir_context.config_path();
                        self.emit_event(
                            "config_changed",
                            serde_json::json!({
                                "path": config_path.to_string_lossy(),
                            }),
                        );
                    }
                }

                // Start the server now so the user doesn't have to
                // re-open the file to see LSP features come alive.
                let file_path = self
                    .active_window()
                    .buffer_metadata
                    .get(&self.active_buffer())
                    .and_then(|meta| meta.file_path().cloned());
                let __active_id = self.active_window;
                if let Some(lsp) = self.windows.get_mut(&__active_id).map(|w| &mut w.lsp) {
                    let (_, message) = lsp.manual_restart(language, file_path.as_deref());
                    self.active_window_mut().status_message = Some(message);
                }
                self.reopen_buffers_for_language(language);
            }
        } else if let Some(language) = action_key.strip_prefix("start:") {
            // Start/restart LSP for this language (same as the "Start/Restart LSP" command)
            let file_path = self
                .active_window()
                .buffer_metadata
                .get(&self.active_buffer())
                .and_then(|meta| meta.file_path().cloned());

            let __active_id = self.active_window;

            if let Some(lsp) = self.windows.get_mut(&__active_id).map(|w| &mut w.lsp) {
                let (_, message) = lsp.manual_restart(language, file_path.as_deref());
                self.active_window_mut().status_message = Some(message);
            } else {
                self.active_window_mut().status_message =
                    Some("No LSP manager available".to_string());
            }
            self.reopen_buffers_for_language(language);
        } else if let Some(target) = action_key.strip_prefix("restart:") {
            // Parse language/server_name
            if let Some((language, server_name)) = target.split_once('/') {
                let file_path = self
                    .active_window()
                    .buffer_metadata
                    .get(&self.active_buffer())
                    .and_then(|meta| meta.file_path().cloned());

                let __active_id = self.active_window;

                if let Some(lsp) = self.windows.get_mut(&__active_id).map(|w| &mut w.lsp) {
                    // Shutdown the specific server first, then re-spawn
                    lsp.shutdown_server_by_name(language, server_name);
                }
                // Remove the status entry so it gets re-created on spawn
                self.active_window_mut()
                    .lsp_server_statuses
                    .remove(&(language.to_string(), server_name.to_string()));
                let __active_id = self.active_window;
                if let Some(lsp) = self.windows.get_mut(&__active_id).map(|w| &mut w.lsp) {
                    let _ = lsp.manual_restart(language, file_path.as_deref());
                }
                self.reopen_buffers_for_language(language);
                self.active_window_mut().status_message = Some(format!(
                    "Restarting LSP server: {}/{}",
                    language, server_name
                ));
            }
        } else if let Some(target) = action_key.strip_prefix("stop:") {
            if let Some((language, server_name)) = target.split_once('/') {
                // Send didClose first so the server drops documents
                // cleanly; the shared helper then shuts the handle,
                // clears lsp_server_statuses (so the status-bar pill
                // flips back off), and clears diagnostics this server
                // published. The old inline path missed the didClose
                // and the diagnostic clear.
                self.send_did_close_to_server(language, server_name);
                let stopped = self.stop_lsp_server_and_cleanup(language, Some(server_name));
                if stopped {
                    self.active_window_mut().status_message =
                        Some(format!("Stopped LSP server: {}/{}", language, server_name));
                } else {
                    self.active_window_mut().status_message = Some(format!(
                        "LSP server not running: {}/{}",
                        language, server_name
                    ));
                }
            }
        } else if let Some(language) = action_key.strip_prefix("log:") {
            let log_path = crate::services::log_dirs::lsp_log_path(language);
            if log_path.exists() {
                match self.active_window_mut().open_local_file(&log_path) {
                    Ok(buffer_id) => {
                        self.active_window_mut()
                            .mark_buffer_read_only(buffer_id, true);
                    }
                    Err(e) => {
                        self.active_window_mut().status_message =
                            Some(format!("Failed to open LSP log: {}", e));
                    }
                }
            } else {
                self.active_window_mut().status_message =
                    Some(format!("No log file found for {}", language));
            }
        } else if let Some(language) = action_key.strip_prefix("dismiss:") {
            // Persist `enabled = false` for every configured server
            // under this language so the decision survives a restart
            // — the old behaviour (just marking the language
            // dismissed in-memory) meant the next editor session
            // re-prompted the user. We keep the session-level
            // `user_dismissed_lsp_languages` flag updated too so
            // anything that still reads it (dimmed pill style, the
            // popup's Enable/Disable toggle) stays consistent with
            // the persisted state until the in-memory cache next
            // re-reads config.
            let lang = language.to_string();
            self.active_window_mut().dismiss_lsp_language(&lang);
            let mut changed = false;
            if let Some(lsp_configs) = self.config_mut().lsp.get_mut(&lang) {
                for c in lsp_configs.as_mut_slice() {
                    if c.enabled {
                        c.enabled = false;
                        changed = true;
                    }
                }
            }
            if changed {
                if let Err(e) = self.save_config() {
                    tracing::warn!("Failed to save config after disabling LSP: {}", e);
                } else {
                    let config_path = self.dir_context.config_path();
                    self.emit_event(
                        "config_changed",
                        serde_json::json!({
                            "path": config_path.to_string_lossy(),
                        }),
                    );
                }
            }

            // Stop any servers currently running for this language —
            // persisting `enabled = false` alone left the running
            // server alive (the user saw "LSP disabled" in the status
            // bar while rust-analyzer kept indexing in the background).
            // For anything else the user would have picked Stop; this
            // row's "Disable" label promises both halves. Send didClose
            // for each server we're about to drop so the server tears
            // down its document state cleanly before the handle goes
            // away.
            let running_server_names: Vec<String> = self
                .active_window()
                .lsp_server_statuses
                .iter()
                .filter_map(|((l, name), status)| {
                    if l == &lang
                        && !matches!(
                            status,
                            crate::services::async_bridge::LspServerStatus::Shutdown,
                        )
                    {
                        Some(name.clone())
                    } else {
                        None
                    }
                })
                .collect();
            for name in &running_server_names {
                self.send_did_close_to_server(&lang, name);
                self.stop_lsp_server_and_cleanup(&lang, Some(name));
            }

            self.active_window_mut().status_message = Some(format!("LSP disabled for {}.", lang));
        } else if let Some(language) = action_key.strip_prefix("enable:") {
            // Symmetric re-enable: flip `enabled = true` on every
            // configured server for this language and persist. The
            // popup's "Enable LSP for <lang>" row is the inverse of
            // the disable action, so it must undo both halves —
            // session dismissal and the on-disk flag.
            let lang = language.to_string();
            self.active_window_mut().undismiss_lsp_language(&lang);
            let mut changed = false;
            if let Some(lsp_configs) = self.config_mut().lsp.get_mut(&lang) {
                for c in lsp_configs.as_mut_slice() {
                    if !c.enabled {
                        c.enabled = true;
                        changed = true;
                    }
                }
            }
            if changed {
                if let Err(e) = self.save_config() {
                    tracing::warn!("Failed to save config after enabling LSP: {}", e);
                } else {
                    let config_path = self.dir_context.config_path();
                    self.emit_event(
                        "config_changed",
                        serde_json::json!({
                            "path": config_path.to_string_lossy(),
                        }),
                    );
                }
            }
            self.active_window_mut().status_message = Some(format!("LSP enabled for {}.", lang));
        }
    }

    // `toggle_fold_at_cursor`, `toggle_fold_at_line`, and the
    // `toggle_fold_at_byte` wrapper live on `impl Window` — call them via
    // `self.active_window_mut().toggle_fold_at_*(...)`.
}

impl crate::app::window::Window {
    /// Toggle folding at the current cursor position in the active buffer.
    pub fn toggle_fold_at_cursor(&mut self) {
        let buffer_id = self.active_buffer();
        let pos = self.active_cursors().primary().position;
        self.toggle_fold_at_byte(buffer_id, pos);
    }

    /// Toggle folding for the given line in the specified buffer.
    ///
    /// Kept for callers that only have a line number (e.g. gutter clicks
    /// that already resolved the line).  Converts to a byte position and
    /// delegates to [`Self::toggle_fold_at_byte`].
    pub fn toggle_fold_at_line(&mut self, buffer_id: crate::model::event::BufferId, line: usize) {
        let byte_pos = {
            let Some(state) = self.buffers.get(&buffer_id) else {
                return;
            };
            state.buffer.line_start_offset(line).unwrap_or_else(|| {
                use crate::view::folding::indent_folding;
                let approx = line * state.buffer.estimated_line_length();
                indent_folding::find_line_start_byte(&state.buffer, approx)
            })
        };
        self.toggle_fold_at_byte(buffer_id, byte_pos);
    }

    /// Toggle folding at the given byte position in the specified buffer.
    pub fn toggle_fold_at_byte(
        &mut self,
        buffer_id: crate::model::event::BufferId,
        byte_pos: usize,
    ) {
        let Some(split_id) = self.buffers.split_manager().map(|m| m.active_split()) else {
            return;
        };

        self.buffers
            .with_buffer_and_split(buffer_id, split_id, |state, view_state| {
                let buf_state = view_state.ensure_buffer_state(buffer_id);

                // Try to unfold first — check if this byte's line is a fold header.
                let header_byte = {
                    use crate::view::folding::indent_folding;
                    indent_folding::find_line_start_byte(&state.buffer, byte_pos)
                };
                if buf_state.folds.remove_by_header_byte(
                    &state.buffer,
                    &mut state.marker_list,
                    header_byte,
                ) {
                    return;
                }

                // Also unfold if the byte position is inside an existing fold.
                if buf_state
                    .folds
                    .remove_if_contains_byte(&mut state.marker_list, byte_pos)
                {
                    return;
                }

                // Determine the fold byte range: prefer LSP ranges, fall back to indent-based.
                if !state.folding_ranges.is_empty() {
                    // --- LSP-provided ranges (line-based) ---
                    let resolved = state
                        .folding_ranges
                        .resolved(&state.buffer, &state.marker_list);
                    let line = state.buffer.get_line_number(byte_pos);
                    let mut exact_range: Option<&lsp_types::FoldingRange> = None;
                    let mut exact_span = usize::MAX;
                    let mut containing_range: Option<&lsp_types::FoldingRange> = None;
                    let mut containing_span = usize::MAX;

                    for range in &resolved {
                        let start_line = range.start_line as usize;
                        let range_end = range.end_line as usize;
                        if range_end <= start_line {
                            continue;
                        }
                        let span = range_end.saturating_sub(start_line);

                        if start_line == line && span < exact_span {
                            exact_span = span;
                            exact_range = Some(range);
                        }
                        if start_line <= line && line <= range_end && span < containing_span {
                            containing_span = span;
                            containing_range = Some(range);
                        }
                    }

                    let chosen = exact_range.or(containing_range);
                    let Some(range) = chosen else {
                        return;
                    };
                    let placeholder = range
                        .collapsed_text
                        .as_ref()
                        .filter(|text| !text.trim().is_empty())
                        .cloned();
                    let header_line = range.start_line as usize;
                    let end_line = range.end_line as usize;
                    let first_hidden = header_line.saturating_add(1);
                    if first_hidden > end_line {
                        return;
                    }
                    let Some(sb) = state.buffer.line_start_offset(first_hidden) else {
                        return;
                    };
                    let eb = state
                        .buffer
                        .line_start_offset(end_line.saturating_add(1))
                        .unwrap_or_else(|| state.buffer.len());
                    let hb = state.buffer.line_start_offset(header_line).unwrap_or(0);
                    create_fold(state, buf_state, sb, eb, hb, placeholder);
                } else {
                    // --- Indent-based folding on bytes ---
                    use crate::view::folding::indent_folding;
                    let tab_size = state.buffer_settings.tab_size;
                    let max_upward = crate::config::INDENT_FOLD_MAX_UPWARD_SCAN;
                    let est_ll = state.buffer.estimated_line_length();
                    let max_scan_bytes = crate::config::INDENT_FOLD_MAX_SCAN_LINES * est_ll;

                    let upward_bytes = max_upward * est_ll;
                    let load_start = byte_pos.saturating_sub(upward_bytes);
                    let load_end = byte_pos
                        .saturating_add(max_scan_bytes)
                        .min(state.buffer.len());
                    drop(
                        state
                            .buffer
                            .get_text_range_mut(load_start, load_end - load_start),
                    );

                    if let Some((hb, sb, eb)) = indent_folding::find_fold_range_at_byte(
                        &state.buffer,
                        byte_pos,
                        tab_size,
                        max_scan_bytes,
                        max_upward,
                    ) {
                        create_fold(state, buf_state, sb, eb, hb, None);
                    }
                }
            });
    }
}

/// Plant a fold over the byte range, moving cursors out of the
/// hidden region and re-anchoring the viewport top if it landed
/// inside the new fold. Free function (not a method) so both
/// `Window::toggle_fold_at_byte` and any future Editor-side
/// orchestrator can call it without a `Self::` qualifier.
fn create_fold(
    state: &mut crate::state::EditorState,
    buf_state: &mut crate::view::split::BufferViewState,
    start_byte: usize,
    end_byte: usize,
    header_byte: usize,
    placeholder: Option<String>,
) {
    if end_byte <= start_byte {
        return;
    }

    // Move any cursors inside the soon-to-be-hidden range to the header line.
    buf_state.cursors.map(|cursor| {
        let in_hidden_range = cursor.position >= start_byte && cursor.position < end_byte;
        let anchor_in_hidden = cursor
            .anchor
            .is_some_and(|anchor| anchor >= start_byte && anchor < end_byte);
        if in_hidden_range || anchor_in_hidden {
            cursor.position = header_byte;
            cursor.anchor = None;
            cursor.sticky_column = None;
            cursor.selection_mode = crate::model::cursor::SelectionMode::Normal;
            cursor.block_anchor = None;
            cursor.deselect_on_move = true;
        }
    });

    buf_state
        .folds
        .add(&mut state.marker_list, start_byte, end_byte, placeholder);

    // If the viewport top is now inside the folded range, move it to the header.
    if buf_state.viewport.top_byte() >= start_byte && buf_state.viewport.top_byte() < end_byte {
        buf_state.viewport.set_top_byte(header_byte);
        buf_state.viewport.set_top_view_line_offset(0);
    }
}

impl Editor {
    /// Send didClose to a specific named server for all buffers of a language.
    ///
    /// Used when stopping a single server out of multiple for the same language,
    /// where we don't want to fully disable LSP for the buffers.
    pub(crate) fn send_did_close_to_server(&mut self, language: &str, server_name: &str) {
        let uris: Vec<_> = self
            .buffers()
            .iter()
            .filter(|(_, s)| s.language == language)
            .filter_map(|(id, _)| {
                self.active_window()
                    .buffer_metadata
                    .get(id)
                    .and_then(|m| m.file_uri())
                    .cloned()
            })
            .collect();

        let __active_id = self.active_window;

        if let Some(lsp) = self.windows.get_mut(&__active_id).map(|w| &mut w.lsp) {
            for sh in lsp.get_handles_mut(language) {
                if sh.name == server_name {
                    for uri in &uris {
                        tracing::info!(
                            "Sending didClose for {} to '{}' (language: {})",
                            uri.as_str(),
                            sh.name,
                            language
                        );
                        if let Err(e) = sh.handle.did_close(uri.as_uri().clone()) {
                            tracing::warn!("Failed to send didClose to '{}': {}", sh.name, e);
                        }
                    }
                    break;
                }
            }
        }
    }

    /// Core server-stop teardown shared by the command-palette and
    /// status-popup stop paths.
    ///
    /// Does the three things that must travel together, in the right
    /// order:
    ///
    /// 1. Shutdown the manager handle(s) — either a single named server
    ///    or every server configured for `language` (`server_name = None`).
    /// 2. Clear the matching `lsp_server_statuses` entries on the editor
    ///    so the status-bar indicator (`compose_lsp_status` in
    ///    `app/render.rs`) doesn't stay stuck at `"LSP (on)"` with a
    ///    stale `Running` entry. This is the step the palette path
    ///    used to miss, producing the user-reported stale-indicator
    ///    bug.
    /// 3. Drop diagnostics published by the stopped server(s) so
    ///    red/yellow overlays don't persist on-screen after the
    ///    producer is gone.
    ///
    /// `didClose` for open buffers is the caller's responsibility and
    /// MUST happen before this function: the handles are removed as
    /// part of step 1. The palette caller layers config updates
    /// (`auto_start = false`) and a user-facing status message on top.
    ///
    /// Returns `true` if anything was actually stopped (matches
    /// `LspManager::shutdown_server`'s contract).
    pub(crate) fn stop_lsp_server_and_cleanup(
        &mut self,
        language: &str,
        server_name: Option<&str>,
    ) -> bool {
        // Snapshot the server names we're about to drop — once the
        // handles are gone the manager can't enumerate them anymore,
        // and we need the names for the status + diagnostic cleanup.
        let stopping_names: Vec<String> = if let Some(name) = server_name {
            vec![name.to_string()]
        } else {
            self.lsp()
                .map(|lsp| lsp.server_names_for_language(language))
                .unwrap_or_default()
        };

        let __active_id = self.active_window;

        let stopped = if let Some(lsp) = self.windows.get_mut(&__active_id).map(|w| &mut w.lsp) {
            if let Some(name) = server_name {
                lsp.shutdown_server_by_name(language, name)
            } else {
                lsp.shutdown_server(language)
            }
        } else {
            false
        };

        if !stopped {
            return false;
        }

        for name in &stopping_names {
            self.active_window_mut()
                .lsp_server_statuses
                .remove(&(language.to_string(), name.clone()));
            // Clear diagnostics this server published so overlays clear
            // from every buffer it touched (not just the active one).
            self.clear_diagnostics_for_server(name);
        }

        // Clear any in-flight `$/progress` entries for this language
        // if the language has no surviving handles. The dead server
        // will never send the matching `end` notifications, so
        // without this cleanup `compose_lsp_status` would keep
        // winning the spinner branch over `(off)` — the indicator
        // would stay stuck on a rotating braille glyph that doesn't
        // actually rotate (no async events fire to re-render).
        //
        // We defer the check to after the shutdown so handle
        // enumeration reflects the new state. Keyed by language
        // because `LspProgressInfo` doesn't carry a server name —
        // safe: if any handle for the language survives, progress
        // on that language is still the surviving server's business
        // and we leave it alone.
        let any_handle_left = self
            .lsp()
            .as_ref()
            .is_some_and(|lsp| lsp.has_handles(language));
        if !any_handle_left {
            self.active_window_mut()
                .lsp_progress
                .retain(|_, info| info.language != language);
        }

        true
    }

    /// Disable LSP for a specific buffer and clear all LSP-related data
    pub(crate) fn disable_lsp_for_buffer(&mut self, buffer_id: crate::model::event::BufferId) {
        // Send didClose to the LSP server so it removes the document from its
        // tracking. This is critical: without didClose, the async handler's
        // document_versions still has the path, and should_skip_did_open will
        // block the didOpen when LSP is re-enabled — causing a desync where
        // the server has stale content. (GitHub issue #952)
        if let Some(uri) = self
            .active_window()
            .buffer_metadata
            .get(&buffer_id)
            .and_then(|m| m.file_uri())
            .cloned()
        {
            let language = self
                .buffers()
                .get(&buffer_id)
                .map(|s| s.language.clone())
                .unwrap_or_default();
            let __active_id = self.active_window;
            if let Some(lsp) = self.windows.get_mut(&__active_id).map(|w| &mut w.lsp) {
                // Broadcast didClose to all handles for this language
                if !lsp.has_handles(&language) {
                    tracing::warn!(
                        "disable_lsp_for_buffer: no handle for language '{}'",
                        language
                    );
                } else {
                    for sh in lsp.get_handles_mut(&language) {
                        tracing::info!(
                            "Sending didClose for {} to '{}' (language: {})",
                            uri.as_str(),
                            sh.name,
                            language
                        );
                        if let Err(e) = sh.handle.did_close(uri.as_uri().clone()) {
                            tracing::warn!("Failed to send didClose to '{}': {}", sh.name, e);
                        }
                    }
                }
            } else {
                tracing::warn!("disable_lsp_for_buffer: no LSP manager");
            }
        } else {
            tracing::warn!("disable_lsp_for_buffer: no URI for buffer");
        }

        // Disable LSP in metadata
        if let Some(metadata) = self.active_window_mut().buffer_metadata.get_mut(&buffer_id) {
            metadata.disable_lsp(t!("lsp.disabled.user").to_string());
            // Clear LSP opened tracking so it will be sent again if re-enabled
            metadata.lsp_opened_with.clear();
        }
        self.set_status_message(t!("lsp.disabled_for_buffer").to_string());

        // Clear diagnostics for this buffer
        let uri = self
            .active_window()
            .buffer_metadata
            .get(&buffer_id)
            .and_then(|m| m.file_uri())
            .map(|u| u.as_str().to_string());

        if let Some(uri_str) = uri {
            self.stored_diagnostics_mut().remove(&uri_str);
            self.active_window_mut()
                .stored_push_diagnostics
                .remove(&uri_str);
            self.active_window_mut()
                .stored_pull_diagnostics
                .remove(&uri_str);
            self.active_window_mut()
                .diagnostic_result_ids
                .remove(&uri_str);
            self.stored_folding_ranges_mut().remove(&uri_str);
        }

        // Cancel scheduled diagnostic pull if it targets this buffer
        if let Some((scheduled_buf, _)) = &self.active_window().scheduled_diagnostic_pull {
            if *scheduled_buf == buffer_id {
                self.active_window_mut().scheduled_diagnostic_pull = None;
            }
        }

        // Cancel scheduled inlay hints refresh if it targets this buffer
        if let Some((scheduled_buf, _)) = &self.active_window().scheduled_inlay_hints_request {
            if *scheduled_buf == buffer_id {
                self.active_window_mut().scheduled_inlay_hints_request = None;
            }
        }

        self.active_window_mut()
            .folding_ranges_in_flight
            .remove(&buffer_id);
        self.active_window_mut()
            .folding_ranges_debounce
            .remove(&buffer_id);
        self.active_window_mut()
            .pending_folding_range_requests
            .retain(|_, req| req.buffer_id != buffer_id);
        // Drop any in-flight inlay hint requests for this buffer so
        // their eventual responses don't repopulate the cleared overlay.
        self.active_window_mut()
            .pending_inlay_hints_requests
            .retain(|_, req| req.buffer_id != buffer_id);

        // Clear all LSP-related overlays for this buffer (diagnostics + inlay hints)
        let diagnostic_ns = crate::services::lsp::diagnostics::lsp_diagnostic_namespace();
        self.active_window_mut()
            .clear_lsp_overlays_for_buffer(buffer_id, &diagnostic_ns);
    }

    /// Enable LSP for a specific buffer and send didOpen notification
    fn enable_lsp_for_buffer(
        &mut self,
        buffer_id: crate::model::event::BufferId,
        language: &str,
        file_path: Option<std::path::PathBuf>,
    ) {
        // Re-enable LSP in metadata
        if let Some(metadata) = self.active_window_mut().buffer_metadata.get_mut(&buffer_id) {
            metadata.lsp_enabled = true;
            metadata.lsp_disabled_reason = None;
        }
        self.set_status_message(t!("lsp.enabled_for_buffer").to_string());

        // Send didOpen if we have a file path
        if let Some(_path) = file_path {
            self.send_lsp_did_open_for_buffer(buffer_id, language);
        }
    }

    /// Send LSP didOpen notification for a buffer
    fn send_lsp_did_open_for_buffer(
        &mut self,
        buffer_id: crate::model::event::BufferId,
        language: &str,
    ) {
        // Get the URI and buffer text
        let (uri, text) = {
            let metadata = self.active_window().buffer_metadata.get(&buffer_id);
            let uri = metadata.and_then(|m| m.file_uri()).cloned();
            let text = self
                .buffers()
                .get(&buffer_id)
                .and_then(|state| state.buffer.to_string());
            (uri, text)
        };

        let Some(uri) = uri else { return };
        let Some(text) = text else { return };

        // Try to spawn and send didOpen
        use crate::services::lsp::manager::LspSpawnResult;
        let file_path = self
            .active_window()
            .buffer_metadata
            .get(&buffer_id)
            .and_then(|m| m.file_path())
            .cloned();
        // Pre-collect buffer info needed later (line/char/version) so
        // we don't have to read self.buffers() while holding the
        // &mut self.windows borrow on lsp.
        let inlay_buffer_info: Option<(u32, u32, u64)> = self
            .windows
            .get(&self.active_window)
            .and_then(|w| w.buffers.get(&buffer_id))
            .map(|state| {
                let line_count = state.buffer.line_count().unwrap_or(1000);
                (
                    line_count.saturating_sub(1) as u32,
                    10000u32,
                    state.buffer.version(),
                )
            });
        let __active_id = self.active_window;
        let enable_inlay_hints = self.config.editor.enable_inlay_hints;

        let Some(__win) = self.windows.get_mut(&__active_id) else {
            return;
        };
        let __next_id = &mut __win.next_lsp_request_id;
        let buffer_metadata = &mut __win.buffer_metadata;
        let lsp = &mut __win.lsp;

        if lsp.try_spawn(language, file_path.as_deref()) != LspSpawnResult::Spawned {
            return;
        }

        let Some(handle) = lsp.get_handle_mut(language) else {
            return;
        };

        let handle_id = handle.id();
        if let Err(e) = handle.did_open(uri.as_uri().clone(), text, language.to_string()) {
            tracing::warn!("Failed to send didOpen to LSP: {}", e);
            return;
        }

        // Mark buffer as opened with this server
        if let Some(metadata) = buffer_metadata.get_mut(&buffer_id) {
            metadata.lsp_opened_with.insert(handle_id);
        }

        // Request diagnostics. We just re-sent didOpen, so ask for a full
        // report (no previous result_id) — any prior per-server result_ids
        // for this URI were cleared when LSP was disabled for the buffer.
        let request_id = {
            let id = *__next_id;
            *__next_id += 1;
            id
        };
        if let Err(e) = handle.document_diagnostic(request_id, uri.as_uri().clone(), None) {
            tracing::warn!("LSP document_diagnostic request failed: {}", e);
        }

        // Request inlay hints if enabled
        if enable_inlay_hints {
            let (last_line, last_char, buffer_version) =
                inlay_buffer_info.unwrap_or((999, 10000, 0));

            let request_id = {
                let id = *__next_id;
                *__next_id += 1;
                id
            };
            if let Err(e) =
                handle.inlay_hints(request_id, uri.as_uri().clone(), 0, 0, last_line, last_char)
            {
                tracing::warn!("LSP inlay_hints request failed: {}", e);
            } else {
                __win.pending_inlay_hints_requests.insert(
                    request_id,
                    super::InlayHintsRequest {
                        buffer_id,
                        version: buffer_version,
                    },
                );
            }
        }

        // Schedule folding range refresh
        let _ = __next_id;
        let _ = lsp;
        let _ = handle;
        self.active_window_mut()
            .schedule_folding_ranges_refresh(buffer_id);
    }

    /// Set up a plugin development workspace for LSP support on a buffer.
    ///
    /// Creates a temp directory with `fresh.d.ts` + `tsconfig.json` so that
    /// `typescript-language-server` can provide autocomplete and type checking
    /// for plugin buffers (including unsaved/unnamed ones).
    pub(crate) fn setup_plugin_dev_lsp(&mut self, buffer_id: BufferId, content: &str) {
        use crate::services::plugins::plugin_dev_workspace::PluginDevWorkspace;

        // Use the exact cached extraction location for fresh.d.ts
        #[cfg(feature = "embed-plugins")]
        let fresh_dts_path = {
            let Some(embedded_dir) = crate::services::plugins::embedded::get_embedded_plugins_dir()
            else {
                tracing::warn!(
                    "Cannot set up plugin dev LSP: embedded plugins directory not available"
                );
                return;
            };
            let path = embedded_dir.join("lib").join("fresh.d.ts");
            if !path.exists() {
                tracing::warn!(
                    "Cannot set up plugin dev LSP: fresh.d.ts not found at {:?}",
                    path
                );
                return;
            }
            path
        };

        #[cfg(not(feature = "embed-plugins"))]
        let fresh_dts_path = {
            // In non-embedded builds (development), use the source tree path
            let path = std::path::Path::new(env!("CARGO_MANIFEST_DIR"))
                .join("plugins")
                .join("lib")
                .join("fresh.d.ts");
            if !path.exists() {
                tracing::warn!(
                    "Cannot set up plugin dev LSP: fresh.d.ts not found at {:?}",
                    path
                );
                return;
            }
            path
        };

        // Create the workspace
        let buffer_id_num: usize = buffer_id.0;
        match PluginDevWorkspace::create(buffer_id_num, content, &fresh_dts_path) {
            Ok(workspace) => {
                let plugin_file = workspace.plugin_file.clone();

                // Update buffer metadata to point at the temp file, enabling LSP
                let plugin_file_uri = super::types::LspUri::from_host_path(
                    &plugin_file,
                    self.authority().path_translation.as_ref(),
                );
                if let Some(uri) = plugin_file_uri {
                    if let Some(metadata) =
                        self.active_window_mut().buffer_metadata.get_mut(&buffer_id)
                    {
                        metadata.kind = super::types::BufferKind::File {
                            path: plugin_file.clone(),
                            uri: Some(uri),
                        };
                        metadata.lsp_enabled = true;
                        metadata.lsp_disabled_reason = None;
                        // Clear any previous LSP opened state so didOpen is sent fresh
                        metadata.lsp_opened_with.clear();

                        tracing::info!(
                            "Plugin dev LSP enabled for buffer {} via {:?}",
                            buffer_id_num,
                            plugin_file
                        );
                    }
                }

                // Set buffer language to TypeScript so LSP requests use the right handle
                if let Some(state) = self
                    .windows
                    .get_mut(&self.active_window)
                    .map(|w| &mut w.buffers)
                    .expect("active window present")
                    .get_mut(&buffer_id)
                {
                    let first_line = state.buffer.first_line_lossy();
                    let detected =
                        crate::primitives::detected_language::DetectedLanguage::from_path(
                            &plugin_file,
                            first_line.as_deref(),
                            &self.grammar_registry,
                            &self.config.languages,
                        );
                    state.apply_language(detected);
                    state.apply_buffer_config(&self.config);
                }

                // Allow TypeScript language so LSP auto-spawns
                let __active_id = self.active_window;
                if let Some(lsp) = self.windows.get_mut(&__active_id).map(|w| &mut w.lsp) {
                    lsp.allow_language("typescript");
                }

                // Store workspace for cleanup
                let workspace_dir = workspace.dir().to_path_buf();
                self.active_window_mut()
                    .plugin_dev_workspaces
                    .insert(buffer_id, workspace);

                // Actually spawn the LSP server and send didOpen for this buffer
                self.send_lsp_did_open_for_buffer(buffer_id, "typescript");

                // Add the plugin workspace folder so tsserver discovers tsconfig.json + fresh.d.ts
                if let Some(lsp) = self.lsp() {
                    if let Some(handle) = lsp.get_handle("typescript") {
                        if let Some(uri) = super::types::file_path_to_lsp_uri_with_translation(
                            &workspace_dir,
                            self.authority().path_translation.as_ref(),
                        ) {
                            let name = workspace_dir
                                .file_name()
                                .unwrap_or_default()
                                .to_string_lossy()
                                .into_owned();
                            if let Err(e) = handle.add_workspace_folder(uri, name) {
                                tracing::warn!("Failed to add plugin workspace folder: {}", e);
                            } else {
                                tracing::info!(
                                    "Added plugin workspace folder: {:?}",
                                    workspace_dir
                                );
                            }
                        }
                    }
                }
            }
            Err(e) => {
                tracing::warn!("Failed to create plugin dev workspace: {}", e);
            }
        }
    }
}
