use std::collections::HashMap;
use std::path::PathBuf;
use std::time::Duration;

use anyhow::anyhow;
use anyhow::Context;
use anyhow::Result;
use dev_server_projects::{DevServer, DevServerId, DevServerProjectId};
use editor::Editor;
use gpui::pulsating_between;
use gpui::AsyncWindowContext;
use gpui::ClipboardItem;
use gpui::PathPromptOptions;
use gpui::Subscription;
use gpui::Task;
use gpui::WeakView;
use gpui::{
    Action, Animation, AnimationExt, AnyElement, AppContext, DismissEvent, EventEmitter,
    FocusHandle, FocusableView, Model, ScrollHandle, View, ViewContext,
};
use project::terminals::wrap_for_ssh;
use project::terminals::SshCommand;
use rpc::{proto::DevServerStatus, ErrorCode, ErrorExt};
use settings::update_settings_file;
use settings::Settings;
use task::HideStrategy;
use task::RevealStrategy;
use task::SpawnInTerminal;
use terminal_view::terminal_panel::TerminalPanel;
use ui::ElevationIndex;
use ui::Section;
use ui::{prelude::*, IconButtonShape, List, ListItem, Modal, ModalFooter, ModalHeader, Tooltip};
use ui_input::{FieldLabelLayout, TextField};
use util::ResultExt;
use workspace::OpenOptions;
use workspace::{notifications::DetachAndPromptErr, AppState, ModalView, Workspace};

use crate::open_dev_server_project;
use crate::ssh_connections::connect_over_ssh;
use crate::ssh_connections::open_ssh_project;
use crate::ssh_connections::RemoteSettingsContent;
use crate::ssh_connections::SshConnection;
use crate::ssh_connections::SshConnectionModal;
use crate::ssh_connections::SshProject;
use crate::ssh_connections::SshPrompt;
use crate::ssh_connections::SshSettings;
use crate::OpenRemote;

pub struct DevServerProjects {
    mode: Mode,
    focus_handle: FocusHandle,
    scroll_handle: ScrollHandle,
    dev_server_store: Model<dev_server_projects::Store>,
    workspace: WeakView<Workspace>,
    project_path_input: View<Editor>,
    dev_server_name_input: View<TextField>,
    _dev_server_subscription: Subscription,
}

#[derive(Default)]
struct CreateDevServer {
    creating: Option<Task<Option<()>>>,
    ssh_prompt: Option<View<SshPrompt>>,
}

struct CreateDevServerProject {
    dev_server_id: DevServerId,
    _opening: Option<Subscription>,
}

enum Mode {
    Default(Option<CreateDevServerProject>),
    CreateDevServer(CreateDevServer),
}

impl DevServerProjects {
    pub fn register(workspace: &mut Workspace, _: &mut ViewContext<Workspace>) {
        workspace.register_action(|workspace, _: &OpenRemote, cx| {
            let handle = cx.view().downgrade();
            workspace.toggle_modal(cx, |cx| Self::new(cx, handle))
        });
    }

    pub fn open(workspace: View<Workspace>, cx: &mut WindowContext) {
        workspace.update(cx, |workspace, cx| {
            let handle = cx.view().downgrade();
            workspace.toggle_modal(cx, |cx| Self::new(cx, handle))
        })
    }

    pub fn new(cx: &mut ViewContext<Self>, workspace: WeakView<Workspace>) -> Self {
        let project_path_input = cx.new_view(|cx| {
            let mut editor = Editor::single_line(cx);
            editor.set_placeholder_text("Project path (~/work/zed, /workspace/zed, …)", cx);
            editor
        });
        let dev_server_name_input = cx.new_view(|cx| {
            TextField::new(cx, "Name", "192.168.0.1").with_label(FieldLabelLayout::Hidden)
        });

        let focus_handle = cx.focus_handle();
        let dev_server_store = dev_server_projects::Store::global(cx);

        let subscription = cx.observe(&dev_server_store, |_, _, cx| {
            cx.notify();
        });

        let mut base_style = cx.text_style();
        base_style.refine(&gpui::TextStyleRefinement {
            color: Some(cx.theme().colors().editor_foreground),
            ..Default::default()
        });

        Self {
            mode: Mode::Default(None),
            focus_handle,
            scroll_handle: ScrollHandle::new(),
            dev_server_store,
            project_path_input,
            dev_server_name_input,
            workspace,
            _dev_server_subscription: subscription,
        }
    }

