use anyhow::anyhow;
use crossbeam::select;
use lsp_server::Message;
use lsp_types::{
    self as types, DidChangeWatchedFilesRegistrationOptions, FileSystemWatcher, Notification as _,
    Request as _,
};

use crate::{
    Server,
    server::{api, schedule},
    session::Client,
};

pub(crate) type MainLoopSender = crossbeam::channel::Sender<Event>;
pub(crate) type MainLoopReceiver = crossbeam::channel::Receiver<Event>;

impl Server {
    pub(super) fn main_loop(&mut self) -> crate::Result<()> {
        self.initialize(&Client::new(
            self.main_loop_sender.clone(),
            self.connection.sender.clone(),
        ));

        let mut scheduler = schedule::Scheduler::new(self.worker_threads);

        while let Ok(next_event) = self.next_event() {
            let Some(next_event) = next_event else {
                anyhow::bail!("client exited without proper shutdown sequence");
            };

            match next_event {
                Event::Message(msg) => {
                    let client = Client::new(
                        self.main_loop_sender.clone(),
                        self.connection.sender.clone(),
                    );

                    let task = match msg {
                        Message::Request(req) => {
                            self.session
                                .request_queue_mut()
                                .incoming_mut()
                                .register(req.id.clone(), req.method.clone());

                            if self.session.is_shutdown_requested() {
                                tracing::warn!(
                                    "Received request after server shutdown was requested, discarding"
                                );
                                client.respond_err(
                                    req.id,
                                    lsp_server::ResponseError {
                                        code: lsp_server::ErrorCode::InvalidRequest as i32,
                                        message: "Shutdown already requested".to_owned(),
                                        data: None,
                                    },
                                )?;
                                continue;
                            }

                            api::request(req)
                        }
                        Message::Notification(notification) => {
                            if notification.method == lsp_types::ExitNotification::METHOD.as_str() {
                                if !self.session.is_shutdown_requested() {
                                    return Err(anyhow!(
                                        "Received exit notification before a shutdown request"
                                    ));
                                }

                                tracing::debug!("Received exit notification, exiting");
                                return Ok(());
                            }

                            api::notification(notification)
                        }

                        // Handle the response from the client to a server request
                        Message::Response(response) => {
                            if let Some(handler) = self
                                .session
                                .request_queue_mut()
                                .outgoing_mut()
                                .complete(&response.id)
                            {
                                handler(&client, response);
                            } else {
                                tracing::error!(
                                    "Received a response with ID {}, which was not expected",
                                    response.id
                                );
                            }

                            continue;
                        }
                    };

                    scheduler.dispatch(task, &mut self.session, client);
                }
                Event::SendResponse(response) => {
                    // Filter out responses for already canceled requests.
                    if let Some((start_time, method)) = self
                        .session
                        .request_queue_mut()
                        .incoming_mut()
                        .complete(&response.id)
                    {
                        let duration = start_time.elapsed();
                        tracing::trace!(name: "message response", method, %response.id, duration = format_args!("{:0.2?}", duration));

                        self.connection.sender.send(Message::Response(response))?;
                    } else {
                        tracing::trace!(
                            "Ignoring response for canceled request id={}",
                            response.id
                        );
                    }
                }
            }
        }

        Ok(())
    }

    /// Waits for the next message from the client or action.
    ///
    /// Returns `Ok(None)` if the client connection is closed.
    fn next_event(&self) -> Result<Option<Event>, crossbeam::channel::RecvError> {
        select!(
            recv(self.connection.receiver) -> msg => {
                // Ignore disconnect errors, they're handled by the main loop (it will exit).
                Ok(msg.ok().map(Event::Message))
            },
            recv(self.main_loop_receiver) -> event => event.map(Some),
        )
    }

