use crate::assistant_configuration::{ConfigurationView, ConfigurationViewEvent};
use crate::{
    terminal_inline_assistant::TerminalInlineAssistant, DeployHistory, InlineAssistant, NewChat,
};
use anyhow::{anyhow, Result};
use assistant_context_editor::{
    make_lsp_adapter_delegate, AssistantContext, AssistantPanelDelegate, ContextEditor,
    ContextEditorToolbarItem, ContextEditorToolbarItemEvent, ContextHistory, ContextId,
    ContextStore, ContextStoreEvent, InsertDraggedFiles, SlashCommandCompletionProvider,
    DEFAULT_TAB_TITLE,
};
use assistant_settings::{AssistantDockPosition, AssistantSettings};
use assistant_slash_command::SlashCommandWorkingSet;
use client::{proto, Client, Status};
use editor::{Editor, EditorEvent};
use fs::Fs;
use gpui::{
    prelude::*, Action, App, AsyncWindowContext, Entity, EventEmitter, ExternalPaths, FocusHandle,
    Focusable, InteractiveElement, IntoElement, ParentElement, Pixels, Render, Styled,
    Subscription, Task, UpdateGlobal, WeakEntity,
};
use language::LanguageRegistry;
use language_model::{
    AuthenticateError, LanguageModelProviderId, LanguageModelRegistry, ZED_CLOUD_PROVIDER_ID,
};
use project::Project;
use prompt_library::{open_prompt_library, PromptLibrary};
use prompt_store::PromptBuilder;
use search::{buffer_search::DivRegistrar, BufferSearchBar};
use settings::{update_settings_file, Settings};
use smol::stream::StreamExt;
use std::{ops::ControlFlow, path::PathBuf, sync::Arc};
use terminal_view::{terminal_panel::TerminalPanel, TerminalView};
use ui::{prelude::*, ContextMenu, PopoverMenu, Tooltip};
use util::{maybe, ResultExt};
use workspace::DraggedTab;
use workspace::{
    dock::{DockPosition, Panel, PanelEvent},
    pane, DraggedSelection, Pane, ShowConfiguration, ToggleZoom, Workspace,
};
use zed_actions::assistant::{DeployPromptLibrary, InlineAssist, ToggleFocus};

pub fn init(cx: &mut App) {
    workspace::FollowableViewRegistry::register::<ContextEditor>(cx);
    cx.observe_new(
        |workspace: &mut Workspace, _window, _cx: &mut Context<Workspace>| {
            workspace
                .register_action(ContextEditor::quote_selection)
                .register_action(ContextEditor::insert_selection)
                .register_action(ContextEditor::copy_code)
                .register_action(ContextEditor::insert_dragged_files)
                .register_action(AssistantPanel::show_configuration)
                .register_action(AssistantPanel::create_new_context)
                .register_action(AssistantPanel::restart_context_servers);
        },
    )
    .detach();

    cx.observe_new(
        |terminal_panel: &mut TerminalPanel, _, cx: &mut Context<TerminalPanel>| {
            let settings = AssistantSettings::get_global(cx);
            terminal_panel.set_assistant_enabled(settings.enabled, cx);
        },
    )
    .detach();
}

pub enum AssistantPanelEvent {
    ContextEdited,
}

pub struct AssistantPanel {
    pane: Entity<Pane>,
    workspace: WeakEntity<Workspace>,
    width: Option<Pixels>,
    height: Option<Pixels>,
    project: Entity<Project>,
    context_store: Entity<ContextStore>,
    languages: Arc<LanguageRegistry>,
    fs: Arc<dyn Fs>,
    subscriptions: Vec<Subscription>,
    model_summary_editor: Entity<Editor>,
    authenticate_provider_task: Option<(LanguageModelProviderId, Task<()>)>,
    configuration_subscription: Option<Subscription>,
    client_status: Option<client::Status>,
    watch_client_status: Option<Task<()>>,
    pub(crate) show_zed_ai_notice: bool,
}

enum InlineAssistTarget {
    Editor(Entity<Editor>, bool),
    Terminal(Entity<TerminalView>),
}

impl AssistantPanel {
    pub fn load(
        workspace: WeakEntity<Workspace>,
        prompt_builder: Arc<PromptBuilder>,
        cx: AsyncWindowContext,
    ) -> Task<Result<Entity<Self>>> {
        cx.spawn(|mut cx| async move {
            let slash_commands = Arc::new(SlashCommandWorkingSet::default());
            let context_store = workspace
                .update(&mut cx, |workspace, cx| {
                    let project = workspace.project().clone();
                    ContextStore::new(project, prompt_builder.clone(), slash_commands, cx)
                })?
                .await?;

            workspace.update_in(&mut cx, |workspace, window, cx| {
                // TODO: deserialize state.
                cx.new(|cx| Self::new(workspace, context_store, window, cx))
            })
        })
    }