    pub fn create_dev_server_project(
        &mut self,
        dev_server_id: DevServerId,
        cx: &mut ViewContext<Self>,
    ) {
        let mut path = self.project_path_input.read(cx).text(cx).trim().to_string();

        if path.is_empty() {
            return;
        }

        if !path.starts_with('/') && !path.starts_with('~') {
            path = format!("~/{}", path);
        }

        if self
            .dev_server_store
            .read(cx)
            .projects_for_server(dev_server_id)
            .iter()
            .any(|p| p.paths.iter().any(|p| p == &path))
        {
            cx.spawn(|_, mut cx| async move {
                cx.prompt(
                    gpui::PromptLevel::Critical,
                    "Failed to create project",
                    Some(&format!("{} is already open on this dev server.", path)),
                    &["Ok"],
                )
                .await
            })
            .detach_and_log_err(cx);
            return;
        }

        let create = {
            let path = path.clone();
            self.dev_server_store.update(cx, |store, cx| {
                store.create_dev_server_project(dev_server_id, path, cx)
            })
        };

        cx.spawn(|this, mut cx| async move {
            let result = create.await;
            this.update(&mut cx, |this, cx| {
                if let Ok(result) = &result {
                    if let Some(dev_server_project_id) =
                        result.dev_server_project.as_ref().map(|p| p.id)
                    {
                        let subscription =
                            cx.observe(&this.dev_server_store, move |this, store, cx| {
                                if let Some(project_id) = store
                                    .read(cx)
                                    .dev_server_project(DevServerProjectId(dev_server_project_id))
                                    .and_then(|p| p.project_id)
                                {
                                    this.project_path_input.update(cx, |editor, cx| {
                                        editor.set_text("", cx);
                                    });
                                    this.mode = Mode::Default(None);
                                    if let Some(app_state) = AppState::global(cx).upgrade() {
                                        workspace::join_dev_server_project(
                                            DevServerProjectId(dev_server_project_id),
                                            project_id,
                                            app_state,
                                            None,
                                            cx,
                                        )
                                        .detach_and_prompt_err(
                                            "Could not join project",
                                            cx,
                                            |_, _| None,
                                        )
                                    }
                                }
                            });

                        this.mode = Mode::Default(Some(CreateDevServerProject {
                            dev_server_id,
                            _opening: Some(subscription),
                        }));
                    }
                } else {
                    this.mode = Mode::Default(Some(CreateDevServerProject {
                        dev_server_id,
                        _opening: None,
                    }));
                }
            })
            .log_err();
            result
        })
        .detach_and_prompt_err("Failed to create project", cx, move |e, _| {
            match e.error_code() {
                ErrorCode::DevServerOffline => Some(
                    "The dev server is offline. Please log in and check it is connected."
                        .to_string(),
                ),
                ErrorCode::DevServerProjectPathDoesNotExist => {
                    Some(format!("The path `{}` does not exist on the server.", path))
                }
                _ => None,
            }
        });

        self.mode = Mode::Default(Some(CreateDevServerProject {
            dev_server_id,

            _opening: None,
        }));
    }

