use anyhow::{anyhow, Result};
use client::DevServerProjectId;
use client::{user::UserStore, Client, ClientSettings};
use extension::ExtensionStore;
use fs::Fs;
use futures::{Future, StreamExt};
use gpui::{AppContext, AsyncAppContext, Context, Global, Model, ModelContext, Task, WeakModel};
use language::LanguageRegistry;
use node_runtime::NodeRuntime;
use postage::stream::Stream;
use project::Project;
use proto::{self, ErrorCode, TypedEnvelope};
use settings::{Settings, SettingsStore};
use std::path::Path;
use std::{collections::HashMap, sync::Arc};
use util::{ResultExt, TryFutureExt};

pub struct DevServer {
    client: Arc<Client>,
    app_state: AppState,
    remote_shutdown: bool,
    projects: HashMap<DevServerProjectId, Model<Project>>,
    _subscriptions: Vec<client::Subscription>,
    _maintain_connection: Task<Option<()>>,
}

pub struct AppState {
    pub node_runtime: NodeRuntime,
    pub user_store: Model<UserStore>,
    pub languages: Arc<LanguageRegistry>,
    pub fs: Arc<dyn Fs>,
}

struct GlobalDevServer(Model<DevServer>);

impl Global for GlobalDevServer {}

pub fn init(client: Arc<Client>, app_state: AppState, cx: &mut AppContext) -> Task<Result<()>> {
    let dev_server = cx.new_model(|cx| DevServer::new(client.clone(), app_state, cx));
    cx.set_global(GlobalDevServer(dev_server.clone()));

    #[cfg(not(target_os = "windows"))]
    {
        use signal_hook::consts::{SIGINT, SIGTERM};
        use signal_hook::iterator::Signals;
        // Set up a handler when the dev server is shut down
        // with ctrl-c or kill
        let (tx, rx) = futures::channel::oneshot::channel();
        let mut signals = Signals::new([SIGTERM, SIGINT]).unwrap();
        std::thread::spawn({
            move || {
                if let Some(sig) = signals.forever().next() {
                    tx.send(sig).log_err();
                }
            }
        });
        cx.spawn(|cx| async move {
            if let Ok(sig) = rx.await {
                log::info!("received signal {sig:?}");
                cx.update(|cx| cx.quit()).log_err();
            }
        })
        .detach();
    }

    let server_url = ClientSettings::get_global(cx).server_url.clone();
    cx.spawn(|cx| async move {
        client
            .authenticate_and_connect(false, &cx)
            .await
            .map_err(|e| anyhow!("Error connecting to '{}': {}", server_url, e))
    })
}

impl DevServer {
    pub fn global(cx: &AppContext) -> Model<DevServer> {
        cx.global::<GlobalDevServer>().0.clone()
    }

    pub fn new(client: Arc<Client>, app_state: AppState, cx: &mut ModelContext<Self>) -> Self {
        cx.on_app_quit(Self::app_will_quit).detach();

        let maintain_connection = cx.spawn({
            let client = client.clone();
            move |this, cx| Self::maintain_connection(this, client.clone(), cx).log_err()
        });

        cx.observe_global::<SettingsStore>(|_, cx| {
            ExtensionStore::global(cx).update(cx, |store, cx| store.auto_install_extensions(cx))
        })
        .detach();

        DevServer {
            _subscriptions: vec![
                client.add_message_handler(cx.weak_model(), Self::handle_dev_server_instructions),
                client.add_request_handler(
                    cx.weak_model(),
                    Self::handle_validate_dev_server_project_request,
                ),
                client.add_request_handler(cx.weak_model(), Self::handle_list_remote_directory),
                client.add_message_handler(cx.weak_model(), Self::handle_shutdown),
            ],
            _maintain_connection: maintain_connection,
            projects: Default::default(),
            remote_shutdown: false,
            app_state,
            client,
        }
    }

    fn app_will_quit(&mut self, _: &mut ModelContext<Self>) -> impl Future<Output = ()> {
        let request = if self.remote_shutdown {
            None
        } else {
            Some(
                self.client
                    .request(proto::ShutdownDevServer { reason: None }),
            )
        };
        async move {
            if let Some(request) = request {
                request.await.log_err();
            }
        }
    }

