use crate::diagnostics::{DiagnosticsOptions, codeblock_fence_for_path, collect_diagnostics};
use acp_thread::{MentionUri, selection_name};
use agent::{ThreadStore, outline};
use agent_client_protocol::schema as acp;
use agent_servers::{AgentServer, AgentServerDelegate};
use anyhow::{Context as _, Result, anyhow};
use collections::{HashMap, HashSet};
use editor::{
    Anchor, Editor, EditorSnapshot, FoldPlaceholder, ToOffset,
    display_map::{Crease, CreaseId, CreaseMetadata, FoldId},
    scroll::Autoscroll,
};
use futures::{AsyncReadExt as _, FutureExt as _, future::Shared};
use gpui::{
    AppContext, ClipboardEntry, Context, Empty, Entity, EntityId, Image, ImageFormat, Img,
    SharedString, Task, WeakEntity,
};
use http_client::{AsyncBody, HttpClientWithUrl};
use itertools::Either;
use language::Buffer;
use language_model::{LanguageModelImage, LanguageModelImageExt};
use multi_buffer::MultiBufferRow;
use postage::stream::Stream as _;
use project::{Project, ProjectItem, ProjectPath, Worktree};
use prompt_store::{PromptId, PromptStore};
use rope::Point;
use std::{
    cell::RefCell,
    ffi::OsStr,
    fmt::Write,
    ops::{Range, RangeInclusive},
    path::{Path, PathBuf},
    rc::Rc,
    sync::Arc,
};
use text::OffsetRangeExt;
use ui::{Disclosure, Toggleable, prelude::*};
use util::{ResultExt, debug_panic, rel_path::RelPath};
use workspace::{Workspace, notifications::NotifyResultExt as _};

use crate::ui::MentionCrease;

pub type MentionTask = Shared<Task<Result<Mention, String>>>;

#[derive(Debug, Clone, Eq, PartialEq)]
pub enum Mention {
    Text {
        content: String,
        tracked_buffers: Vec<Entity<Buffer>>,
    },
    Image(MentionImage),
    Link,
}

#[derive(Clone, Debug, Eq, PartialEq)]
pub struct MentionImage {
    pub data: SharedString,
    pub format: ImageFormat,
}

pub struct MentionSet {
    project: WeakEntity<Project>,
    thread_store: Option<Entity<ThreadStore>>,
    prompt_store: Option<Entity<PromptStore>>,
    mentions: HashMap<CreaseId, (MentionUri, MentionTask)>,
    crease_entities: HashMap<CreaseId, Entity<LoadingContext>>,
}

impl MentionSet {
    pub fn new(
        project: WeakEntity<Project>,
        thread_store: Option<Entity<ThreadStore>>,
        prompt_store: Option<Entity<PromptStore>>,
    ) -> Self {
        Self {
            project,
            thread_store,
            prompt_store,
            mentions: HashMap::default(),
            crease_entities: HashMap::default(),
        }
    }

    pub fn contents(
        &self,
        full_mention_content: bool,
        cx: &mut App,
    ) -> Task<Result<HashMap<CreaseId, (MentionUri, Mention)>>> {
        let Some(project) = self.project.upgrade() else {
            return Task::ready(Err(anyhow!("Project not found")));
        };
        let mentions = self.mentions.clone();
        cx.spawn(async move |cx| {
            let mut contents = HashMap::default();
            for (crease_id, (mention_uri, task)) in mentions {
                let content = if full_mention_content
                    && let MentionUri::Directory { abs_path } = &mention_uri
                {
                    cx.update(|cx| full_mention_for_directory(&project, abs_path, cx))
                        .await?
                } else {
                    task.await.map_err(|e| anyhow!("{e}"))?
                };

                contents.insert(crease_id, (mention_uri, content));
            }
            Ok(contents)
        })
    }

    pub fn remove_invalid(&mut self, snapshot: &EditorSnapshot) {
        for (crease_id, crease) in snapshot.crease_snapshot.creases() {
            if !crease.range().start.is_valid(snapshot.buffer_snapshot()) {
                self.mentions.remove(&crease_id);
                self.crease_entities.remove(&crease_id);
            }
        }
    }

    pub fn insert_mention(
        &mut self,
        crease_id: CreaseId,
        uri: MentionUri,
        task: MentionTask,
        crease_entity: Option<Entity<LoadingContext>>,
        cx: &mut App,
    ) {
        self.mentions.insert(crease_id, (uri, task));
        if let Some(entity) = crease_entity {
            self.crease_entities.insert(crease_id, entity);
        }
        self.recompute_disambiguation(cx);
    }

    /// Creates the appropriate confirmation task for a mention based on its URI type.
    /// This is used when pasting mention links to properly load their content.
    pub fn confirm_mention_for_uri(
        &mut self,
        mention_uri: MentionUri,
        supports_images: bool,
        http_client: Arc<HttpClientWithUrl>,
        cx: &mut Context<Self>,
    ) -> Task<Result<Mention>> {
        match mention_uri {
            MentionUri::Fetch { url } => self.confirm_mention_for_fetch(url, http_client, cx),
            MentionUri::Directory { .. } => Task::ready(Ok(Mention::Link)),
            MentionUri::Thread { id, .. } => self.confirm_mention_for_thread(id, cx),
            MentionUri::File { abs_path } => {
                self.confirm_mention_for_file(abs_path, supports_images, cx)
            }
            MentionUri::Symbol {
                abs_path,
                line_range,
                ..
            } => self.confirm_mention_for_symbol(abs_path, line_range, cx),
            MentionUri::Rule { id, .. } => self.confirm_mention_for_rule(id, cx),
            MentionUri::Skill {
                skill_file_path, ..
            } => self.confirm_mention_for_skill(skill_file_path, cx),
            MentionUri::Diagnostics {
                include_errors,
                include_warnings,
            } => self.confirm_mention_for_diagnostics(include_errors, include_warnings, cx),
            MentionUri::GitDiff { base_ref } => {
                self.confirm_mention_for_git_diff(base_ref.into(), cx)
            }
            MentionUri::Selection {
                abs_path: Some(abs_path),
                line_range,
                ..
            } => self.confirm_mention_for_symbol(abs_path, line_range, cx),
            MentionUri::Selection { abs_path: None, .. } => Task::ready(Err(anyhow!(
                "Untitled buffer selection mentions are not supported for paste"
            ))),
            MentionUri::PastedImage { .. }
            | MentionUri::TerminalSelection { .. }
            | MentionUri::MergeConflict { .. } => {
                Task::ready(Err(anyhow!("Unsupported mention URI type for paste")))
            }
        }
    }