    fn create_ssh_server(&mut self, cx: &mut ViewContext<Self>) {
        let host = get_text(&self.dev_server_name_input, cx);
        if host.is_empty() {
            return;
        }

        let mut host = host.trim_start_matches("ssh ");
        let mut username: Option<String> = None;
        let mut port: Option<u16> = None;

        if let Some((u, rest)) = host.split_once('@') {
            host = rest;
            username = Some(u.to_string());
        }
        if let Some((rest, p)) = host.split_once(':') {
            host = rest;
            port = p.parse().ok()
        }

        if let Some((rest, p)) = host.split_once(" -p") {
            host = rest;
            port = p.trim().parse().ok()
        }

        let connection_options = remote::SshConnectionOptions {
            host: host.to_string(),
            username: username.clone(),
            port,
            password: None,
        };
        let ssh_prompt = cx.new_view(|cx| SshPrompt::new(&connection_options, cx));

        let connection = connect_over_ssh(
            connection_options.dev_server_identifier(),
            connection_options.clone(),
            ssh_prompt.clone(),
            cx,
        )
        .prompt_err("Failed to connect", cx, |_, _| None);

        let creating = cx.spawn(move |this, mut cx| async move {
            match connection.await {
                Some(_) => this
                    .update(&mut cx, |this, cx| {
                        let _ = this.workspace.update(cx, |workspace, _| {
                            workspace
                                .client()
                                .telemetry()
                                .report_app_event("create ssh server".to_string())
                        });

                        this.add_ssh_server(connection_options, cx);
                        this.mode = Mode::Default(None);
                        cx.notify()
                    })
                    .log_err(),
                None => this
                    .update(&mut cx, |this, cx| {
                        this.mode = Mode::CreateDevServer(CreateDevServer::default());
                        cx.notify()
                    })
                    .log_err(),
            };
            None
        });
        self.mode = Mode::CreateDevServer(CreateDevServer {
            ssh_prompt: Some(ssh_prompt.clone()),
            creating: Some(creating),
        });
    }

    fn create_ssh_project(
        &mut self,
        ix: usize,
        ssh_connection: SshConnection,
        cx: &mut ViewContext<Self>,
    ) {
        let Some(workspace) = self.workspace.upgrade() else {
            return;
        };

        let connection_options = ssh_connection.into();
        workspace.update(cx, |_, cx| {
            cx.defer(move |workspace, cx| {
                workspace.toggle_modal(cx, |cx| {
                    SshConnectionModal::new(&connection_options, false, cx)
                });
                let prompt = workspace
                    .active_modal::<SshConnectionModal>(cx)
                    .unwrap()
                    .read(cx)
                    .prompt
                    .clone();

                let connect = connect_over_ssh(
                    connection_options.dev_server_identifier(),
                    connection_options,
                    prompt,
                    cx,
                )
                .prompt_err("Failed to connect", cx, |_, _| None);
                cx.spawn(|workspace, mut cx| async move {
                    let Some(session) = connect.await else {
                        workspace
                            .update(&mut cx, |workspace, cx| {
                                let weak = cx.view().downgrade();
                                workspace.toggle_modal(cx, |cx| DevServerProjects::new(cx, weak));
                            })
                            .log_err();
                        return;
                    };
                    let Ok((app_state, project, paths)) =
                        workspace.update(&mut cx, |workspace, cx| {
                            let app_state = workspace.app_state().clone();
                            let project = project::Project::ssh(
                                session,
                                app_state.client.clone(),
                                app_state.node_runtime.clone(),
                                app_state.user_store.clone(),
                                app_state.languages.clone(),
                                app_state.fs.clone(),
                                cx,
                            );
                            let paths = workspace.prompt_for_open_path(
                                PathPromptOptions {
                                    files: true,
                                    directories: true,
                                    multiple: true,
                                },
                                project::DirectoryLister::Project(project.clone()),
                                cx,
                            );
                            (app_state, project, paths)
                        })
                    else {
                        return;
                    };

                    let Ok(Some(paths)) = paths.await else {
                        workspace
                            .update(&mut cx, |workspace, cx| {
                                let weak = cx.view().downgrade();
                                workspace.toggle_modal(cx, |cx| DevServerProjects::new(cx, weak));
                            })
                            .log_err();
                        return;
                    };

                    let Some(options) = cx
                        .update(|cx| (app_state.build_window_options)(None, cx))
                        .log_err()
                    else {
                        return;
                    };

                    cx.open_window(options, |cx| {
                        cx.activate_window();

                        let fs = app_state.fs.clone();
                        update_settings_file::<SshSettings>(fs, cx, {
                            let paths = paths
                                .iter()
                                .map(|path| path.to_string_lossy().to_string())
                                .collect();
                            move |setting, _| {
                                if let Some(server) = setting
                                    .ssh_connections
                                    .as_mut()
                                    .and_then(|connections| connections.get_mut(ix))
                                {
                                    server.projects.push(SshProject { paths })
                                }
                            }
                        });

                        let tasks = paths
                            .into_iter()
                            .map(|path| {
                                project.update(cx, |project, cx| {
                                    project.find_or_create_worktree(&path, true, cx)
                                })
                            })
                            .collect::<Vec<_>>();
                        cx.spawn(|_| async move {
                            for task in tasks {
                                task.await?;
                            }
                            Ok(())
                        })
                        .detach_and_prompt_err(
                            "Failed to open path",
                            cx,
                            |_, _| None,
                        );

                        cx.new_view(|cx| {
                            let workspace =
                                Workspace::new(None, project.clone(), app_state.clone(), cx);

                            workspace
                                .client()
                                .telemetry()
                                .report_app_event("create ssh project".to_string());

                            workspace
                        })
                    })
                    .log_err();
                })
                .detach()
            })
        })
    }

