//
// backend.rs
//
// Copyright (C) 2022-2026 Posit Software, PBC. All rights reserved.
//
//

#![allow(deprecated)]

use std::path::PathBuf;
use std::sync::atomic::Ordering;
use std::sync::Arc;
use std::time::Duration;

use amalthea::comm::server_comm::ServerStartMessage;
use amalthea::comm::server_comm::ServerStartedMessage;
use anyhow::Context;
use crossbeam::channel::Sender;
use serde_json::Value;
use stdext::result::ResultExt;
use tokio::net::TcpListener;
use tokio::runtime::Runtime;
use tokio::sync::mpsc::unbounded_channel as tokio_unbounded_channel;
use tokio::sync::mpsc::UnboundedSender as AsyncUnboundedSender;
use tower_lsp_server::jsonrpc;
use tower_lsp_server::jsonrpc::Result;
use tower_lsp_server::ls_types::request::GotoImplementationParams;
use tower_lsp_server::ls_types::request::GotoImplementationResponse;
use tower_lsp_server::ls_types::FoldingRange;
use tower_lsp_server::ls_types::SelectionRange;
use tower_lsp_server::ls_types::*;
use tower_lsp_server::Client;
use tower_lsp_server::LanguageServer;
use tower_lsp_server::LspService;
use tower_lsp_server::Server;

use super::main_loop::LSP_HAS_CRASHED;
use crate::console::Console;
use crate::console::ConsoleNotification;
use crate::lsp::handlers::VirtualDocumentParams;
use crate::lsp::handlers::VirtualDocumentResponse;
use crate::lsp::handlers::ARK_VDOC_REQUEST;
use crate::lsp::help_topic;
use crate::lsp::help_topic::HelpTopicParams;
use crate::lsp::help_topic::HelpTopicResponse;
use crate::lsp::input_boundaries;
use crate::lsp::input_boundaries::InputBoundariesParams;
use crate::lsp::input_boundaries::InputBoundariesResponse;
use crate::lsp::main_loop::Event;
use crate::lsp::main_loop::GlobalState;
use crate::lsp::main_loop::LoopHandles;
use crate::lsp::main_loop::TokioUnboundedSender;
use crate::lsp::statement_range;
use crate::lsp::statement_range::StatementRangeParams;
use crate::lsp::statement_range::StatementRangeResponse;
use crate::r_task;

// This enum is useful for two things. First it allows us to distinguish a
// normal request failure from a crash. In the latter case we send a
// notification to the client so the user knows the LSP has crashed.
//
// Once the LSP has crashed all requests respond with an error. This prevents
// any handler from running while we process the message to shut down the
// server. The `Disabled` enum variant is an indicator of this state. We could
// have just created an anyhow error passed through the `Result` variant but that
// would flood the LSP logs with irrelevant backtraces.
#[expect(clippy::large_enum_variant)]
pub(crate) enum RequestResponse {
    Disabled,
    Crashed(anyhow::Error),
    Result(LspResult<LspResponse>),
}

// Based on https://stackoverflow.com/a/69324393/1725177
macro_rules! cast_response {
    ($self:expr, $target:expr, $pat:path) => {{
        match $target {
            RequestResponse::Result(Ok($pat(resp))) => Ok(resp),
            RequestResponse::Result(Ok(_)) => {
                let message = format!("Unexpected variant while casting to {}", stringify!($pat));
                log::error!("{message}");
                Err(new_jsonrpc_error(message))
            },
            RequestResponse::Result(Err(err)) => match err {
                LspError::JsonRpc(err) => Err(err),
                // `{err}` (Display) prints just the message. `{err:?}` (Debug)
                // would include the captured backtrace when `RUST_BACKTRACE`
                // is set, which then surfaces in the client's error popup.
                LspError::Anyhow(err) => Err(new_jsonrpc_error(format!("{err}"))),
            },
            RequestResponse::Crashed(err) => {
                // Notify user that the LSP has crashed and is no longer active
                report_crash($self.client()).await;

                // The backtrace is reported via `err` and eventually shows up
                // in the LSP logs on the client side
                let _ = $self.shutdown_tx.send(()).await;
                Err(new_jsonrpc_error(format!("{err:?}")))
            },
            RequestResponse::Disabled => Err(new_jsonrpc_error(String::from(
                "The LSP server has crashed and is now shut down!",
            ))),
        }
    }};
}