    async fn handle_dev_server_instructions(
        this: Model<Self>,
        envelope: TypedEnvelope<proto::DevServerInstructions>,
        mut cx: AsyncAppContext,
    ) -> Result<()> {
        let (added_projects, retained_projects, removed_projects_ids) =
            this.read_with(&mut cx, |this, _| {
                let removed_projects = this
                    .projects
                    .keys()
                    .filter(|dev_server_project_id| {
                        !envelope
                            .payload
                            .projects
                            .iter()
                            .any(|p| p.id == dev_server_project_id.0)
                    })
                    .cloned()
                    .collect::<Vec<_>>();

                let mut added_projects = vec![];
                let mut retained_projects = vec![];

                for project in envelope.payload.projects.iter() {
                    if this.projects.contains_key(&DevServerProjectId(project.id)) {
                        retained_projects.push(project.clone());
                    } else {
                        added_projects.push(project.clone());
                    }
                }

                (added_projects, retained_projects, removed_projects)
            })?;

        for dev_server_project in added_projects {
            DevServer::share_project(this.clone(), &dev_server_project, &mut cx).await?;
        }

        for dev_server_project in retained_projects {
            DevServer::update_project(this.clone(), &dev_server_project, &mut cx).await?;
        }

        this.update(&mut cx, |this, cx| {
            for old_project_id in &removed_projects_ids {
                this.unshare_project(old_project_id, cx)?;
            }
            Ok::<(), anyhow::Error>(())
        })??;
        Ok(())
    }

    async fn handle_validate_dev_server_project_request(
        this: Model<Self>,
        envelope: TypedEnvelope<proto::ValidateDevServerProjectRequest>,
        cx: AsyncAppContext,
    ) -> Result<proto::Ack> {
        let expanded = shellexpand::tilde(&envelope.payload.path).to_string();
        let path = std::path::Path::new(&expanded);
        let fs = cx.read_model(&this, |this, _| this.app_state.fs.clone())?;

        let path_exists = fs.metadata(path).await.is_ok_and(|result| result.is_some());
        if !path_exists {
            return Err(anyhow!(ErrorCode::DevServerProjectPathDoesNotExist))?;
        }

        Ok(proto::Ack {})
    }

    async fn handle_list_remote_directory(
        this: Model<Self>,
        envelope: TypedEnvelope<proto::ListRemoteDirectory>,
        cx: AsyncAppContext,
    ) -> Result<proto::ListRemoteDirectoryResponse> {
        let expanded = shellexpand::tilde(&envelope.payload.path).to_string();
        let fs = cx.read_model(&this, |this, _| this.app_state.fs.clone())?;

        let mut entries = Vec::new();
        let mut response = fs.read_dir(Path::new(&expanded)).await?;
        while let Some(path) = response.next().await {
            if let Some(file_name) = path?.file_name() {
                entries.push(file_name.to_string_lossy().to_string());
            }
        }
        Ok(proto::ListRemoteDirectoryResponse { entries })
    }

    async fn handle_shutdown(
        this: Model<Self>,
        _envelope: TypedEnvelope<proto::ShutdownDevServer>,
        mut cx: AsyncAppContext,
    ) -> Result<()> {
        this.update(&mut cx, |this, cx| {
            this.remote_shutdown = true;
            cx.quit();
        })
    }

    fn unshare_project(
        &mut self,
        dev_server_project_id: &DevServerProjectId,
        cx: &mut ModelContext<Self>,
    ) -> Result<()> {
        if let Some(project) = self.projects.remove(dev_server_project_id) {
            project.update(cx, |project, cx| project.unshare(cx))?;
        }
        Ok(())
    }