    pub fn remove_mention(&mut self, crease_id: &CreaseId, cx: &mut App) {
        self.mentions.remove(crease_id);
        self.crease_entities.remove(crease_id);
        self.recompute_disambiguation(cx);
    }

    pub fn creases(&self) -> HashSet<CreaseId> {
        self.mentions.keys().cloned().collect()
    }

    pub fn is_empty(&self) -> bool {
        self.mentions.is_empty()
    }

    pub fn mentions(&self) -> HashSet<MentionUri> {
        self.mentions.values().map(|(uri, _)| uri.clone()).collect()
    }

    pub fn mention_uri_for_crease(&self, crease_id: &CreaseId) -> Option<MentionUri> {
        self.mentions.get(crease_id).map(|(uri, _)| uri.clone())
    }

    /// Returns the resolved mention for a crease, if any.
    pub fn resolved_mention_for_crease(
        &self,
        crease_id: &CreaseId,
    ) -> Option<(MentionUri, Option<Mention>)> {
        let (uri, task) = self.mentions.get(crease_id)?;
        let mention = task.clone().now_or_never().and_then(|result| result.ok());
        Some((uri.clone(), mention))
    }

    pub fn set_mentions(&mut self, mentions: HashMap<CreaseId, (MentionUri, MentionTask)>) {
        self.crease_entities
            .retain(|id, _| mentions.contains_key(id));
        self.mentions = mentions;
    }

    pub fn clear(&mut self) -> impl Iterator<Item = (CreaseId, (MentionUri, MentionTask))> {
        self.crease_entities.clear();
        self.mentions.drain()
    }

    fn recompute_disambiguation(&self, cx: &mut App) {
        let labels =
            compute_disambiguated_labels(self.mentions.iter().map(|(id, (uri, _))| (*id, uri)));

        for (crease_id, new_label) in labels {
            if let Some(entity) = self.crease_entities.get(&crease_id) {
                entity.update(cx, |loading_ctx, cx| {
                    if loading_ctx.label != new_label {
                        loading_ctx.label = new_label;
                        cx.notify();
                    }
                });
            }
        }
    }

    pub fn confirm_mention_completion(
        &mut self,
        crease_text: SharedString,
        start: text::Anchor,
        content_len: usize,
        mention_uri: MentionUri,
        supports_images: bool,
        editor: Entity<Editor>,
        workspace: &Entity<Workspace>,
        window: &mut Window,
        cx: &mut Context<Self>,
    ) -> Task<()> {
        let Some(project) = self.project.upgrade() else {
            return Task::ready(());
        };

        let snapshot = editor.update(cx, |editor, cx| editor.snapshot(window, cx));
        let Some(start_anchor) = snapshot.buffer_snapshot().anchor_in_excerpt(start) else {
            return Task::ready(());
        };
        let end_anchor = snapshot.buffer_snapshot().anchor_before(
            start_anchor.to_offset(&snapshot.buffer_snapshot()) + content_len + 1usize,
        );

        let crease = if let MentionUri::File { abs_path } = &mention_uri
            && is_raster_image_path(abs_path)
        {
            let Some(project_path) = project
                .read(cx)
                .project_path_for_absolute_path(&abs_path, cx)
            else {
                log::error!("project path not found");
                return Task::ready(());
            };
            let image_task = project.update(cx, |project, cx| project.open_image(project_path, cx));
            let image = cx
                .spawn(async move |_, cx| {
                    let image = image_task.await.map_err(|e| e.to_string())?;
                    let image = image.update(cx, |image, _| image.image.clone());
                    Ok(image)
                })
                .shared();
            insert_crease_for_mention(
                start,
                content_len,
                mention_uri.name().into(),
                IconName::Image.path().into(),
                mention_uri.tooltip_text(),
                Some(mention_uri.clone()),
                Some(workspace.downgrade()),
                Some(image),
                editor.clone(),
                window,
                cx,
            )
        } else {
            insert_crease_for_mention(
                start,
                content_len,
                crease_text,
                mention_uri.icon_path(cx),
                mention_uri.tooltip_text(),
                Some(mention_uri.clone()),
                Some(workspace.downgrade()),
                None,
                editor.clone(),
                window,
                cx,
            )
        };
        let Some((crease_id, tx, crease_entity)) = crease else {
            return Task::ready(());
        };

        let task = match mention_uri.clone() {
            MentionUri::Fetch { url } => {
                self.confirm_mention_for_fetch(url, workspace.read(cx).client().http_client(), cx)
            }
            MentionUri::Directory { .. } => Task::ready(Ok(Mention::Link)),
            MentionUri::Thread { id, .. } => self.confirm_mention_for_thread(id, cx),
            MentionUri::File { abs_path } => {
                self.confirm_mention_for_file(abs_path, supports_images, cx)
            }
            MentionUri::Symbol {
                abs_path,
                line_range,
                ..
            } => self.confirm_mention_for_symbol(abs_path, line_range, cx),
            MentionUri::Rule { id, .. } => self.confirm_mention_for_rule(id, cx),
            MentionUri::Skill {
                skill_file_path, ..
            } => self.confirm_mention_for_skill(skill_file_path, cx),
            MentionUri::Diagnostics {
                include_errors,
                include_warnings,
            } => self.confirm_mention_for_diagnostics(include_errors, include_warnings, cx),
            MentionUri::PastedImage { .. } => {
                debug_panic!("pasted image URI should not be included in completions");
                Task::ready(Err(anyhow!(
                    "pasted imaged URI should not be included in completions"
                )))
            }
            MentionUri::Selection { .. } => {
                debug_panic!("unexpected selection URI");
                Task::ready(Err(anyhow!("unexpected selection URI")))
            }
            MentionUri::TerminalSelection { .. } => {
                debug_panic!("unexpected terminal URI");
                Task::ready(Err(anyhow!("unexpected terminal URI")))
            }
            MentionUri::GitDiff { base_ref } => {
                self.confirm_mention_for_git_diff(base_ref.into(), cx)
            }
            MentionUri::MergeConflict { .. } => {
                debug_panic!("unexpected merge conflict URI");
                Task::ready(Err(anyhow!("unexpected merge conflict URI")))
            }
        };
        let task = cx
            .spawn(async move |_, _| task.await.map_err(|e| e.to_string()))
            .shared();
        self.mentions.insert(crease_id, (mention_uri, task.clone()));
        if let Some(entity) = crease_entity {
            self.crease_entities.insert(crease_id, entity);
        }
        self.recompute_disambiguation(cx);

        // Notify the user if we failed to load the mentioned context
        let workspace = workspace.downgrade();
        cx.spawn(async move |this, mut cx| {
            let result = task.await.notify_workspace_async_err(workspace, &mut cx);
            drop(tx);
            if result.is_none() {
                this.update(cx, |this, cx| {
                    editor.update(cx, |editor, cx| {
                        // Remove mention
                        editor.edit([(start_anchor..end_anchor, "")], cx);
                    });
                    this.mentions.remove(&crease_id);
                    this.crease_entities.remove(&crease_id);
                })
                .ok();
            }
        })
    }