/// Send via `request::ShowMessageRequest` not `notification::ShowMessage` so that we can
/// ensure that the message has been received on the frontend side. We are about to shut
/// the LSP down, and sending out a fire-and-forget notification often won't get sent out
/// before shutdown occurs. The request returns control to us when the user acknowledges
/// the message. It doesn't matter if that takes awhile because we shut down right after,
/// and we've already flipped the `LSP_HAS_CRASHED` global flag, but we do bound it with
/// a 5 second timeout just in case the user ignores the message entirely, so we can still
/// shutdown.
async fn report_crash(client: &Client) {
    let user_message = concat!(
        "The R language server has crashed and has been disabled. ",
        "Smart features such as completions will no longer work in this session. ",
        "Please report this crash to https://github.com/posit-dev/positron/issues ",
        "with full logs (see https://positron.posit.co/troubleshooting.html#python-and-r-logs)."
    );
    let request = client.send_request::<request::ShowMessageRequest>(ShowMessageRequestParams {
        typ: MessageType::ERROR,
        message: String::from(user_message),
        actions: None,
    });
    match tokio::time::timeout(Duration::from_secs(5), request).await {
        Ok(result) => {
            result.log_err();
        },
        Err(_) => {
            log::warn!("Timed out waiting for frontend to acknowledge LSP crash notification");
        },
    }
}

#[derive(Debug)]
#[expect(clippy::large_enum_variant)]
pub(crate) enum LspMessage {
    Notification(LspNotification),
    Request(LspRequest, TokioUnboundedSender<RequestResponse>),
}

#[derive(Debug)]
pub(crate) enum LspNotification {
    Initialized(InitializedParams),
    DidChangeWorkspaceFolders(DidChangeWorkspaceFoldersParams),
    DidChangeConfiguration(DidChangeConfigurationParams),
    DidChangeWatchedFiles(DidChangeWatchedFilesParams),
    DidOpenTextDocument(DidOpenTextDocumentParams),
    DidChangeTextDocument(DidChangeTextDocumentParams),
    DidSaveTextDocument(DidSaveTextDocumentParams),
    DidCloseTextDocument(DidCloseTextDocumentParams),
}

#[derive(Debug)]
#[expect(clippy::large_enum_variant)]
pub(crate) enum LspRequest {
    Initialize(InitializeParams),
    WorkspaceSymbol(WorkspaceSymbolParams),
    DocumentSymbol(DocumentSymbolParams),
    FoldingRange(FoldingRangeParams),
    ExecuteCommand(ExecuteCommandParams),
    Completion(CompletionParams),
    CompletionResolve(CompletionItem),
    Hover(HoverParams),
    SignatureHelp(SignatureHelpParams),
    GotoDefinition(GotoDefinitionParams),
    GotoImplementation(GotoImplementationParams),
    SelectionRange(SelectionRangeParams),
    References(ReferenceParams),
    PrepareRename(TextDocumentPositionParams),
    Rename(RenameParams),
    StatementRange(StatementRangeParams),
    HelpTopic(HelpTopicParams),
    OnTypeFormatting(DocumentOnTypeFormattingParams),
    CodeAction(CodeActionParams),
    VirtualDocument(VirtualDocumentParams),
    InputBoundaries(InputBoundariesParams),
}

#[derive(Debug)]
#[expect(clippy::large_enum_variant)]
pub(crate) enum LspResponse {
    Initialize(InitializeResult),
    WorkspaceSymbol(Option<Vec<SymbolInformation>>),
    DocumentSymbol(Option<DocumentSymbolResponse>),
    FoldingRange(Option<Vec<FoldingRange>>),
    ExecuteCommand(Option<Value>),
    Completion(Option<CompletionResponse>),
    CompletionResolve(CompletionItem),
    Hover(Option<Hover>),
    SignatureHelp(Option<SignatureHelp>),
    GotoDefinition(Option<GotoDefinitionResponse>),
    GotoImplementation(Option<GotoImplementationResponse>),
    SelectionRange(Option<Vec<SelectionRange>>),
    References(Option<Vec<Location>>),
    PrepareRename(Option<PrepareRenameResponse>),
    Rename(Option<WorkspaceEdit>),
    StatementRange(Option<StatementRangeResponse>),
    HelpTopic(Option<HelpTopicResponse>),
    OnTypeFormatting(Option<Vec<TextEdit>>),
    CodeAction(Option<CodeActionResponse>),
    VirtualDocument(VirtualDocumentResponse),
    InputBoundaries(InputBoundariesResponse),
}

pub(crate) type LspResult<T> = std::result::Result<T, LspError>;

#[derive(Debug)]
pub(crate) enum LspError {
    JsonRpc(jsonrpc::Error),
    Anyhow(anyhow::Error),
}

impl std::error::Error for LspError {}