    fn initialize(&mut self, client: &Client) {
        let supports_watched_files = self
            .client_capabilities
            .workspace
            .as_ref()
            .and_then(|workspace| workspace.did_change_watched_files)
            .and_then(|watched_files| watched_files.dynamic_registration)
            .unwrap_or_default();
        let supports_formatting = Self::supports_dynamic_formatting(&self.client_capabilities);
        let supports_range_formatting =
            Self::supports_dynamic_range_formatting(&self.client_capabilities);
        let dynamic_registration =
            supports_watched_files || supports_formatting || supports_range_formatting;

        if dynamic_registration {
            // Register all dynamic capabilities here
            let mut registrations = vec![];

            if supports_watched_files {
                // `workspace/didChangeWatchedFiles`
                // (this registers the configuration file watcher)
                registrations.push(lsp_types::Registration {
                    id: "ruff-server-watch".into(),
                    method: "workspace/didChangeWatchedFiles".into(),
                    register_options: Some(
                        serde_json::to_value(DidChangeWatchedFilesRegistrationOptions {
                            watchers: vec![
                                FileSystemWatcher {
                                    glob_pattern: types::GlobPattern::Pattern(
                                        "**/.ruff.toml".into(),
                                    ),
                                    kind: None,
                                },
                                FileSystemWatcher {
                                    glob_pattern: types::GlobPattern::Pattern(
                                        "**/ruff.toml".into(),
                                    ),
                                    kind: None,
                                },
                                FileSystemWatcher {
                                    glob_pattern: types::GlobPattern::Pattern(
                                        "**/pyproject.toml".into(),
                                    ),
                                    kind: None,
                                },
                            ],
                        })
                        .unwrap(),
                    ),
                });
            }

            if supports_formatting || supports_range_formatting {
                let document_selector = vec![
                    types::TextDocumentFilter::Language(types::TextDocumentFilterLanguage {
                        language: "python".to_string(),
                        scheme: None,
                        pattern: None,
                    })
                    .into(),
                    types::TextDocumentFilter::Language(types::TextDocumentFilterLanguage {
                        language: "markdown".to_string(),
                        scheme: None,
                        pattern: None,
                    })
                    .into(),
                    types::NotebookCellTextDocumentFilter {
                        notebook: "*".into(),
                        language: Some("python".into()),
                    }
                    .into(),
                ];

                let text_document_registration_options = types::TextDocumentRegistrationOptions {
                    document_selector: Some(document_selector),
                };

                if supports_formatting {
                    registrations.push(types::Registration {
                        id: "ruff-server-format".into(),
                        method: types::DocumentFormattingRequest::METHOD.to_string(),
                        register_options: Some(
                            serde_json::to_value(types::DocumentFormattingRegistrationOptions {
                                text_document_registration_options:
                                    text_document_registration_options.clone(),
                                document_formatting_options:
                                    types::DocumentFormattingOptions::default(),
                            })
                            .unwrap(),
                        ),
                    });
                }

                if supports_range_formatting {
                    registrations.push(types::Registration {
                        id: "ruff-server-format-range".into(),
                        method: types::DocumentRangeFormattingRequest::METHOD.to_string(),
                        register_options: Some(
                            serde_json::to_value(
                                types::DocumentRangeFormattingRegistrationOptions {
                                    text_document_registration_options,
                                    document_range_formatting_options:
                                        types::DocumentRangeFormattingOptions::default(),
                                },
                            )
                            .unwrap(),
                        ),
                    });
                }
            }

            let params = types::RegistrationParams { registrations };
            let response_handler = |_: &Client, ()| {
                tracing::info!("Dynamic capabilities successfully registered");
            };

            if let Err(err) = client.send_request::<lsp_types::RegistrationRequest>(
                &self.session,
                params,
                response_handler,
            ) {
                tracing::error!(
                    "An error occurred when trying to register dynamic capabilities: {err}"
                );
            }
        } else {
            tracing::warn!(
                "LSP client does not support dynamic capability registration - automatic configuration reloading will not be available."
            );
        }
    }
}

#[derive(Debug)]
pub(crate) enum Event {
    /// An incoming message from the LSP client.
    Message(lsp_server::Message),

    /// Send a response to the client
    SendResponse(lsp_server::Response),
}