    pub fn confirm_mention_for_file(
        &self,
        abs_path: PathBuf,
        supports_images: bool,
        cx: &mut Context<Self>,
    ) -> Task<Result<Mention>> {
        let Some(project) = self.project.upgrade() else {
            return Task::ready(Err(anyhow!("project not found")));
        };

        let Some(project_path) = project
            .read(cx)
            .project_path_for_absolute_path(&abs_path, cx)
        else {
            return Task::ready(Err(anyhow!("project path not found")));
        };

        if is_raster_image_path(&abs_path) {
            if !supports_images {
                return Task::ready(Err(anyhow!("This model does not support images yet")));
            }
            let task = project.update(cx, |project, cx| project.open_image(project_path, cx));
            return cx.spawn(async move |_, cx| {
                let image = task.await?;
                let image = image.update(cx, |image, _| image.image.clone());
                let image = cx
                    .update(|cx| LanguageModelImage::from_image(image, cx))
                    .await;
                if let Some(image) = image {
                    Ok(Mention::Image(MentionImage {
                        data: image.source,
                        format: LanguageModelImage::FORMAT,
                    }))
                } else {
                    Err(anyhow!("Failed to convert image"))
                }
            });
        }

        let buffer = project.update(cx, |project, cx| project.open_buffer(project_path, cx));
        cx.spawn(async move |_, cx| {
            let buffer = buffer.await?;
            let buffer_content = outline::get_buffer_content_or_outline(
                buffer.clone(),
                Some(&abs_path.to_string_lossy()),
                &cx,
            )
            .await?;

            Ok(Mention::Text {
                content: buffer_content.text,
                tracked_buffers: vec![buffer],
            })
        })
    }

    fn confirm_mention_for_fetch(
        &self,
        url: url::Url,
        http_client: Arc<HttpClientWithUrl>,
        cx: &mut Context<Self>,
    ) -> Task<Result<Mention>> {
        cx.background_executor().spawn(async move {
            let content = fetch_url_content(http_client, url.to_string()).await?;
            Ok(Mention::Text {
                content,
                tracked_buffers: Vec::new(),
            })
        })
    }

    fn confirm_mention_for_symbol(
        &self,
        abs_path: PathBuf,
        line_range: RangeInclusive<u32>,
        cx: &mut Context<Self>,
    ) -> Task<Result<Mention>> {
        let Some(project) = self.project.upgrade() else {
            return Task::ready(Err(anyhow!("project not found")));
        };
        let Some(project_path) = project
            .read(cx)
            .project_path_for_absolute_path(&abs_path, cx)
        else {
            return Task::ready(Err(anyhow!("project path not found")));
        };
        let buffer = project.update(cx, |project, cx| project.open_buffer(project_path, cx));
        cx.spawn(async move |_, cx| {
            let buffer = buffer.await?;
            let mention = buffer.update(cx, |buffer, cx| {
                let start = Point::new(*line_range.start(), 0).min(buffer.max_point());
                let end = Point::new(*line_range.end() + 1, 0).min(buffer.max_point());
                let content = buffer.text_for_range(start..end).collect();
                Mention::Text {
                    content,
                    tracked_buffers: vec![cx.entity()],
                }
            });
            Ok(mention)
        })
    }

    fn confirm_mention_for_skill(
        &self,
        skill_file_path: PathBuf,
        cx: &mut Context<Self>,
    ) -> Task<Result<Mention>> {
        // Built-in skills have synthetic paths that don't exist on disk;
        // serve their content directly from the compiled-in data.
        if let Some(content) = agent_skills::builtin_skill_content(&skill_file_path) {
            return Task::ready(Ok(Mention::Text {
                content: content.to_string(),
                tracked_buffers: Vec::new(),
            }));
        }
        cx.background_spawn(async move {
            let content = std::fs::read_to_string(&skill_file_path).map_err(|e| {
                anyhow!(
                    "Failed to read skill file {}: {}",
                    skill_file_path.display(),
                    e
                )
            })?;
            Ok(Mention::Text {
                content,
                tracked_buffers: Vec::new(),
            })
        })
    }

    fn confirm_mention_for_rule(
        &mut self,
        id: PromptId,
        cx: &mut Context<Self>,
    ) -> Task<Result<Mention>> {
        let Some(prompt_store) = self.prompt_store.as_ref() else {
            return Task::ready(Err(anyhow!("Missing prompt store")));
        };
        let prompt = prompt_store.read(cx).load(id, cx);
        cx.spawn(async move |_, _| {
            let prompt = prompt.await?;
            Ok(Mention::Text {
                content: prompt,
                tracked_buffers: Vec::new(),
            })
        })
    }