impl std::fmt::Display for LspError {
    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
        match self {
            LspError::JsonRpc(error) => write!(f, "{error:?}"),
            LspError::Anyhow(error) => write!(f, "{error:?}"),
        }
    }
}

// For the ability to `?` a `jsonrpc::Error` into an `LspError`
impl From<jsonrpc::Error> for LspError {
    fn from(error: jsonrpc::Error) -> Self {
        Self::JsonRpc(error)
    }
}

// For the ability to `?` an `anyhow::Error` into an `LspError`
impl From<anyhow::Error> for LspError {
    fn from(error: anyhow::Error) -> Self {
        Self::Anyhow(error)
    }
}

#[derive(Debug)]
struct Backend {
    /// Shutdown notifier used to unwind tower-lsp and disconnect from the
    /// client when an LSP handler panics.
    shutdown_tx: tokio::sync::mpsc::Sender<()>,

    /// Channel for communication with the main loop.
    events_tx: TokioUnboundedSender<Event>,

    /// Copy of the Client, for reporting crash messages.
    client: Client,

    /// Handle to the LSP loops. Drop it to shut the loops down and drop all
    /// owned state.
    _main_loop: LoopHandles,
}

impl Backend {
    async fn request(&self, request: LspRequest) -> RequestResponse {
        if LSP_HAS_CRASHED.load(Ordering::Acquire) {
            return RequestResponse::Disabled;
        }

        let (response_tx, mut response_rx) = tokio_unbounded_channel::<RequestResponse>();

        // Relay request to main loop
        self.events_tx
            .send(Event::Lsp(LspMessage::Request(request, response_tx)))
            .unwrap();

        // Wait for response from main loop
        response_rx.recv().await.unwrap()
    }

    fn notify(&self, notif: LspNotification) {
        // Relay notification to main loop
        self.events_tx
            .send(Event::Lsp(LspMessage::Notification(notif)))
            .unwrap();
    }

    fn client(&self) -> &Client {
        &self.client
    }
}

impl LanguageServer for Backend {
    async fn initialize(&self, params: InitializeParams) -> Result<InitializeResult> {
        cast_response!(
            self,
            self.request(LspRequest::Initialize(params)).await,
            LspResponse::Initialize
        )
    }

    async fn initialized(&self, params: InitializedParams) {
        self.notify(LspNotification::Initialized(params));
    }

    async fn shutdown(&self) -> Result<()> {
        // Don't go through the main loop because we want this request to
        // succeed even when the LSP has crashed and has been disabled.
        Ok(())
    }

    async fn did_change_workspace_folders(&self, params: DidChangeWorkspaceFoldersParams) {
        self.notify(LspNotification::DidChangeWorkspaceFolders(params));
    }

    async fn did_change_configuration(&self, params: DidChangeConfigurationParams) {
        self.notify(LspNotification::DidChangeConfiguration(params));
    }

    async fn did_change_watched_files(&self, params: DidChangeWatchedFilesParams) {
        self.notify(LspNotification::DidChangeWatchedFiles(params));
    }

    async fn symbol(
        &self,
        params: WorkspaceSymbolParams,
    ) -> Result<Option<WorkspaceSymbolResponse>> {
        let info: Option<Vec<SymbolInformation>> = cast_response!(
            self,
            self.request(LspRequest::WorkspaceSymbol(params)).await,
            LspResponse::WorkspaceSymbol
        )?;
        Ok(info.map(WorkspaceSymbolResponse::Flat))
    }

    async fn document_symbol(
        &self,
        params: DocumentSymbolParams,
    ) -> Result<Option<DocumentSymbolResponse>> {
        cast_response!(
            self,
            self.request(LspRequest::DocumentSymbol(params)).await,
            LspResponse::DocumentSymbol
        )
    }

    async fn folding_range(&self, params: FoldingRangeParams) -> Result<Option<Vec<FoldingRange>>> {
        cast_response!(
            self,
            self.request(LspRequest::FoldingRange(params)).await,
            LspResponse::FoldingRange
        )
    }

    async fn execute_command(
        &self,
        params: ExecuteCommandParams,
    ) -> jsonrpc::Result<Option<Value>> {
        cast_response!(
            self,
            self.request(LspRequest::ExecuteCommand(params)).await,
            LspResponse::ExecuteCommand
        )
    }

    async fn did_open(&self, params: DidOpenTextDocumentParams) {
        self.notify(LspNotification::DidOpenTextDocument(params));
    }

    async fn did_change(&self, params: DidChangeTextDocumentParams) {
        self.notify(LspNotification::DidChangeTextDocument(params));
    }