    fn new(
        workspace: &Workspace,
        context_store: Entity<ContextStore>,
        window: &mut Window,
        cx: &mut Context<Self>,
    ) -> Self {
        let model_summary_editor = cx.new(|cx| Editor::single_line(window, cx));
        let context_editor_toolbar =
            cx.new(|_| ContextEditorToolbarItem::new(model_summary_editor.clone()));

        let pane = cx.new(|cx| {
            let mut pane = Pane::new(
                workspace.weak_handle(),
                workspace.project().clone(),
                Default::default(),
                None,
                NewChat.boxed_clone(),
                window,
                cx,
            );

            let project = workspace.project().clone();
            pane.set_custom_drop_handle(cx, move |_, dropped_item, window, cx| {
                let action = maybe!({
                    if project.read(cx).is_local() {
                        if let Some(paths) = dropped_item.downcast_ref::<ExternalPaths>() {
                            return Some(InsertDraggedFiles::ExternalFiles(paths.paths().to_vec()));
                        }
                    }

                    let project_paths = if let Some(tab) = dropped_item.downcast_ref::<DraggedTab>()
                    {
                        if tab.pane == cx.entity() {
                            return None;
                        }
                        let item = tab.pane.read(cx).item_for_index(tab.ix);
                        Some(
                            item.and_then(|item| item.project_path(cx))
                                .into_iter()
                                .collect::<Vec<_>>(),
                        )
                    } else if let Some(selection) = dropped_item.downcast_ref::<DraggedSelection>()
                    {
                        Some(
                            selection
                                .items()
                                .filter_map(|item| {
                                    project.read(cx).path_for_entry(item.entry_id, cx)
                                })
                                .collect::<Vec<_>>(),
                        )
                    } else {
                        None
                    }?;

                    let paths = project_paths
                        .into_iter()
                        .filter_map(|project_path| {
                            let worktree = project
                                .read(cx)
                                .worktree_for_id(project_path.worktree_id, cx)?;

                            let mut full_path = PathBuf::from(worktree.read(cx).root_name());
                            full_path.push(&project_path.path);
                            Some(full_path)
                        })
                        .collect::<Vec<_>>();

                    Some(InsertDraggedFiles::ProjectPaths(paths))
                });

                if let Some(action) = action {
                    window.dispatch_action(action.boxed_clone(), cx);
                }

                ControlFlow::Break(())
            });

            pane.set_can_navigate(true, cx);
            pane.display_nav_history_buttons(None);
            pane.set_should_display_tab_bar(|_, _| true);
            pane.set_render_tab_bar_buttons(cx, move |pane, _window, cx| {
                let focus_handle = pane.focus_handle(cx);
                let left_children = IconButton::new("history", IconName::HistoryRerun)
                    .icon_size(IconSize::Small)
                    .on_click(cx.listener({
                        let focus_handle = focus_handle.clone();
                        move |_, _, window, cx| {
                            focus_handle.focus(window);
                            window.dispatch_action(DeployHistory.boxed_clone(), cx)
                        }
                    }))
                    .tooltip({
                        let focus_handle = focus_handle.clone();
                        move |window, cx| {
                            Tooltip::for_action_in(
                                "Open History",
                                &DeployHistory,
                                &focus_handle,
                                window,
                                cx,
                            )
                        }
                    })
                    .toggle_state(
                        pane.active_item()
                            .map_or(false, |item| item.downcast::<ContextHistory>().is_some()),
                    );
                let _pane = cx.entity().clone();
                let right_children = h_flex()
                    .gap(DynamicSpacing::Base02.rems(cx))
                    .child(
                        IconButton::new("new-chat", IconName::Plus)
                            .icon_size(IconSize::Small)
                            .on_click(cx.listener(|_, _, window, cx| {
                                window.dispatch_action(NewChat.boxed_clone(), cx)
                            }))
                            .tooltip(move |window, cx| {
                                Tooltip::for_action_in(
                                    "New Chat",
                                    &NewChat,
                                    &focus_handle,
                                    window,
                                    cx,
                                )
                            }),
                    )
                    .child(
                        PopoverMenu::new("assistant-panel-popover-menu")
                            .trigger_with_tooltip(
                                IconButton::new("menu", IconName::EllipsisVertical)
                                    .icon_size(IconSize::Small),
                                Tooltip::text("Toggle Assistant Menu"),
                            )
                            .menu(move |window, cx| {
                                let zoom_label = if _pane.read(cx).is_zoomed() {
                                    "Zoom Out"
                                } else {
                                    "Zoom In"
                                };
                                let focus_handle = _pane.focus_handle(cx);
                                Some(ContextMenu::build(window, cx, move |menu, _, _| {
                                    menu.context(focus_handle.clone())
                                        .action("New Chat", Box::new(NewChat))
                                        .action("History", Box::new(DeployHistory))
                                        .action("Prompt Library", Box::new(DeployPromptLibrary))
                                        .action("Configure", Box::new(ShowConfiguration))
                                        .action(zoom_label, Box::new(ToggleZoom))
                                }))
                            }),
                    )
                    .into_any_element()
                    .into();

                (Some(left_children.into_any_element()), right_children)
            });
            pane.toolbar().update(cx, |toolbar, cx| {
                toolbar.add_item(context_editor_toolbar.clone(), window, cx);
                toolbar.add_item(
                    cx.new(|cx| {
                        BufferSearchBar::new(
                            Some(workspace.project().read(cx).languages().clone()),
                            window,
                            cx,
                        )
                    }),
                    window,
                    cx,
                )
            });
            pane
        });

        let subscriptions = vec![
            cx.observe(&pane, |_, _, cx| cx.notify()),
            cx.subscribe_in(&pane, window, Self::handle_pane_event),
            cx.subscribe(&context_editor_toolbar, Self::handle_toolbar_event),
            cx.subscribe(&model_summary_editor, Self::handle_summary_editor_event),
            cx.subscribe_in(&context_store, window, Self::handle_context_store_event),
            cx.subscribe_in(
                &LanguageModelRegistry::global(cx),
                window,
                |this, _, event: &language_model::Event, window, cx| match event {
                    language_model::Event::ActiveModelChanged => {
                        this.completion_provider_changed(window, cx);
                    }
                    language_model::Event::ProviderStateChanged => {
                        this.ensure_authenticated(window, cx);
                        cx.notify()
                    }
                    language_model::Event::AddedProvider(_)
                    | language_model::Event::RemovedProvider(_) => {
                        this.ensure_authenticated(window, cx);
                    }
                },
            ),
        ];

        let watch_client_status = Self::watch_client_status(workspace.client().clone(), window, cx);

        let mut this = Self {
            pane,
            workspace: workspace.weak_handle(),
            width: None,
            height: None,
            project: workspace.project().clone(),
            context_store,
            languages: workspace.app_state().languages.clone(),
            fs: workspace.app_state().fs.clone(),
            subscriptions,
            model_summary_editor,
            authenticate_provider_task: None,
            configuration_subscription: None,
            client_status: None,
            watch_client_status: Some(watch_client_status),
            show_zed_ai_notice: false,
        };
        this.new_context(window, cx);
        this
    }