    fn confirm(&mut self, _: &menu::Confirm, cx: &mut ViewContext<Self>) {
        match &self.mode {
            Mode::Default(None) => {}
            Mode::Default(Some(create_project)) => {
                self.create_dev_server_project(create_project.dev_server_id, cx);
            }
            Mode::CreateDevServer(state) => {
                if let Some(prompt) = state.ssh_prompt.as_ref() {
                    prompt.update(cx, |prompt, cx| {
                        prompt.confirm(cx);
                    });
                    return;
                }

                self.create_ssh_server(cx);
            }
        }
    }

    fn cancel(&mut self, _: &menu::Cancel, cx: &mut ViewContext<Self>) {
        match &self.mode {
            Mode::Default(None) => cx.emit(DismissEvent),
            Mode::CreateDevServer(state) if state.ssh_prompt.is_some() => {
                self.mode = Mode::CreateDevServer(CreateDevServer {
                    ..Default::default()
                });
                cx.notify();
            }
            _ => {
                self.mode = Mode::Default(None);
                self.focus_handle(cx).focus(cx);
                cx.notify();
            }
        }
    }

    fn render_ssh_connection(
        &mut self,
        ix: usize,
        ssh_connection: SshConnection,
        cx: &mut ViewContext<Self>,
    ) -> impl IntoElement {
        v_flex()
            .w_full()
            .px(Spacing::Small.rems(cx) + Spacing::Small.rems(cx))
            .child(
                h_flex()
                    .w_full()
                    .group("ssh-server")
                    .justify_between()
                    .child(
                        h_flex()
                            .gap_2()
                            .w_full()
                            .child(
                                div()
                                    .id(("status", ix))
                                    .relative()
                                    .child(Icon::new(IconName::Server).size(IconSize::Small)),
                            )
                            .child(
                                h_flex()
                                    .max_w(rems(26.))
                                    .overflow_hidden()
                                    .whitespace_nowrap()
                                    .child(Label::new(ssh_connection.host.clone())),
                            ),
                    )
                    .child(
                        h_flex()
                            .visible_on_hover("ssh-server")
                            .gap_1()
                            .child({
                                IconButton::new("copy-dev-server-address", IconName::Copy)
                                    .icon_size(IconSize::Small)
                                    .on_click(cx.listener(move |this, _, cx| {
                                        this.update_settings_file(cx, move |servers, cx| {
                                            if let Some(content) = servers
                                                .ssh_connections
                                                .as_ref()
                                                .and_then(|connections| {
                                                    connections
                                                        .get(ix)
                                                        .map(|connection| connection.host.clone())
                                                })
                                            {
                                                cx.write_to_clipboard(ClipboardItem::new_string(
                                                    content,
                                                ));
                                            }
                                        });
                                    }))
                                    .tooltip(|cx| Tooltip::text("Copy Server Address", cx))
                            })
                            .child({
                                IconButton::new("remove-dev-server", IconName::TrashAlt)
                                    .icon_size(IconSize::Small)
                                    .on_click(cx.listener(move |this, _, cx| {
                                        this.delete_ssh_server(ix, cx)
                                    }))
                                    .tooltip(|cx| Tooltip::text("Remove Dev Server", cx))
                            }),
                    ),
            )
            .child(
                v_flex()
                    .w_full()
                    .border_l_1()
                    .border_color(cx.theme().colors().border_variant)
                    .mb_1()
                    .mx_1p5()
                    .pl_2()
                    .child(
                        List::new()
                            .empty_message("No projects.")
                            .children(ssh_connection.projects.iter().enumerate().map(|(pix, p)| {
                                v_flex().gap_0p5().child(self.render_ssh_project(
                                    ix,
                                    &ssh_connection,
                                    pix,
                                    p,
                                    cx,
                                ))
                            }))
                            .child(
                                h_flex().mt_1().pl_1().child(
                                    Button::new(("new-remote_project", ix), "Open Folder…")
                                        .size(ButtonSize::Default)
                                        .layer(ElevationIndex::ModalSurface)
                                        .icon(IconName::Plus)
                                        .icon_color(Color::Muted)
                                        .icon_position(IconPosition::Start)
                                        .on_click(cx.listener(move |this, _, cx| {
                                            this.create_ssh_project(ix, ssh_connection.clone(), cx);
                                        })),
                                ),
                            ),
                    ),
            )
    }