    pub fn confirm_mention_for_selection(
        &mut self,
        source_range: Range<text::Anchor>,
        selections: Vec<(Entity<Buffer>, Range<text::Anchor>, Range<usize>)>,
        editor: Entity<Editor>,
        window: &mut Window,
        cx: &mut Context<Self>,
    ) {
        let Some(project) = self.project.upgrade() else {
            return;
        };

        let snapshot = editor.read(cx).buffer().read(cx).snapshot(cx);
        let Some(start) = snapshot.anchor_in_excerpt(source_range.start) else {
            return;
        };

        let offset = start.to_offset(&snapshot);

        for (buffer, selection_range, range_to_fold) in selections {
            let range = snapshot.anchor_after(offset + range_to_fold.start)
                ..snapshot.anchor_after(offset + range_to_fold.end);

            let abs_path = buffer
                .read(cx)
                .project_path(cx)
                .and_then(|project_path| project.read(cx).absolute_path(&project_path, cx));
            let snapshot = buffer.read(cx).snapshot();

            let text = snapshot
                .text_for_range(selection_range.clone())
                .collect::<String>();
            let point_range = selection_range.to_point(&snapshot);
            let line_range = point_range.start.row..=point_range.end.row;

            let uri = MentionUri::Selection {
                abs_path: abs_path.clone(),
                line_range: line_range.clone(),
                column: None,
            };
            let crease = crease_for_mention(
                selection_name(abs_path.as_deref(), &line_range).into(),
                uri.icon_path(cx),
                uri.tooltip_text(),
                range,
                editor.downgrade(),
            );

            let crease_id = editor.update(cx, |editor, cx| {
                let crease_ids = editor.insert_creases(vec![crease.clone()], cx);
                editor.fold_creases(vec![crease], false, window, cx);
                crease_ids.first().copied().unwrap()
            });

            self.mentions.insert(
                crease_id,
                (
                    uri,
                    Task::ready(Ok(Mention::Text {
                        content: text,
                        tracked_buffers: vec![buffer],
                    }))
                    .shared(),
                ),
            );
        }

        // Take this explanation with a grain of salt but, with creases being
        // inserted, GPUI's recomputes the editor layout in the next frames, so
        // directly calling `editor.request_autoscroll` wouldn't work as
        // expected. We're leveraging `cx.on_next_frame` to wait 2 frames and
        // ensure that the layout has been recalculated so that the autoscroll
        // request actually shows the cursor's new position.
        cx.on_next_frame(window, move |_, window, cx| {
            cx.on_next_frame(window, move |_, _, cx| {
                editor.update(cx, |editor, cx| {
                    editor.request_autoscroll(Autoscroll::fit(), cx)
                });
            });
        });
    }

    fn confirm_mention_for_thread(
        &mut self,
        id: acp::SessionId,
        cx: &mut Context<Self>,
    ) -> Task<Result<Mention>> {
        let Some(thread_store) = self.thread_store.clone() else {
            return Task::ready(Err(anyhow!(
                "Thread mentions are only supported for the native agent"
            )));
        };
        let Some(project) = self.project.upgrade() else {
            return Task::ready(Err(anyhow!("project not found")));
        };

        let server = Rc::new(agent::NativeAgentServer::new(
            project.read(cx).fs().clone(),
            thread_store,
        ));
        let delegate =
            AgentServerDelegate::new(project.read(cx).agent_server_store().clone(), None);
        let connection = server.connect(delegate, project.clone(), cx);
        cx.spawn(async move |_, cx| {
            let agent = connection.await?;
            let agent = agent.downcast::<agent::NativeAgentConnection>().unwrap();
            let summary = agent
                .0
                .update(cx, |agent, cx| {
                    agent.thread_summary(id, project.clone(), cx)
                })
                .await?;
            Ok(Mention::Text {
                content: summary.to_string(),
                tracked_buffers: Vec::new(),
            })
        })
    }

    fn confirm_mention_for_diagnostics(
        &self,
        include_errors: bool,
        include_warnings: bool,
        cx: &mut Context<Self>,
    ) -> Task<Result<Mention>> {
        let Some(project) = self.project.upgrade() else {
            return Task::ready(Err(anyhow!("project not found")));
        };

        let diagnostics_task = collect_diagnostics(
            project,
            DiagnosticsOptions {
                include_errors,
                include_warnings,
                path_matcher: None,
            },
            cx,
        );
        cx.spawn(async move |_, _| {
            let content = diagnostics_task
                .await?
                .unwrap_or_else(|| "No diagnostics found.".into());
            Ok(Mention::Text {
                content,
                tracked_buffers: Vec::new(),
            })
        })
    }

    pub fn confirm_mention_for_git_diff(
        &self,
        base_ref: SharedString,
        cx: &mut Context<Self>,
    ) -> Task<Result<Mention>> {
        let Some(project) = self.project.upgrade() else {
            return Task::ready(Err(anyhow!("project not found")));
        };

        let Some(repo) = project.read(cx).active_repository(cx) else {
            return Task::ready(Err(anyhow!("no active repository")));
        };

        let diff_receiver = repo.update(cx, |repo, cx| {
            repo.diff(
                git::repository::DiffType::MergeBase { base_ref: base_ref },
                cx,
            )
        });

        cx.spawn(async move |_, _| {
            let diff_text = diff_receiver.await??;
            if diff_text.is_empty() {
                Ok(Mention::Text {
                    content: "No changes found in branch diff.".into(),
                    tracked_buffers: Vec::new(),
                })
            } else {
                Ok(Mention::Text {
                    content: diff_text,
                    tracked_buffers: Vec::new(),
                })
            }
        })
    }
}