    pub fn toggle_focus(
        workspace: &mut Workspace,
        _: &ToggleFocus,
        window: &mut Window,
        cx: &mut Context<Workspace>,
    ) {
        let settings = AssistantSettings::get_global(cx);
        if !settings.enabled {
            return;
        }

        workspace.toggle_panel_focus::<Self>(window, cx);
    }

    fn watch_client_status(
        client: Arc<Client>,
        window: &mut Window,
        cx: &mut Context<Self>,
    ) -> Task<()> {
        let mut status_rx = client.status();

        cx.spawn_in(window, |this, mut cx| async move {
            while let Some(status) = status_rx.next().await {
                this.update(&mut cx, |this, cx| {
                    if this.client_status.is_none()
                        || this
                            .client_status
                            .map_or(false, |old_status| old_status != status)
                    {
                        this.update_zed_ai_notice_visibility(status, cx);
                    }
                    this.client_status = Some(status);
                })
                .log_err();
            }
            this.update(&mut cx, |this, _cx| this.watch_client_status = None)
                .log_err();
        })
    }

    fn handle_pane_event(
        &mut self,
        pane: &Entity<Pane>,
        event: &pane::Event,
        window: &mut Window,
        cx: &mut Context<Self>,
    ) {
        let update_model_summary = match event {
            pane::Event::Remove { .. } => {
                cx.emit(PanelEvent::Close);
                false
            }
            pane::Event::ZoomIn => {
                cx.emit(PanelEvent::ZoomIn);
                false
            }
            pane::Event::ZoomOut => {
                cx.emit(PanelEvent::ZoomOut);
                false
            }

            pane::Event::AddItem { item } => {
                self.workspace
                    .update(cx, |workspace, cx| {
                        item.added_to_pane(workspace, self.pane.clone(), window, cx)
                    })
                    .ok();
                true
            }

            pane::Event::ActivateItem { local, .. } => {
                if *local {
                    self.workspace
                        .update(cx, |workspace, cx| {
                            workspace.unfollow_in_pane(&pane, window, cx);
                        })
                        .ok();
                }
                cx.emit(AssistantPanelEvent::ContextEdited);
                true
            }
            pane::Event::RemovedItem { .. } => {
                let has_configuration_view = self
                    .pane
                    .read(cx)
                    .items_of_type::<ConfigurationView>()
                    .next()
                    .is_some();

                if !has_configuration_view {
                    self.configuration_subscription = None;
                }

                cx.emit(AssistantPanelEvent::ContextEdited);
                true
            }

            _ => false,
        };

        if update_model_summary {
            if let Some(editor) = self.active_context_editor(cx) {
                self.show_updated_summary(&editor, window, cx)
            }
        }
    }

    fn handle_summary_editor_event(
        &mut self,
        model_summary_editor: Entity<Editor>,
        event: &EditorEvent,
        cx: &mut Context<Self>,
    ) {
        if matches!(event, EditorEvent::Edited { .. }) {
            if let Some(context_editor) = self.active_context_editor(cx) {
                let new_summary = model_summary_editor.read(cx).text(cx);
                context_editor.update(cx, |context_editor, cx| {
                    context_editor.context().update(cx, |context, cx| {
                        if context.summary().is_none()
                            && (new_summary == DEFAULT_TAB_TITLE || new_summary.trim().is_empty())
                        {
                            return;
                        }
                        context.custom_summary(new_summary, cx)
                    });
                });
            }
        }
    }

    fn update_zed_ai_notice_visibility(&mut self, client_status: Status, cx: &mut Context<Self>) {
        let active_provider = LanguageModelRegistry::read_global(cx).active_provider();

        // If we're signed out and don't have a provider configured, or we're signed-out AND Zed.dev is
        // the provider, we want to show a nudge to sign in.
        let show_zed_ai_notice = client_status.is_signed_out()
            && active_provider.map_or(true, |provider| provider.id().0 == ZED_CLOUD_PROVIDER_ID);

        self.show_zed_ai_notice = show_zed_ai_notice;
        cx.notify();
    }

    fn handle_toolbar_event(
        &mut self,
        _: Entity<ContextEditorToolbarItem>,
        _: &ContextEditorToolbarItemEvent,
        cx: &mut Context<Self>,
    ) {
        if let Some(context_editor) = self.active_context_editor(cx) {
            context_editor.update(cx, |context_editor, cx| {
                context_editor.context().update(cx, |context, cx| {
                    context.summarize(true, cx);
                })
            })
        }
    }