    fn render_ssh_project(
        &self,
        server_ix: usize,
        server: &SshConnection,
        ix: usize,
        project: &SshProject,
        cx: &ViewContext<Self>,
    ) -> impl IntoElement {
        let project = project.clone();
        let server = server.clone();

        ListItem::new(("remote-project", ix))
            .inset(true)
            .spacing(ui::ListItemSpacing::Sparse)
            .start_slot(
                Icon::new(IconName::Folder)
                    .color(Color::Muted)
                    .size(IconSize::Small),
            )
            .child(Label::new(project.paths.join(", ")))
            .on_click(cx.listener(move |this, _, cx| {
                let Some(app_state) = this
                    .workspace
                    .update(cx, |workspace, _| workspace.app_state().clone())
                    .log_err()
                else {
                    return;
                };
                let project = project.clone();
                let server = server.clone();
                cx.spawn(|_, mut cx| async move {
                    let result = open_ssh_project(
                        server.into(),
                        project.paths.into_iter().map(PathBuf::from).collect(),
                        app_state,
                        OpenOptions::default(),
                        &mut cx,
                    )
                    .await;
                    if let Err(e) = result {
                        log::error!("Failed to connect: {:?}", e);
                        cx.prompt(
                            gpui::PromptLevel::Critical,
                            "Failed to connect",
                            Some(&e.to_string()),
                            &["Ok"],
                        )
                        .await
                        .ok();
                    }
                })
                .detach();
            }))
            .end_hover_slot::<AnyElement>(Some(
                IconButton::new("remove-remote-project", IconName::TrashAlt)
                    .on_click(
                        cx.listener(move |this, _, cx| this.delete_ssh_project(server_ix, ix, cx)),
                    )
                    .tooltip(|cx| Tooltip::text("Delete Remote Project", cx))
                    .into_any_element(),
            ))
    }

    fn update_settings_file(
        &mut self,
        cx: &mut ViewContext<Self>,
        f: impl FnOnce(&mut RemoteSettingsContent, &AppContext) + Send + Sync + 'static,
    ) {
        let Some(fs) = self
            .workspace
            .update(cx, |workspace, _| workspace.app_state().fs.clone())
            .log_err()
        else {
            return;
        };
        update_settings_file::<SshSettings>(fs, cx, move |setting, cx| f(setting, cx));
    }