/// Computes disambiguated labels for a set of mentions. When multiple mentions
/// share the same base name, their labels include extra context (additional
/// parent path components for files/directories, source for skills) so the user
/// can tell them apart. Driven by [`util::disambiguate::compute_disambiguation_details`],
/// which is the same utility used for buffer tab titles and the sidebar.
fn compute_disambiguated_labels<'a>(
    mentions: impl Iterator<Item = (CreaseId, &'a MentionUri)>,
) -> HashMap<CreaseId, SharedString> {
    let mentions: Vec<_> = mentions.collect();
    let details =
        util::disambiguate::compute_disambiguation_details(&mentions, |(_, uri), detail| {
            uri.disambiguated_name(detail)
        });
    mentions
        .into_iter()
        .zip(details)
        .map(|((id, uri), detail)| (id, uri.disambiguated_name(detail).into()))
        .collect()
}

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

    use fs::FakeFs;
    use gpui::TestAppContext;
    use project::Project;
    use prompt_store;
    use release_channel;
    use semver::Version;
    use serde_json::json;
    use settings::SettingsStore;
    use std::path::Path;
    use theme;
    use util::path;

    fn init_test(cx: &mut TestAppContext) {
        let settings_store = cx.update(SettingsStore::test);
        cx.set_global(settings_store);
        cx.update(|cx| {
            theme_settings::init(theme::LoadThemes::JustBase, cx);
            release_channel::init(Version::new(0, 0, 0), cx);
            prompt_store::init(cx);
        });
    }

    #[gpui::test]
    async fn test_thread_mentions_disabled(cx: &mut TestAppContext) {
        init_test(cx);

        let fs = FakeFs::new(cx.executor());
        fs.insert_tree("/project", json!({"file": ""})).await;
        let project = Project::test(fs, [Path::new(path!("/project"))], cx).await;
        let thread_store = None;
        let mention_set = cx.new(|_cx| MentionSet::new(project.downgrade(), thread_store, None));

        let task = mention_set.update(cx, |mention_set, cx| {
            mention_set.confirm_mention_for_thread(acp::SessionId::new("thread-1"), cx)
        });

        let error = task.await.unwrap_err();
        assert!(
            error
                .to_string()
                .contains("Thread mentions are only supported for the native agent"),
            "Unexpected error: {error:#}"
        );
    }

    #[gpui::test]
    async fn test_selection_mentions_supported_for_paste(cx: &mut TestAppContext) {
        init_test(cx);

        let fs = FakeFs::new(cx.executor());
        fs.insert_tree(
            "/project",
            json!({"file.rs": "line 1\nline 2\nline 3\nline 4\n"}),
        )
        .await;
        let project = Project::test(fs, [Path::new(path!("/project"))], cx).await;
        let mention_set = cx.new(|_cx| MentionSet::new(project.downgrade(), None, None));

        let mention_task = mention_set.update(cx, |mention_set, cx| {
            let http_client = project.read(cx).client().http_client();
            mention_set.confirm_mention_for_uri(
                MentionUri::Selection {
                    abs_path: Some(path!("/project/file.rs").into()),
                    line_range: 1..=2,
                    column: None,
                },
                false,
                http_client,
                cx,
            )
        });

        let mention = mention_task.await.unwrap();
        match mention {
            Mention::Text {
                content,
                tracked_buffers,
            } => {
                assert_eq!(content, "line 2\nline 3\n");
                assert_eq!(tracked_buffers.len(), 1);
            }
            other => panic!("Expected selection mention to resolve as text, got {other:?}"),
        }
    }

    #[test]
    fn test_is_raster_image_path_is_case_insensitive() {
        // Regression test for #54308: drag-and-dropping a file whose extension
        // is uppercase (e.g. `.PNG`) used to be treated as a non-image file.
        assert!(is_raster_image_path(Path::new("/tmp/image.png")));
        assert!(is_raster_image_path(Path::new("/tmp/image.PNG")));
        assert!(is_raster_image_path(Path::new("/tmp/image.Png")));
        assert!(is_raster_image_path(Path::new("/tmp/photo.JPEG")));
        assert!(is_raster_image_path(Path::new("/tmp/animation.GIF")));

        // SVG is handled via a different code path and must not be reported here.
        assert!(!is_raster_image_path(Path::new("/tmp/icon.svg")));
        assert!(!is_raster_image_path(Path::new("/tmp/icon.SVG")));

        // Non-image extensions and paths with no extension.
        assert!(!is_raster_image_path(Path::new("/tmp/notes.txt")));
        assert!(!is_raster_image_path(Path::new("/tmp/README")));
    }
}

/// Inserts a list of images into the editor as context mentions.
/// This is the shared implementation used by both paste and file picker operations.
pub(crate) async fn insert_images_as_context(
    images: Vec<(gpui::Image, SharedString)>,
    editor: Entity<Editor>,
    mention_set: Entity<MentionSet>,
    workspace: WeakEntity<Workspace>,
    cx: &mut gpui::AsyncWindowContext,
) {
    if images.is_empty() {
        return;
    }

    for (image, name) in images {
        let mention_uri = MentionUri::PastedImage {
            name: name.to_string(),
        };
        let replacement_text = mention_uri.as_link().to_string();
        let Some((text_anchor, multibuffer_anchor)) = editor
            .update_in(cx, |editor, window, cx| {
                let snapshot = editor.snapshot(window, cx);
                let (cursor_anchor, buffer_snapshot) = snapshot
                    .buffer_snapshot()
                    .anchor_to_buffer_anchor(editor.selections.newest_anchor().start)
                    .unwrap();
                let text_anchor = cursor_anchor.bias_left(buffer_snapshot);
                let multibuffer_anchor = snapshot.buffer_snapshot().anchor_in_excerpt(text_anchor);
                editor.insert(&format!("{replacement_text} "), window, cx);
                (text_anchor, multibuffer_anchor)
            })
            .ok()
        else {
            break;
        };

        let content_len = replacement_text.len();
        let Some(start_anchor) = multibuffer_anchor else {
            continue;
        };
        let end_anchor = editor.update(cx, |editor, cx| {
            let snapshot = editor.buffer().read(cx).snapshot(cx);
            snapshot.anchor_before(start_anchor.to_offset(&snapshot) + content_len)
        });
        let image = Arc::new(image);
        let Ok(Some((crease_id, tx, crease_entity))) = cx.update(|window, cx| {
            insert_crease_for_mention(
                text_anchor,
                content_len,
                name.clone(),
                IconName::Image.path().into(),
                None,
                None,
                None,
                Some(Task::ready(Ok(image.clone())).shared()),
                editor.clone(),
                window,
                cx,
            )
        }) else {
            continue;
        };
        let task = cx
            .spawn(async move |cx| {
                let image = cx
                    .update(|_, cx| LanguageModelImage::from_image(image, cx))
                    .map_err(|e| e.to_string())?
                    .await;
                drop(tx);
                if let Some(image) = image {
                    Ok(Mention::Image(MentionImage {
                        data: image.source,
                        format: LanguageModelImage::FORMAT,
                    }))
                } else {
                    Err("Failed to convert image".into())
                }
            })
            .shared();

        mention_set.update(cx, |mention_set, cx| {
            mention_set.insert_mention(
                crease_id,
                MentionUri::PastedImage {
                    name: name.to_string(),
                },
                task.clone(),
                crease_entity,
                cx,
            )
        });

        if task
            .await
            .notify_workspace_async_err(workspace.clone(), cx)
            .is_none()
        {
            editor.update(cx, |editor, cx| {
                editor.edit([(start_anchor..end_anchor, "")], cx);
            });
            mention_set.update(cx, |mention_set, cx| {
                mention_set.remove_mention(&crease_id, cx)
            });
        }
    }
}