    fn handle_context_store_event(
        &mut self,
        _context_store: &Entity<ContextStore>,
        event: &ContextStoreEvent,
        window: &mut Window,
        cx: &mut Context<Self>,
    ) {
        let ContextStoreEvent::ContextCreated(context_id) = event;
        let Some(context) = self
            .context_store
            .read(cx)
            .loaded_context_for_id(&context_id, cx)
        else {
            log::error!("no context found with ID: {}", context_id.to_proto());
            return;
        };
        let lsp_adapter_delegate = make_lsp_adapter_delegate(&self.project, cx)
            .log_err()
            .flatten();

        let editor = cx.new(|cx| {
            let mut editor = ContextEditor::for_context(
                context,
                self.fs.clone(),
                self.workspace.clone(),
                self.project.clone(),
                lsp_adapter_delegate,
                window,
                cx,
            );
            editor.insert_default_prompt(window, cx);
            editor
        });

        self.show_context(editor.clone(), window, cx);
    }

    fn completion_provider_changed(&mut self, window: &mut Window, cx: &mut Context<Self>) {
        if let Some(editor) = self.active_context_editor(cx) {
            editor.update(cx, |active_context, cx| {
                active_context
                    .context()
                    .update(cx, |context, cx| context.completion_provider_changed(cx))
            })
        }

        let Some(new_provider_id) = LanguageModelRegistry::read_global(cx)
            .active_provider()
            .map(|p| p.id())
        else {
            return;
        };

        if self
            .authenticate_provider_task
            .as_ref()
            .map_or(true, |(old_provider_id, _)| {
                *old_provider_id != new_provider_id
            })
        {
            self.authenticate_provider_task = None;
            self.ensure_authenticated(window, cx);
        }

        if let Some(status) = self.client_status {
            self.update_zed_ai_notice_visibility(status, cx);
        }
    }

    fn ensure_authenticated(&mut self, window: &mut Window, cx: &mut Context<Self>) {
        if self.is_authenticated(cx) {
            return;
        }

        let Some(provider) = LanguageModelRegistry::read_global(cx).active_provider() else {
            return;
        };

        let load_credentials = self.authenticate(cx);

        if self.authenticate_provider_task.is_none() {
            self.authenticate_provider_task = Some((
                provider.id(),
                cx.spawn_in(window, |this, mut cx| async move {
                    if let Some(future) = load_credentials {
                        let _ = future.await;
                    }
                    this.update(&mut cx, |this, _cx| {
                        this.authenticate_provider_task = None;
                    })
                    .log_err();
                }),
            ));
        }
    }

    pub fn inline_assist(
        workspace: &mut Workspace,
        action: &InlineAssist,
        window: &mut Window,
        cx: &mut Context<Workspace>,
    ) {
        let settings = AssistantSettings::get_global(cx);
        if !settings.enabled {
            return;
        }

        let Some(assistant_panel) = workspace.panel::<AssistantPanel>(cx) else {
            return;
        };

        let Some(inline_assist_target) =
            Self::resolve_inline_assist_target(workspace, &assistant_panel, window, cx)
        else {
            return;
        };

        let initial_prompt = action.prompt.clone();

        if assistant_panel.update(cx, |assistant, cx| assistant.is_authenticated(cx)) {
            match inline_assist_target {
                InlineAssistTarget::Editor(active_editor, include_context) => {
                    InlineAssistant::update_global(cx, |assistant, cx| {
                        assistant.assist(
                            &active_editor,
                            Some(cx.entity().downgrade()),
                            include_context.then_some(&assistant_panel),
                            initial_prompt,
                            window,
                            cx,
                        )
                    })
                }
                InlineAssistTarget::Terminal(active_terminal) => {
                    TerminalInlineAssistant::update_global(cx, |assistant, cx| {
                        assistant.assist(
                            &active_terminal,
                            Some(cx.entity().downgrade()),
                            Some(&assistant_panel),
                            initial_prompt,
                            window,
                            cx,
                        )
                    })
                }
            }
        } else {
            let assistant_panel = assistant_panel.downgrade();
            cx.spawn_in(window, |workspace, mut cx| async move {
                let Some(task) =
                    assistant_panel.update(&mut cx, |assistant, cx| assistant.authenticate(cx))?
                else {
                    let answer = cx
                        .prompt(
                            gpui::PromptLevel::Warning,
                            "No language model provider configured",
                            None,
                            &["Configure", "Cancel"],
                        )
                        .await
                        .ok();
                    if let Some(answer) = answer {
                        if answer == 0 {
                            cx.update(|window, cx| {
                                window.dispatch_action(Box::new(ShowConfiguration), cx)
                            })
                            .ok();
                        }
                    }
                    return Ok(());
                };
                task.await?;
                if assistant_panel.update(&mut cx, |panel, cx| panel.is_authenticated(cx))? {
                    cx.update(|window, cx| match inline_assist_target {
                        InlineAssistTarget::Editor(active_editor, include_context) => {
                            let assistant_panel = if include_context {
                                assistant_panel.upgrade()
                            } else {
                                None
                            };
                            InlineAssistant::update_global(cx, |assistant, cx| {
                                assistant.assist(
                                    &active_editor,
                                    Some(workspace),
                                    assistant_panel.as_ref(),
                                    initial_prompt,
                                    window,
                                    cx,
                                )
                            })
                        }
                        InlineAssistTarget::Terminal(active_terminal) => {
                            TerminalInlineAssistant::update_global(cx, |assistant, cx| {
                                assistant.assist(
                                    &active_terminal,
                                    Some(workspace),
                                    assistant_panel.upgrade().as_ref(),
                                    initial_prompt,
                                    window,
                                    cx,
                                )
                            })
                        }
                    })?
                } else {
                    workspace.update_in(&mut cx, |workspace, window, cx| {
                        workspace.focus_panel::<AssistantPanel>(window, cx)
                    })?;
                }

                anyhow::Ok(())
            })
            .detach_and_log_err(cx)
        }
    }