    async fn did_save(&self, params: DidSaveTextDocumentParams) {
        self.notify(LspNotification::DidSaveTextDocument(params));
    }

    async fn did_close(&self, params: DidCloseTextDocumentParams) {
        self.notify(LspNotification::DidCloseTextDocument(params));
    }

    async fn completion(&self, params: CompletionParams) -> Result<Option<CompletionResponse>> {
        cast_response!(
            self,
            self.request(LspRequest::Completion(params)).await,
            LspResponse::Completion
        )
    }

    async fn completion_resolve(&self, item: CompletionItem) -> Result<CompletionItem> {
        cast_response!(
            self,
            self.request(LspRequest::CompletionResolve(item)).await,
            LspResponse::CompletionResolve
        )
    }

    async fn hover(&self, params: HoverParams) -> Result<Option<Hover>> {
        cast_response!(
            self,
            self.request(LspRequest::Hover(params)).await,
            LspResponse::Hover
        )
    }

    async fn signature_help(&self, params: SignatureHelpParams) -> Result<Option<SignatureHelp>> {
        cast_response!(
            self,
            self.request(LspRequest::SignatureHelp(params)).await,
            LspResponse::SignatureHelp
        )
    }

    async fn goto_definition(
        &self,
        params: GotoDefinitionParams,
    ) -> Result<Option<GotoDefinitionResponse>> {
        cast_response!(
            self,
            self.request(LspRequest::GotoDefinition(params)).await,
            LspResponse::GotoDefinition
        )
    }

    async fn goto_implementation(
        &self,
        params: GotoImplementationParams,
    ) -> Result<Option<GotoImplementationResponse>> {
        cast_response!(
            self,
            self.request(LspRequest::GotoImplementation(params)).await,
            LspResponse::GotoImplementation
        )
    }

    async fn selection_range(
        &self,
        params: SelectionRangeParams,
    ) -> Result<Option<Vec<SelectionRange>>> {
        cast_response!(
            self,
            self.request(LspRequest::SelectionRange(params)).await,
            LspResponse::SelectionRange
        )
    }

    async fn references(&self, params: ReferenceParams) -> Result<Option<Vec<Location>>> {
        cast_response!(
            self,
            self.request(LspRequest::References(params)).await,
            LspResponse::References
        )
    }

    async fn prepare_rename(
        &self,
        params: TextDocumentPositionParams,
    ) -> Result<Option<PrepareRenameResponse>> {
        cast_response!(
            self,
            self.request(LspRequest::PrepareRename(params)).await,
            LspResponse::PrepareRename
        )
    }

    async fn rename(&self, params: RenameParams) -> Result<Option<WorkspaceEdit>> {
        cast_response!(
            self,
            self.request(LspRequest::Rename(params)).await,
            LspResponse::Rename
        )
    }

    async fn on_type_formatting(
        &self,
        params: DocumentOnTypeFormattingParams,
    ) -> Result<Option<Vec<TextEdit>>> {
        cast_response!(
            self,
            self.request(LspRequest::OnTypeFormatting(params)).await,
            LspResponse::OnTypeFormatting
        )
    }

    async fn code_action(&self, params: CodeActionParams) -> Result<Option<CodeActionResponse>> {
        cast_response!(
            self,
            self.request(LspRequest::CodeAction(params)).await,
            LspResponse::CodeAction
        )
    }
}

// Custom methods for the backend.
//
// NOTE: Request / notification methods _must_ accept a params object,
// even for notifications that don't include any auxiliary data.
//
// I'm not positive, but I think this is related to the way VSCode
// serializes parameters for notifications / requests when no data
// is supplied. Instead of supplying "nothing", it supplies something
// like `[null]` which tower_lsp seems to quietly reject when attempting
// to invoke the registered method.
//
// See also:
//
// https://github.com/Microsoft/vscode-languageserver-node/blob/18fad46b0e8085bb72e1b76f9ea23a379569231a/client/src/common/client.ts#L802-L838
// https://github.com/Microsoft/vscode-languageserver-node/blob/18fad46b0e8085bb72e1b76f9ea23a379569231a/client/src/common/client.ts#L701-L752
impl Backend {
    async fn statement_range(
        &self,
        params: StatementRangeParams,
    ) -> jsonrpc::Result<Option<StatementRangeResponse>> {
        cast_response!(
            self,
            self.request(LspRequest::StatementRange(params)).await,
            LspResponse::StatementRange
        )
    }