fn image_format_from_external_content(format: image::ImageFormat) -> Option<ImageFormat> {
    match format {
        image::ImageFormat::Png => Some(ImageFormat::Png),
        image::ImageFormat::Jpeg => Some(ImageFormat::Jpeg),
        image::ImageFormat::WebP => Some(ImageFormat::Webp),
        image::ImageFormat::Gif => Some(ImageFormat::Gif),
        image::ImageFormat::Bmp => Some(ImageFormat::Bmp),
        image::ImageFormat::Tiff => Some(ImageFormat::Tiff),
        image::ImageFormat::Ico => Some(ImageFormat::Ico),
        image::ImageFormat::Pnm => Some(ImageFormat::Pnm),
        _ => {
            debug_panic!("An unhandled image format: {format:?}");
            None
        }
    }
}

// Case-insensitive so that e.g. `foo.PNG` is recognized the same as `foo.png`.
// SVG is excluded because it is handled separately.
fn is_raster_image_path(path: &Path) -> bool {
    let Some(extension) = path.extension().and_then(OsStr::to_str) else {
        return false;
    };
    if extension.eq_ignore_ascii_case("svg") {
        return false;
    }
    Img::extensions()
        .iter()
        .any(|known| known.eq_ignore_ascii_case(extension))
}

pub(crate) fn load_external_image_from_path(
    path: &Path,
    default_name: &SharedString,
) -> Option<(Image, SharedString)> {
    let content = std::fs::read(path).ok()?;
    let format = image::guess_format(&content)
        .ok()
        .and_then(image_format_from_external_content)?;
    let name = path
        .file_name()
        .and_then(|name| name.to_str())
        .map(|name| SharedString::from(name.to_owned()))
        .unwrap_or_else(|| default_name.clone());

    Some((Image::from_bytes(format, content), name))
}

pub(crate) fn paste_images_as_context(
    editor: Entity<Editor>,
    mention_set: Entity<MentionSet>,
    workspace: WeakEntity<Workspace>,
    window: &mut Window,
    cx: &mut App,
) -> Option<Task<()>> {
    let clipboard = cx.read_from_clipboard()?;

    // Only handle paste if the first clipboard entry is an image or file path.
    // If text comes first, return None so the caller falls through to text paste.
    // This respects the priority order set by the source application.
    if matches!(
        clipboard.entries().first(),
        Some(ClipboardEntry::String(_)) | None
    ) {
        return None;
    }

    Some(window.spawn(cx, async move |mut cx| {
        use itertools::Itertools;
        let default_name: SharedString = "Image".into();
        let (mut images, paths): (Vec<(gpui::Image, SharedString)>, Vec<_>) = clipboard
            .into_entries()
            .filter_map(|entry| match entry {
                ClipboardEntry::Image(image) => Some(Either::Left((image, default_name.clone()))),
                ClipboardEntry::ExternalPaths(paths) => Some(Either::Right(paths)),
                _ => None,
            })
            .partition_map::<Vec<_>, Vec<_>, _, _, _>(std::convert::identity);

        if !paths.is_empty() {
            images.extend(
                cx.background_spawn(async move {
                    paths
                        .into_iter()
                        .flat_map(|paths| paths.paths().to_owned())
                        .filter_map(|path| load_external_image_from_path(&path, &default_name))
                        .collect::<Vec<_>>()
                })
                .await,
            );
        }

        if !images.is_empty() {
            insert_images_as_context(images, editor, mention_set, workspace, &mut cx).await;
        }
    }))
}

pub(crate) fn insert_crease_for_mention(
    anchor: text::Anchor,
    content_len: usize,
    crease_label: SharedString,
    crease_icon: SharedString,
    crease_tooltip: Option<SharedString>,
    mention_uri: Option<MentionUri>,
    workspace: Option<WeakEntity<Workspace>>,
    image: Option<Shared<Task<Result<Arc<Image>, String>>>>,
    editor: Entity<Editor>,
    window: &mut Window,
    cx: &mut App,
) -> Option<(
    CreaseId,
    postage::barrier::Sender,
    Option<Entity<LoadingContext>>,
)> {
    let (tx, rx) = postage::barrier::channel();

    let crease_id = editor.update(cx, |editor, cx| {
        let snapshot = editor.buffer().read(cx).snapshot(cx);

        let start = snapshot.anchor_in_excerpt(anchor)?;

        let start = start.bias_right(&snapshot);
        let end = snapshot.anchor_before(start.to_offset(&snapshot) + content_len);

        let (render, crease_entity) = render_mention_fold_button(
            crease_label.clone(),
            crease_icon.clone(),
            crease_tooltip,
            mention_uri.clone(),
            workspace.clone(),
            start..end,
            rx,
            image,
            cx.weak_entity(),
            cx,
        );
        let placeholder = FoldPlaceholder {
            render,
            merge_adjacent: false,
            ..Default::default()
        };

        let crease = Crease::Inline {
            range: start..end,
            placeholder,
            render_toggle: None,
            render_trailer: None,
            metadata: Some(CreaseMetadata {
                label: crease_label,
                icon_path: crease_icon,
            }),
        };

        let ids = editor.insert_creases(vec![crease.clone()], cx);
        editor.fold_creases(vec![crease], false, window, cx);

        Some((ids[0], crease_entity))
    })?;

    let (crease_id, crease_entity) = crease_id;
    Some((crease_id, tx, Some(crease_entity)))
}