    fn resolve_inline_assist_target(
        workspace: &mut Workspace,
        assistant_panel: &Entity<AssistantPanel>,
        window: &mut Window,
        cx: &mut App,
    ) -> Option<InlineAssistTarget> {
        if let Some(terminal_panel) = workspace.panel::<TerminalPanel>(cx) {
            if terminal_panel
                .read(cx)
                .focus_handle(cx)
                .contains_focused(window, cx)
            {
                if let Some(terminal_view) = terminal_panel.read(cx).pane().and_then(|pane| {
                    pane.read(cx)
                        .active_item()
                        .and_then(|t| t.downcast::<TerminalView>())
                }) {
                    return Some(InlineAssistTarget::Terminal(terminal_view));
                }
            }
        }
        let context_editor =
            assistant_panel
                .read(cx)
                .active_context_editor(cx)
                .and_then(|editor| {
                    let editor = &editor.read(cx).editor().clone();
                    if editor.read(cx).is_focused(window) {
                        Some(editor.clone())
                    } else {
                        None
                    }
                });

        if let Some(context_editor) = context_editor {
            Some(InlineAssistTarget::Editor(context_editor, false))
        } else if let Some(workspace_editor) = workspace
            .active_item(cx)
            .and_then(|item| item.act_as::<Editor>(cx))
        {
            Some(InlineAssistTarget::Editor(workspace_editor, true))
        } else if let Some(terminal_view) = workspace
            .active_item(cx)
            .and_then(|item| item.act_as::<TerminalView>(cx))
        {
            Some(InlineAssistTarget::Terminal(terminal_view))
        } else {
            None
        }
    }

    pub fn create_new_context(
        workspace: &mut Workspace,
        _: &NewChat,
        window: &mut Window,
        cx: &mut Context<Workspace>,
    ) {
        if let Some(panel) = workspace.panel::<AssistantPanel>(cx) {
            let did_create_context = panel
                .update(cx, |panel, cx| {
                    panel.new_context(window, cx)?;

                    Some(())
                })
                .is_some();
            if did_create_context {
                ContextEditor::quote_selection(workspace, &Default::default(), window, cx);
            }
        }
    }

    pub fn new_context(
        &mut self,
        window: &mut Window,
        cx: &mut Context<Self>,
    ) -> Option<Entity<ContextEditor>> {
        let project = self.project.read(cx);
        if project.is_via_collab() {
            let task = self
                .context_store
                .update(cx, |store, cx| store.create_remote_context(cx));

            cx.spawn_in(window, |this, mut cx| async move {
                let context = task.await?;

                this.update_in(&mut cx, |this, window, cx| {
                    let workspace = this.workspace.clone();
                    let project = this.project.clone();
                    let lsp_adapter_delegate =
                        make_lsp_adapter_delegate(&project, cx).log_err().flatten();

                    let fs = this.fs.clone();
                    let project = this.project.clone();

                    let editor = cx.new(|cx| {
                        ContextEditor::for_context(
                            context,
                            fs,
                            workspace,
                            project,
                            lsp_adapter_delegate,
                            window,
                            cx,
                        )
                    });

                    this.show_context(editor, window, cx);

                    anyhow::Ok(())
                })??;

                anyhow::Ok(())
            })
            .detach_and_log_err(cx);

            None
        } else {
            let context = self.context_store.update(cx, |store, cx| store.create(cx));
            let lsp_adapter_delegate = make_lsp_adapter_delegate(&self.project, cx)
                .log_err()
                .flatten();

            let editor = cx.new(|cx| {
                let mut editor = ContextEditor::for_context(
                    context,
                    self.fs.clone(),
                    self.workspace.clone(),
                    self.project.clone(),
                    lsp_adapter_delegate,
                    window,
                    cx,
                );
                editor.insert_default_prompt(window, cx);
                editor
            });

            self.show_context(editor.clone(), window, cx);
            let workspace = self.workspace.clone();
            cx.spawn_in(window, move |_, mut cx| async move {
                workspace
                    .update_in(&mut cx, |workspace, window, cx| {
                        workspace.focus_panel::<AssistantPanel>(window, cx);
                    })
                    .ok();
            })
            .detach();
            Some(editor)
        }
    }

    fn show_context(
        &mut self,
        context_editor: Entity<ContextEditor>,
        window: &mut Window,
        cx: &mut Context<Self>,
    ) {
        let focus = self.focus_handle(cx).contains_focused(window, cx);
        let prev_len = self.pane.read(cx).items_len();
        self.pane.update(cx, |pane, cx| {
            pane.add_item(
                Box::new(context_editor.clone()),
                focus,
                focus,
                None,
                window,
                cx,
            )
        });

        if prev_len != self.pane.read(cx).items_len() {
            self.subscriptions.push(cx.subscribe_in(
                &context_editor,
                window,
                Self::handle_context_editor_event,
            ));
        }

        self.show_updated_summary(&context_editor, window, cx);

        cx.emit(AssistantPanelEvent::ContextEdited);
        cx.notify();
    }