    async fn share_project(
        this: Model<Self>,
        dev_server_project: &proto::DevServerProject,
        cx: &mut AsyncAppContext,
    ) -> Result<()> {
        let (client, project) = this.update(cx, |this, cx| {
            let project = Project::local(
                this.client.clone(),
                this.app_state.node_runtime.clone(),
                this.app_state.user_store.clone(),
                this.app_state.languages.clone(),
                this.app_state.fs.clone(),
                None,
                cx,
            );

            (this.client.clone(), project)
        })?;

        for path in &dev_server_project.paths {
            let path = shellexpand::tilde(path).to_string();

            let (worktree, _) = project
                .update(cx, |project, cx| {
                    project.find_or_create_worktree(&path, true, cx)
                })?
                .await?;

            worktree.update(cx, |worktree, cx| {
                worktree.as_local_mut().unwrap().share_private_files(cx)
            })?;
        }

        let worktrees =
            project.read_with(cx, |project, cx| project.worktree_metadata_protos(cx))?;

        let response = client
            .request(proto::ShareDevServerProject {
                dev_server_project_id: dev_server_project.id,
                worktrees,
            })
            .await?;

        let project_id = response.project_id;
        project.update(cx, |project, cx| project.shared(project_id, cx))??;
        this.update(cx, |this, _| {
            this.projects
                .insert(DevServerProjectId(dev_server_project.id), project);
        })?;
        Ok(())
    }

    async fn update_project(
        this: Model<Self>,
        dev_server_project: &proto::DevServerProject,
        cx: &mut AsyncAppContext,
    ) -> Result<()> {
        let tasks = this.update(cx, |this, cx| {
            let Some(project) = this
                .projects
                .get(&DevServerProjectId(dev_server_project.id))
            else {
                return vec![];
            };

            let mut to_delete = vec![];
            let mut tasks = vec![];

            project.update(cx, |project, cx| {
                for worktree in project.visible_worktrees(cx) {
                    let mut delete = true;
                    for config in dev_server_project.paths.iter() {
                        if worktree.read(cx).abs_path().to_string_lossy()
                            == shellexpand::tilde(config)
                        {
                            delete = false;
                        }
                    }
                    if delete {
                        to_delete.push(worktree.read(cx).id())
                    }
                }

                for worktree_id in to_delete {
                    project.remove_worktree(worktree_id, cx)
                }

                for config in dev_server_project.paths.iter() {
                    tasks.push(project.find_or_create_worktree(
                        shellexpand::tilde(config).to_string(),
                        true,
                        cx,
                    ));
                }

                tasks
            })
        })?;
        futures::future::join_all(tasks).await;
        Ok(())
    }

    async fn maintain_connection(
        this: WeakModel<Self>,
        client: Arc<Client>,
        mut cx: AsyncAppContext,
    ) -> Result<()> {
        let mut client_status = client.status();

        let _ = client_status.try_recv();
        let current_status = *client_status.borrow();
        if current_status.is_connected() {
            // wait for first disconnect
            client_status.recv().await;
        }

        loop {
            let Some(current_status) = client_status.recv().await else {
                return Ok(());
            };
            let Some(this) = this.upgrade() else {
                return Ok(());
            };

            if !current_status.is_connected() {
                continue;
            }

            this.update(&mut cx, |this, cx| this.rejoin(cx))?.await?;
        }
    }

    fn rejoin(&mut self, cx: &mut ModelContext<Self>) -> Task<Result<()>> {
        let mut projects: HashMap<u64, Model<Project>> = HashMap::default();
        let request = self.client.request(proto::ReconnectDevServer {
            reshared_projects: self
                .projects
                .iter()
                .flat_map(|(_, handle)| {
                    let project = handle.read(cx);
                    let project_id = project.remote_id()?;
                    projects.insert(project_id, handle.clone());
                    Some(proto::UpdateProject {
                        project_id,
                        worktrees: project.worktree_metadata_protos(cx),
                    })
                })
                .collect(),
        });
        cx.spawn(|_, mut cx| async move {
            let response = request.await?;

            for reshared_project in response.reshared_projects {
                if let Some(project) = projects.get(&reshared_project.id) {
                    project.update(&mut cx, |project, cx| {
                        project.reshared(reshared_project, cx).log_err();
                    })?;
                }
            }
            Ok(())
        })
    }
}