pub(crate) fn crease_for_mention(
    label: SharedString,
    icon_path: SharedString,
    tooltip: Option<SharedString>,
    range: Range<Anchor>,
    editor_entity: WeakEntity<Editor>,
) -> Crease<Anchor> {
    let placeholder = FoldPlaceholder {
        render: render_fold_icon_button(icon_path.clone(), label.clone(), tooltip, editor_entity),
        merge_adjacent: false,
        ..Default::default()
    };

    let render_trailer = move |_row, _unfold, _window: &mut Window, _cx: &mut App| Empty.into_any();

    Crease::inline(range, placeholder, fold_toggle("mention"), render_trailer)
        .with_metadata(CreaseMetadata { icon_path, label })
}

fn render_fold_icon_button(
    icon_path: SharedString,
    label: SharedString,
    tooltip: Option<SharedString>,
    editor: WeakEntity<Editor>,
) -> Arc<dyn Send + Sync + Fn(FoldId, Range<Anchor>, &mut App) -> AnyElement> {
    Arc::new({
        move |fold_id, fold_range, cx| {
            let is_in_text_selection = editor
                .update(cx, |editor, cx| editor.is_range_selected(&fold_range, cx))
                .unwrap_or_default();

            MentionCrease::new(fold_id, icon_path.clone(), label.clone())
                .is_toggled(is_in_text_selection)
                .when_some(tooltip.clone(), |this, tooltip_text| {
                    this.tooltip(tooltip_text)
                })
                .into_any_element()
        }
    })
}

fn fold_toggle(
    name: &'static str,
) -> impl Fn(
    MultiBufferRow,
    bool,
    Arc<dyn Fn(bool, &mut Window, &mut App) + Send + Sync>,
    &mut Window,
    &mut App,
) -> AnyElement {
    move |row, is_folded, fold, _window, _cx| {
        Disclosure::new((name, row.0 as u64), !is_folded)
            .toggle_state(is_folded)
            .on_click(move |_e, window, cx| fold(!is_folded, window, cx))
            .into_any_element()
    }
}

fn full_mention_for_directory(
    project: &Entity<Project>,
    abs_path: &Path,
    cx: &mut App,
) -> Task<Result<Mention>> {
    fn collect_files_in_path(worktree: &Worktree, path: &RelPath) -> Vec<(Arc<RelPath>, String)> {
        let mut files = Vec::new();

        for entry in worktree.child_entries(path) {
            if entry.is_dir() {
                files.extend(collect_files_in_path(worktree, &entry.path));
            } else if entry.is_file() {
                files.push((
                    entry.path.clone(),
                    worktree
                        .full_path(&entry.path)
                        .to_string_lossy()
                        .to_string(),
                ));
            }
        }

        files
    }

    let Some(project_path) = project
        .read(cx)
        .project_path_for_absolute_path(&abs_path, cx)
    else {
        return Task::ready(Err(anyhow!("project path not found")));
    };
    let Some(entry) = project.read(cx).entry_for_path(&project_path, cx) else {
        return Task::ready(Err(anyhow!("project entry not found")));
    };
    let directory_path = entry.path.clone();
    let worktree_id = project_path.worktree_id;
    let Some(worktree) = project.read(cx).worktree_for_id(worktree_id, cx) else {
        return Task::ready(Err(anyhow!("worktree not found")));
    };
    let project = project.clone();
    cx.spawn(async move |cx| {
        let file_paths = worktree.read_with(cx, |worktree, _cx| {
            collect_files_in_path(worktree, &directory_path)
        });
        let descendants_future = cx.update(|cx| {
            futures::future::join_all(file_paths.into_iter().map(
                |(worktree_path, full_path): (Arc<RelPath>, String)| {
                    let rel_path = worktree_path
                        .strip_prefix(&directory_path)
                        .log_err()
                        .map_or_else(|| worktree_path.clone(), |rel_path| rel_path.into());

                    let open_task = project.update(cx, |project, cx| {
                        project.buffer_store().update(cx, |buffer_store, cx| {
                            let project_path = ProjectPath {
                                worktree_id,
                                path: worktree_path,
                            };
                            buffer_store.open_buffer(project_path, cx)
                        })
                    });

                    cx.spawn(async move |cx| {
                        let buffer = open_task.await.log_err()?;
                        let buffer_content = outline::get_buffer_content_or_outline(
                            buffer.clone(),
                            Some(&full_path),
                            &cx,
                        )
                        .await
                        .ok()?;

                        Some((rel_path, full_path, buffer_content.text, buffer))
                    })
                },
            ))
        });

        let contents = cx
            .background_spawn(async move {
                let (contents, tracked_buffers): (Vec<_>, Vec<_>) = descendants_future
                    .await
                    .into_iter()
                    .flatten()
                    .map(|(rel_path, full_path, rope, buffer)| {
                        ((rel_path, full_path, rope), buffer)
                    })
                    .unzip();
                Mention::Text {
                    content: render_directory_contents(contents),
                    tracked_buffers,
                }
            })
            .await;
        anyhow::Ok(contents)
    })
}

fn render_directory_contents(entries: Vec<(Arc<RelPath>, String, String)>) -> String {
    let mut output = String::new();
    for (_relative_path, full_path, content) in entries {
        let fence = codeblock_fence_for_path(Some(&full_path), None);
        write!(output, "\n{fence}\n{content}\n```").unwrap();
    }
    output
}