    fn show_updated_summary(
        &self,
        context_editor: &Entity<ContextEditor>,
        window: &mut Window,
        cx: &mut Context<Self>,
    ) {
        context_editor.update(cx, |context_editor, cx| {
            let new_summary = context_editor.title(cx).to_string();
            self.model_summary_editor.update(cx, |summary_editor, cx| {
                if summary_editor.text(cx) != new_summary {
                    summary_editor.set_text(new_summary, window, cx);
                }
            });
        });
    }

    fn handle_context_editor_event(
        &mut self,
        context_editor: &Entity<ContextEditor>,
        event: &EditorEvent,
        window: &mut Window,
        cx: &mut Context<Self>,
    ) {
        match event {
            EditorEvent::TitleChanged => {
                self.show_updated_summary(&context_editor, window, cx);
                cx.notify()
            }
            EditorEvent::Edited { .. } => {
                self.workspace
                    .update(cx, |workspace, cx| {
                        let is_via_ssh = workspace
                            .project()
                            .update(cx, |project, _| project.is_via_ssh());

                        workspace
                            .client()
                            .telemetry()
                            .log_edit_event("assistant panel", is_via_ssh);
                    })
                    .log_err();
                cx.emit(AssistantPanelEvent::ContextEdited)
            }
            _ => {}
        }
    }

    fn show_configuration(
        workspace: &mut Workspace,
        _: &ShowConfiguration,
        window: &mut Window,
        cx: &mut Context<Workspace>,
    ) {
        let Some(panel) = workspace.panel::<AssistantPanel>(cx) else {
            return;
        };

        if !panel.focus_handle(cx).contains_focused(window, cx) {
            workspace.toggle_panel_focus::<AssistantPanel>(window, cx);
        }

        panel.update(cx, |this, cx| {
            this.show_configuration_tab(window, cx);
        })
    }

    fn show_configuration_tab(&mut self, window: &mut Window, cx: &mut Context<Self>) {
        let configuration_item_ix = self
            .pane
            .read(cx)
            .items()
            .position(|item| item.downcast::<ConfigurationView>().is_some());

        if let Some(configuration_item_ix) = configuration_item_ix {
            self.pane.update(cx, |pane, cx| {
                pane.activate_item(configuration_item_ix, true, true, window, cx);
            });
        } else {
            let configuration = cx.new(|cx| ConfigurationView::new(window, cx));
            self.configuration_subscription = Some(cx.subscribe_in(
                &configuration,
                window,
                |this, _, event: &ConfigurationViewEvent, window, cx| match event {
                    ConfigurationViewEvent::NewProviderContextEditor(provider) => {
                        if LanguageModelRegistry::read_global(cx)
                            .active_provider()
                            .map_or(true, |p| p.id() != provider.id())
                        {
                            if let Some(model) = provider.default_model(cx) {
                                update_settings_file::<AssistantSettings>(
                                    this.fs.clone(),
                                    cx,
                                    move |settings, _| settings.set_model(model),
                                );
                            }
                        }

                        this.new_context(window, cx);
                    }
                },
            ));
            self.pane.update(cx, |pane, cx| {
                pane.add_item(Box::new(configuration), true, true, None, window, cx);
            });
        }
    }

    fn deploy_history(&mut self, _: &DeployHistory, window: &mut Window, cx: &mut Context<Self>) {
        let history_item_ix = self
            .pane
            .read(cx)
            .items()
            .position(|item| item.downcast::<ContextHistory>().is_some());

        if let Some(history_item_ix) = history_item_ix {
            self.pane.update(cx, |pane, cx| {
                pane.activate_item(history_item_ix, true, true, window, cx);
            });
        } else {
            let history = cx.new(|cx| {
                ContextHistory::new(
                    self.project.clone(),
                    self.context_store.clone(),
                    self.workspace.clone(),
                    window,
                    cx,
                )
            });
            self.pane.update(cx, |pane, cx| {
                pane.add_item(Box::new(history), true, true, None, window, cx);
            });
        }
    }

    fn deploy_prompt_library(
        &mut self,
        _: &DeployPromptLibrary,
        _window: &mut Window,
        cx: &mut Context<Self>,
    ) {
        open_prompt_library(
            self.languages.clone(),
            Box::new(PromptLibraryInlineAssist),
            Arc::new(|| {
                Box::new(SlashCommandCompletionProvider::new(
                    Arc::new(SlashCommandWorkingSet::default()),
                    None,
                    None,
                ))
            }),
            cx,
        )
        .detach_and_log_err(cx);
    }

    pub(crate) fn active_context_editor(&self, cx: &App) -> Option<Entity<ContextEditor>> {
        self.pane
            .read(cx)
            .active_item()?
            .downcast::<ContextEditor>()
    }

    pub fn active_context(&self, cx: &App) -> Option<Entity<AssistantContext>> {
        Some(self.active_context_editor(cx)?.read(cx).context().clone())
    }