    fn delete_ssh_server(&mut self, server: usize, cx: &mut ViewContext<Self>) {
        self.update_settings_file(cx, move |setting, _| {
            if let Some(connections) = setting.ssh_connections.as_mut() {
                connections.remove(server);
            }
        });
    }

    fn delete_ssh_project(&mut self, server: usize, project: usize, cx: &mut ViewContext<Self>) {
        self.update_settings_file(cx, move |setting, _| {
            if let Some(server) = setting
                .ssh_connections
                .as_mut()
                .and_then(|connections| connections.get_mut(server))
            {
                server.projects.remove(project);
            }
        });
    }

    fn add_ssh_server(
        &mut self,
        connection_options: remote::SshConnectionOptions,
        cx: &mut ViewContext<Self>,
    ) {
        self.update_settings_file(cx, move |setting, _| {
            setting
                .ssh_connections
                .get_or_insert(Default::default())
                .push(SshConnection {
                    host: connection_options.host,
                    username: connection_options.username,
                    port: connection_options.port,
                    projects: vec![],
                })
        });
    }

    fn render_create_dev_server(
        &self,
        state: &CreateDevServer,
        cx: &mut ViewContext<Self>,
    ) -> impl IntoElement {
        let creating = state.creating.is_some();
        let ssh_prompt = state.ssh_prompt.clone();

        self.dev_server_name_input.update(cx, |input, cx| {
            input.editor().update(cx, |editor, cx| {
                if editor.text(cx).is_empty() {
                    editor.set_placeholder_text("ssh me@my.server / ssh@secret-box:2222", cx);
                }
            })
        });
        let theme = cx.theme();

        v_flex()
            .id("create-dev-server")
            .overflow_hidden()
            .size_full()
            .flex_1()
            .child(
                h_flex()
                    .p_2()
                    .gap_2()
                    .items_center()
                    .border_b_1()
                    .border_color(theme.colors().border_variant)
                    .child(
                        IconButton::new("cancel-dev-server-creation", IconName::ArrowLeft)
                            .shape(IconButtonShape::Square)
                            .on_click(|_, cx| {
                                cx.dispatch_action(menu::Cancel.boxed_clone());
                            }),
                    )
                    .child(Label::new("Connect New Dev Server")),
            )
            .child(
                v_flex()
                    .p_3()
                    .border_b_1()
                    .border_color(theme.colors().border_variant)
                    .child(Label::new("SSH Arguments"))
                    .child(
                        Label::new("Enter the command you use to SSH into this server.")
                            .size(LabelSize::Small)
                            .color(Color::Muted),
                    )
                    .child(
                        h_flex()
                            .mt_2()
                            .w_full()
                            .gap_2()
                            .child(self.dev_server_name_input.clone())
                            .child(
                                Button::new("create-dev-server", "Connect Server")
                                    .style(ButtonStyle::Filled)
                                    .layer(ElevationIndex::ModalSurface)
                                    .disabled(creating)
                                    .on_click(cx.listener({
                                        move |this, _, cx| {
                                            this.create_ssh_server(cx);
                                        }
                                    })),
                            ),
                    ),
            )
            .child(
                h_flex()
                    .bg(theme.colors().editor_background)
                    .rounded_b_md()
                    .w_full()
                    .map(|this| {
                        if let Some(ssh_prompt) = ssh_prompt {
                            this.child(h_flex().w_full().child(ssh_prompt))
                        } else {
                            let color = Color::Muted.color(cx);
                            this.child(
                                h_flex()
                                    .p_2()
                                    .w_full()
                                    .justify_center()
                                    .gap_1p5()
                                    .child(
                                        div().p_1().rounded_lg().bg(color).with_animation(
                                            "pulse-ssh-waiting-for-connection",
                                            Animation::new(Duration::from_secs(2))
                                                .repeat()
                                                .with_easing(pulsating_between(0.2, 0.5)),
                                            move |this, progress| this.bg(color.opacity(progress)),
                                        ),
                                    )
                                    .child(
                                        Label::new("Waiting for connection…")
                                            .size(LabelSize::Small),
                                    ),
                            )
                        }
                    }),
            )
    }