fn render_mention_fold_button(
    label: SharedString,
    icon: SharedString,
    tooltip: Option<SharedString>,
    mention_uri: Option<MentionUri>,
    workspace: Option<WeakEntity<Workspace>>,
    range: Range<Anchor>,
    mut loading_finished: postage::barrier::Receiver,
    image_task: Option<Shared<Task<Result<Arc<Image>, String>>>>,
    editor: WeakEntity<Editor>,
    cx: &mut App,
) -> (
    Arc<dyn Send + Sync + Fn(FoldId, Range<Anchor>, &mut App) -> AnyElement>,
    Entity<LoadingContext>,
) {
    let loading = cx.new(|cx| {
        let loading = cx.spawn(async move |this, cx| {
            loading_finished.recv().await;
            this.update(cx, |this: &mut LoadingContext, cx| {
                this.loading = None;
                cx.notify();
            })
            .ok();
        });
        LoadingContext {
            id: cx.entity_id(),
            label,
            icon,
            tooltip,
            mention_uri: mention_uri.clone(),
            workspace: workspace.clone(),
            range,
            editor,
            loading: Some(loading),
            image: image_task.clone(),
        }
    });
    let loading_clone = loading.clone();
    let render: Arc<dyn Send + Sync + Fn(FoldId, Range<Anchor>, &mut App) -> AnyElement> =
        Arc::new(move |_fold_id, _fold_range, _cx| loading_clone.clone().into_any_element());
    (render, loading)
}

pub struct LoadingContext {
    id: EntityId,
    label: SharedString,
    icon: SharedString,
    tooltip: Option<SharedString>,
    mention_uri: Option<MentionUri>,
    workspace: Option<WeakEntity<Workspace>>,
    range: Range<Anchor>,
    editor: WeakEntity<Editor>,
    loading: Option<Task<()>>,
    image: Option<Shared<Task<Result<Arc<Image>, String>>>>,
}

impl Render for LoadingContext {
    fn render(&mut self, _window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
        let is_in_text_selection = self
            .editor
            .update(cx, |editor, cx| editor.is_range_selected(&self.range, cx))
            .unwrap_or_default();

        let id = ElementId::from(("loading_context", self.id));

        MentionCrease::new(id, self.icon.clone(), self.label.clone())
            .mention_uri(self.mention_uri.clone())
            .workspace(self.workspace.clone())
            .is_toggled(is_in_text_selection)
            .is_loading(self.loading.is_some())
            .when_some(self.tooltip.clone(), |this, tooltip_text| {
                this.tooltip(tooltip_text)
            })
            .when_some(self.image.clone(), |this, image_task| {
                this.image_preview(move |_, cx| {
                    let image = image_task.peek().cloned().transpose().ok().flatten();
                    let image_task = image_task.clone();
                    cx.new::<ImageHover>(|cx| ImageHover {
                        image,
                        _task: cx.spawn(async move |this, cx| {
                            if let Ok(image) = image_task.clone().await {
                                this.update(cx, |this, cx| {
                                    if this.image.replace(image).is_none() {
                                        cx.notify();
                                    }
                                })
                                .ok();
                            }
                        }),
                    })
                    .into()
                })
            })
    }
}

struct ImageHover {
    image: Option<Arc<Image>>,
    _task: Task<()>,
}

impl Render for ImageHover {
    fn render(&mut self, _window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
        if let Some(image) = self.image.clone() {
            div()
                .p_1p5()
                .elevation_2(cx)
                .child(gpui::img(image).h_auto().max_w_96().rounded_sm())
                .into_any_element()
        } else {
            gpui::Empty.into_any_element()
        }
    }
}

async fn fetch_url_content(http_client: Arc<HttpClientWithUrl>, url: String) -> Result<String> {
    #[derive(Debug, PartialEq, Eq, PartialOrd, Ord, Hash, Clone, Copy)]
    enum ContentType {
        Html,
        Plaintext,
        Json,
    }
    use html_to_markdown::{TagHandler, convert_html_to_markdown, markdown};

    let url = if !url.starts_with("https://") && !url.starts_with("http://") {
        format!("https://{url}")
    } else {
        url
    };

    let mut response = http_client.get(&url, AsyncBody::default(), true).await?;
    let mut body = Vec::new();
    response
        .body_mut()
        .read_to_end(&mut body)
        .await
        .context("error reading response body")?;

    if response.status().is_client_error() {
        let text = String::from_utf8_lossy(body.as_slice());
        anyhow::bail!(
            "status error {}, response: {text:?}",
            response.status().as_u16()
        );
    }

    let Some(content_type) = response.headers().get("content-type") else {
        anyhow::bail!("missing Content-Type header");
    };
    let content_type = content_type
        .to_str()
        .context("invalid Content-Type header")?;
    let content_type = match content_type {
        "text/html" => ContentType::Html,
        "text/plain" => ContentType::Plaintext,
        "application/json" => ContentType::Json,
        _ => ContentType::Html,
    };

    match content_type {
        ContentType::Html => {
            let mut handlers: Vec<TagHandler> = vec![
                Rc::new(RefCell::new(markdown::WebpageChromeRemover)),
                Rc::new(RefCell::new(markdown::ParagraphHandler)),
                Rc::new(RefCell::new(markdown::HeadingHandler)),
                Rc::new(RefCell::new(markdown::ListHandler)),
                Rc::new(RefCell::new(markdown::TableHandler::new())),
                Rc::new(RefCell::new(markdown::StyledTextHandler)),
            ];
            if url.contains("wikipedia.org") {
                use html_to_markdown::structure::wikipedia;

                handlers.push(Rc::new(RefCell::new(wikipedia::WikipediaChromeRemover)));
                handlers.push(Rc::new(RefCell::new(wikipedia::WikipediaInfoboxHandler)));
                handlers.push(Rc::new(
                    RefCell::new(wikipedia::WikipediaCodeHandler::new()),
                ));
            } else {
                handlers.push(Rc::new(RefCell::new(markdown::CodeHandler)));
            }
            convert_html_to_markdown(&body[..], &mut handlers)
        }
        ContentType::Plaintext => Ok(std::str::from_utf8(&body)?.to_owned()),
        ContentType::Json => {
            let json: serde_json::Value = serde_json::from_slice(&body)?;

            Ok(format!(
                "```json\n{}\n```",
                serde_json::to_string_pretty(&json)?
            ))
        }
    }
}