    pub fn open_saved_context(
        &mut self,
        path: PathBuf,
        window: &mut Window,
        cx: &mut Context<Self>,
    ) -> Task<Result<()>> {
        let existing_context = self.pane.read(cx).items().find_map(|item| {
            item.downcast::<ContextEditor>()
                .filter(|editor| editor.read(cx).context().read(cx).path() == Some(&path))
        });
        if let Some(existing_context) = existing_context {
            return cx.spawn_in(window, |this, mut cx| async move {
                this.update_in(&mut cx, |this, window, cx| {
                    this.show_context(existing_context, window, cx)
                })
            });
        }

        let context = self
            .context_store
            .update(cx, |store, cx| store.open_local_context(path.clone(), cx));
        let fs = self.fs.clone();
        let project = self.project.clone();
        let workspace = self.workspace.clone();

        let lsp_adapter_delegate = make_lsp_adapter_delegate(&project, cx).log_err().flatten();

        cx.spawn_in(window, |this, mut cx| async move {
            let context = context.await?;
            this.update_in(&mut cx, |this, window, cx| {
                let editor = cx.new(|cx| {
                    ContextEditor::for_context(
                        context,
                        fs,
                        workspace,
                        project,
                        lsp_adapter_delegate,
                        window,
                        cx,
                    )
                });
                this.show_context(editor, window, cx);
                anyhow::Ok(())
            })??;
            Ok(())
        })
    }

    pub fn open_remote_context(
        &mut self,
        id: ContextId,
        window: &mut Window,
        cx: &mut Context<Self>,
    ) -> Task<Result<Entity<ContextEditor>>> {
        let existing_context = self.pane.read(cx).items().find_map(|item| {
            item.downcast::<ContextEditor>()
                .filter(|editor| *editor.read(cx).context().read(cx).id() == id)
        });
        if let Some(existing_context) = existing_context {
            return cx.spawn_in(window, |this, mut cx| async move {
                this.update_in(&mut cx, |this, window, cx| {
                    this.show_context(existing_context.clone(), window, cx)
                })?;
                Ok(existing_context)
            });
        }

        let context = self
            .context_store
            .update(cx, |store, cx| store.open_remote_context(id, cx));
        let fs = self.fs.clone();
        let workspace = self.workspace.clone();
        let lsp_adapter_delegate = make_lsp_adapter_delegate(&self.project, cx)
            .log_err()
            .flatten();

        cx.spawn_in(window, |this, mut cx| async move {
            let context = context.await?;
            this.update_in(&mut cx, |this, window, cx| {
                let editor = cx.new(|cx| {
                    ContextEditor::for_context(
                        context,
                        fs,
                        workspace,
                        this.project.clone(),
                        lsp_adapter_delegate,
                        window,
                        cx,
                    )
                });
                this.show_context(editor.clone(), window, cx);
                anyhow::Ok(editor)
            })?
        })
    }

    fn is_authenticated(&mut self, cx: &mut Context<Self>) -> bool {
        LanguageModelRegistry::read_global(cx)
            .active_provider()
            .map_or(false, |provider| provider.is_authenticated(cx))
    }

    fn authenticate(
        &mut self,
        cx: &mut Context<Self>,
    ) -> Option<Task<Result<(), AuthenticateError>>> {
        LanguageModelRegistry::read_global(cx)
            .active_provider()
            .map_or(None, |provider| Some(provider.authenticate(cx)))
    }

    fn restart_context_servers(
        workspace: &mut Workspace,
        _action: &context_server::Restart,
        _: &mut Window,
        cx: &mut Context<Workspace>,
    ) {
        let Some(assistant_panel) = workspace.panel::<AssistantPanel>(cx) else {
            return;
        };

        assistant_panel.update(cx, |assistant_panel, cx| {
            assistant_panel
                .context_store
                .update(cx, |context_store, cx| {
                    context_store.restart_context_servers(cx);
                });
        });
    }
}

impl Render for AssistantPanel {
    fn render(&mut self, _: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
        let mut registrar = DivRegistrar::new(
            |panel, _, cx| {
                panel
                    .pane
                    .read(cx)
                    .toolbar()
                    .read(cx)
                    .item_of_type::<BufferSearchBar>()
            },
            cx,
        );
        BufferSearchBar::register(&mut registrar);
        let registrar = registrar.into_div();

        v_flex()
            .key_context("AssistantPanel")
            .size_full()
            .on_action(cx.listener(|this, _: &NewChat, window, cx| {
                this.new_context(window, cx);
            }))
            .on_action(cx.listener(|this, _: &ShowConfiguration, window, cx| {
                this.show_configuration_tab(window, cx)
            }))
            .on_action(cx.listener(AssistantPanel::deploy_history))
            .on_action(cx.listener(AssistantPanel::deploy_prompt_library))
            .child(registrar.size_full().child(self.pane.clone()))
            .into_any_element()
    }
}