    fn render_default(&mut self, cx: &mut ViewContext<Self>) -> impl IntoElement {
        let dev_servers = self.dev_server_store.read(cx).dev_servers();
        let ssh_connections = SshSettings::get_global(cx)
            .ssh_connections()
            .collect::<Vec<_>>();

        let footer = format!("Connections: {}", ssh_connections.len() + dev_servers.len());
        Modal::new("remote-projects", Some(self.scroll_handle.clone()))
            .header(
                ModalHeader::new().child(
                    h_flex()
                        .justify_between()
                        .child(Headline::new("Remote Projects (alpha)").size(HeadlineSize::XSmall))
                        .child(
                            Button::new("register-dev-server-button", "Connect New Server")
                                .style(ButtonStyle::Filled)
                                .layer(ElevationIndex::ModalSurface)
                                .icon(IconName::Plus)
                                .icon_position(IconPosition::Start)
                                .icon_color(Color::Muted)
                                .on_click(cx.listener(|this, _, cx| {
                                    this.mode = Mode::CreateDevServer(CreateDevServer {
                                        ..Default::default()
                                    });
                                    this.dev_server_name_input.update(cx, |text_field, cx| {
                                        text_field.editor().update(cx, |editor, cx| {
                                            editor.set_text("", cx);
                                        });
                                    });
                                    cx.notify();
                                })),
                        ),
                ),
            )
            .section(
                Section::new().padded(false).child(
                    div()
                        .border_y_1()
                        .border_color(cx.theme().colors().border_variant)
                        .w_full()
                        .child(
                            div().p_2().child(
                                List::new()
                                    .empty_message("No dev servers registered yet.")
                                    .children(ssh_connections.iter().cloned().enumerate().map(
                                        |(ix, connection)| {
                                            self.render_ssh_connection(ix, connection, cx)
                                                .into_any_element()
                                        },
                                    )),
                            ),
                        ),
                ),
            )
            .footer(
                ModalFooter::new()
                    .start_slot(div().child(Label::new(footer).size(LabelSize::Small))),
            )
    }
}

fn get_text(element: &View<TextField>, cx: &mut WindowContext) -> String {
    element
        .read(cx)
        .editor()
        .read(cx)
        .text(cx)
        .trim()
        .to_string()
}

impl ModalView for DevServerProjects {}

impl FocusableView for DevServerProjects {
    fn focus_handle(&self, _cx: &AppContext) -> FocusHandle {
        self.focus_handle.clone()
    }
}

impl EventEmitter<DismissEvent> for DevServerProjects {}

impl Render for DevServerProjects {
    fn render(&mut self, cx: &mut ViewContext<Self>) -> impl IntoElement {
        div()
            .track_focus(&self.focus_handle)
            .elevation_3(cx)
            .key_context("DevServerModal")
            .on_action(cx.listener(Self::cancel))
            .on_action(cx.listener(Self::confirm))
            .capture_any_mouse_down(cx.listener(|this, _, cx| {
                this.focus_handle(cx).focus(cx);
            }))
            .on_mouse_down_out(cx.listener(|this, _, cx| {
                if matches!(this.mode, Mode::Default(None)) {
                    cx.emit(DismissEvent)
                }
            }))
            .w(rems(34.))
            .max_h(rems(40.))
            .child(match &self.mode {
                Mode::Default(_) => self.render_default(cx).into_any_element(),
                Mode::CreateDevServer(state) => {
                    self.render_create_dev_server(state, cx).into_any_element()
                }
            })
    }
}