    async fn help_topic(
        &self,
        params: HelpTopicParams,
    ) -> jsonrpc::Result<Option<HelpTopicResponse>> {
        cast_response!(
            self,
            self.request(LspRequest::HelpTopic(params)).await,
            LspResponse::HelpTopic
        )
    }

    async fn virtual_document(
        &self,
        params: VirtualDocumentParams,
    ) -> tower_lsp_server::jsonrpc::Result<VirtualDocumentResponse> {
        cast_response!(
            self,
            self.request(LspRequest::VirtualDocument(params)).await,
            LspResponse::VirtualDocument
        )
    }

    async fn input_boundaries(
        &self,
        params: InputBoundariesParams,
    ) -> tower_lsp_server::jsonrpc::Result<InputBoundariesResponse> {
        cast_response!(
            self,
            self.request(LspRequest::InputBoundaries(params)).await,
            LspResponse::InputBoundaries
        )
    }

    async fn notification(&self, params: Option<Value>) {
        log::info!("Received Positron notification: {:?}", params);
    }
}

pub(crate) fn start_lsp(
    r_home: PathBuf,
    runtime: Arc<Runtime>,
    server_start: ServerStartMessage,
    server_started_tx: Sender<ServerStartedMessage>,
    console_notification_tx: AsyncUnboundedSender<ConsoleNotification>,
) {
    runtime.block_on(async {
        let ip_address = server_start.ip_address();

        // Binding to port `0` to allow the OS to allocate a port for us to bind to
        let listener = TcpListener::bind(format!("{ip_address}:0")).await.unwrap();

        let address = match listener.local_addr() {
            Ok(address) => address,
            Err(error) => {
                log::error!("LSP: Failed to bind to {ip_address}:0: {error}");
                return;
            },
        };

        // Get the OS allocated port
        let port = address.port();

        log::trace!("LSP: Thread starting at address {ip_address}:{port}.");

        // Send the port back to `Shell` and eventually out to the frontend so it can connect
        server_started_tx
            .send(ServerStartedMessage::new(port))
            .context("LSP: Can't send server started notification")
            .log_err();

        log::trace!("LSP: Waiting for client");
        let (stream, address) = listener.accept().await.unwrap();
        log::trace!("LSP: Connected to client: '{address}'");
        let (read, write) = tokio::io::split(stream);

        let (shutdown_tx, mut shutdown_rx) = tokio::sync::mpsc::channel::<()>(1);

        let init = |client: Client| {
            let state = GlobalState::new(client.clone(), r_home, console_notification_tx);
            let events_tx = state.events_tx();

            // Start main loop and hold onto the handle that keeps it alive
            let main_loop = state.start();

            // Forward event channel along to `Console`.
            // This also updates an outdated channel after a reconnect.
            // `Console` should be initialized by now, since the caller of this
            // function waits to receive the init notification sent on
            // `kernel_init_rx`. Even if it isn't, this should be okay because
            // `r_task()` defensively blocks until its sender is initialized.
            r_task({
                let events_tx = events_tx.clone();
                move || {
                    Console::get_mut().set_lsp_channel(events_tx);
                }
            });

            Backend {
                shutdown_tx,
                events_tx,
                client,
                _main_loop: main_loop,
            }
        };

        let (service, socket) = LspService::build(init)
            .custom_method(
                statement_range::POSITRON_STATEMENT_RANGE_REQUEST,
                Backend::statement_range,
            )
            .custom_method(help_topic::POSITRON_HELP_TOPIC_REQUEST, Backend::help_topic)
            .custom_method(ARK_VDOC_REQUEST, Backend::virtual_document)
            // In principle this should probably be a Jupyter request
            .custom_method(
                input_boundaries::POSITRON_INPUT_BOUNDARIES_REQUEST,
                Backend::input_boundaries,
            )
            .custom_method("positron/notification", Backend::notification)
            .finish();

        let server = Server::new(read, write, socket);

        tokio::select! {
            _ = server.serve(service) => {
                log::trace!(
                    "LSP: Thread exiting gracefully after connection closed ({:?}).",
                    address
                );
            },
            _ = shutdown_rx.recv() => {
                log::trace!(
                    "LSP: Thread exiting after receiving a shutdown request ({:?}).",
                    address
                );
            }
        }

        // Remove the LSP channel on the way out, we can no longer handle any LSP updates
        // from `Console`, at least until someone starts the LSP up again.
        r_task({
            move || {
                Console::get_mut().remove_lsp_channel();
            }
        });
    })
}

fn new_jsonrpc_error(message: String) -> jsonrpc::Error {
    jsonrpc::Error {
        code: jsonrpc::ErrorCode::ServerError(-1),
        message: message.into(),
        data: None,
    }
}