impl Panel for AssistantPanel {
    fn persistent_name() -> &'static str {
        "AssistantPanel"
    }

    fn position(&self, _: &Window, cx: &App) -> DockPosition {
        match AssistantSettings::get_global(cx).dock {
            AssistantDockPosition::Left => DockPosition::Left,
            AssistantDockPosition::Bottom => DockPosition::Bottom,
            AssistantDockPosition::Right => DockPosition::Right,
        }
    }

    fn position_is_valid(&self, _: DockPosition) -> bool {
        true
    }

    fn set_position(&mut self, position: DockPosition, _: &mut Window, cx: &mut Context<Self>) {
        settings::update_settings_file::<AssistantSettings>(
            self.fs.clone(),
            cx,
            move |settings, _| {
                let dock = match position {
                    DockPosition::Left => AssistantDockPosition::Left,
                    DockPosition::Bottom => AssistantDockPosition::Bottom,
                    DockPosition::Right => AssistantDockPosition::Right,
                };
                settings.set_dock(dock);
            },
        );
    }

    fn size(&self, window: &Window, cx: &App) -> Pixels {
        let settings = AssistantSettings::get_global(cx);
        match self.position(window, cx) {
            DockPosition::Left | DockPosition::Right => {
                self.width.unwrap_or(settings.default_width)
            }
            DockPosition::Bottom => self.height.unwrap_or(settings.default_height),
        }
    }

    fn set_size(&mut self, size: Option<Pixels>, window: &mut Window, cx: &mut Context<Self>) {
        match self.position(window, cx) {
            DockPosition::Left | DockPosition::Right => self.width = size,
            DockPosition::Bottom => self.height = size,
        }
        cx.notify();
    }

    fn is_zoomed(&self, _: &Window, cx: &App) -> bool {
        self.pane.read(cx).is_zoomed()
    }

    fn set_zoomed(&mut self, zoomed: bool, _: &mut Window, cx: &mut Context<Self>) {
        self.pane.update(cx, |pane, cx| pane.set_zoomed(zoomed, cx));
    }

    fn set_active(&mut self, active: bool, window: &mut Window, cx: &mut Context<Self>) {
        if active {
            if self.pane.read(cx).items_len() == 0 {
                self.new_context(window, cx);
            }

            self.ensure_authenticated(window, cx);
        }
    }

    fn pane(&self) -> Option<Entity<Pane>> {
        Some(self.pane.clone())
    }

    fn remote_id() -> Option<proto::PanelId> {
        Some(proto::PanelId::AssistantPanel)
    }

    fn icon(&self, _: &Window, cx: &App) -> Option<IconName> {
        let settings = AssistantSettings::get_global(cx);
        if !settings.enabled || !settings.button {
            return None;
        }

        Some(IconName::ZedAssistant)
    }

    fn icon_tooltip(&self, _: &Window, _: &App) -> Option<&'static str> {
        Some("Assistant Panel")
    }

    fn toggle_action(&self) -> Box<dyn Action> {
        Box::new(ToggleFocus)
    }

    fn activation_priority(&self) -> u32 {
        4
    }
}

impl EventEmitter<PanelEvent> for AssistantPanel {}
impl EventEmitter<AssistantPanelEvent> for AssistantPanel {}

impl Focusable for AssistantPanel {
    fn focus_handle(&self, cx: &App) -> FocusHandle {
        self.pane.focus_handle(cx)
    }
}

struct PromptLibraryInlineAssist;

impl prompt_library::InlineAssistDelegate for PromptLibraryInlineAssist {
    fn assist(
        &self,
        prompt_editor: &Entity<Editor>,
        initial_prompt: Option<String>,
        window: &mut Window,
        cx: &mut Context<PromptLibrary>,
    ) {
        InlineAssistant::update_global(cx, |assistant, cx| {
            assistant.assist(&prompt_editor, None, None, initial_prompt, window, cx)
        })
    }

    fn focus_assistant_panel(
        &self,
        workspace: &mut Workspace,
        window: &mut Window,
        cx: &mut Context<Workspace>,
    ) -> bool {
        workspace
            .focus_panel::<AssistantPanel>(window, cx)
            .is_some()
    }
}

pub struct ConcreteAssistantPanelDelegate;

impl AssistantPanelDelegate for ConcreteAssistantPanelDelegate {
    fn active_context_editor(
        &self,
        workspace: &mut Workspace,
        _window: &mut Window,
        cx: &mut Context<Workspace>,
    ) -> Option<Entity<ContextEditor>> {
        let panel = workspace.panel::<AssistantPanel>(cx)?;
        panel.read(cx).active_context_editor(cx)
    }

    fn open_saved_context(
        &self,
        workspace: &mut Workspace,
        path: PathBuf,
        window: &mut Window,
        cx: &mut Context<Workspace>,
    ) -> Task<Result<()>> {
        let Some(panel) = workspace.panel::<AssistantPanel>(cx) else {
            return Task::ready(Err(anyhow!("no Assistant panel found")));
        };

        panel.update(cx, |panel, cx| panel.open_saved_context(path, window, cx))
    }

    fn open_remote_context(
        &self,
        workspace: &mut Workspace,
        context_id: ContextId,
        window: &mut Window,
        cx: &mut Context<Workspace>,
    ) -> Task<Result<Entity<ContextEditor>>> {
        let Some(panel) = workspace.panel::<AssistantPanel>(cx) else {
            return Task::ready(Err(anyhow!("no Assistant panel found")));
        };

        panel.update(cx, |panel, cx| {
            panel.open_remote_context(context_id, window, cx)
        })
    }

    fn quote_selection(
        &self,
        workspace: &mut Workspace,
        creases: Vec<(String, String)>,
        window: &mut Window,
        cx: &mut Context<Workspace>,
    ) {
        let Some(panel) = workspace.panel::<AssistantPanel>(cx) else {
            return;
        };

        if !panel.focus_handle(cx).contains_focused(window, cx) {
            workspace.toggle_panel_focus::<AssistantPanel>(window, cx);
        }

        panel.update(cx, |_, cx| {
            // Wait to create a new context until the workspace is no longer
            // being updated.
            cx.defer_in(window, move |panel, window, cx| {
                if let Some(context) = panel
                    .active_context_editor(cx)
                    .or_else(|| panel.new_context(window, cx))
                {
                    context.update(cx, |context, cx| context.quote_creases(creases, window, cx));
                };
            });
        });
    }
}

#[derive(Debug, PartialEq, Eq, Clone, Copy)]
pub enum WorkflowAssistStatus {
    Pending,
    Confirmed,
    Done,
    Idle,
}