pub fn reconnect_to_dev_server_project(
    workspace: View<Workspace>,
    dev_server: DevServer,
    dev_server_project_id: DevServerProjectId,
    replace_current_window: bool,
    cx: &mut WindowContext,
) -> Task<Result<()>> {
    let store = dev_server_projects::Store::global(cx);
    let reconnect = reconnect_to_dev_server(workspace.clone(), dev_server, cx);
    cx.spawn(|mut cx| async move {
        reconnect.await?;

        cx.background_executor()
            .timer(Duration::from_millis(1000))
            .await;

        if let Some(project_id) = store.update(&mut cx, |store, _| {
            store
                .dev_server_project(dev_server_project_id)
                .and_then(|p| p.project_id)
        })? {
            workspace
                .update(&mut cx, move |_, cx| {
                    open_dev_server_project(
                        replace_current_window,
                        dev_server_project_id,
                        project_id,
                        cx,
                    )
                })?
                .await?;
        }

        Ok(())
    })
}

pub fn reconnect_to_dev_server(
    workspace: View<Workspace>,
    dev_server: DevServer,
    cx: &mut WindowContext,
) -> Task<Result<()>> {
    let Some(ssh_connection_string) = dev_server.ssh_connection_string else {
        return Task::ready(Err(anyhow!("Can't reconnect, no ssh_connection_string")));
    };
    let dev_server_store = dev_server_projects::Store::global(cx);
    let get_access_token = dev_server_store.update(cx, |store, cx| {
        store.regenerate_dev_server_token(dev_server.id, cx)
    });

    cx.spawn(|mut cx| async move {
        let access_token = get_access_token.await?.access_token;

        spawn_ssh_task(
            workspace,
            dev_server_store,
            dev_server.id,
            ssh_connection_string.to_string(),
            access_token,
            &mut cx,
        )
        .await
    })
}

pub async fn spawn_ssh_task(
    workspace: View<Workspace>,
    dev_server_store: Model<dev_server_projects::Store>,
    dev_server_id: DevServerId,
    ssh_connection_string: String,
    access_token: String,
    cx: &mut AsyncWindowContext,
) -> Result<()> {
    let terminal_panel = workspace
        .update(cx, |workspace, cx| workspace.panel::<TerminalPanel>(cx))
        .ok()
        .flatten()
        .with_context(|| anyhow!("No terminal panel"))?;

    let command = "sh".to_string();
    let args = vec![
        "-x".to_string(),
        "-c".to_string(),
        format!(
            r#"~/.local/bin/zed -v >/dev/stderr || (curl -f https://zed.dev/install.sh || wget -qO- https://zed.dev/install.sh) | sh && ZED_HEADLESS=1 ~/.local/bin/zed --dev-server-token {}"#,
            access_token
        ),
    ];

    let ssh_connection_string = ssh_connection_string.to_string();
    let (command, args) = wrap_for_ssh(
        &SshCommand::DevServer(ssh_connection_string.clone()),
        Some((&command, &args)),
        None,
        HashMap::default(),
        None,
    );

    let terminal = terminal_panel
        .update(cx, |terminal_panel, cx| {
            terminal_panel.spawn_in_new_terminal(
                SpawnInTerminal {
                    id: task::TaskId("ssh-remote".into()),
                    full_label: "Install zed over ssh".into(),
                    label: "Install zed over ssh".into(),
                    command,
                    args,
                    command_label: ssh_connection_string.clone(),
                    cwd: None,
                    use_new_terminal: true,
                    allow_concurrent_runs: false,
                    reveal: RevealStrategy::Always,
                    hide: HideStrategy::Never,
                    env: Default::default(),
                    shell: Default::default(),
                },
                cx,
            )
        })?
        .await?;

    terminal
        .update(cx, |terminal, cx| terminal.wait_for_completed_task(cx))?
        .await;

    // There's a race-condition between the task completing successfully, and the server sending us the online status. Make it less likely we'll show the error state.
    if dev_server_store.update(cx, |this, _| this.dev_server_status(dev_server_id))?
        == DevServerStatus::Offline
    {
        cx.background_executor()
            .timer(Duration::from_millis(200))
            .await
    }

    if dev_server_store.update(cx, |this, _| this.dev_server_status(dev_server_id))?
        == DevServerStatus::Offline
    {
        return Err(anyhow!("couldn't reconnect"))?;
    }

    Ok(())
}
