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

use std::collections::HashMap;
use std::io::BufReader;
use std::io::BufWriter;
use std::io::Read;
use std::io::Write;
use std::net::TcpListener;
use std::sync::Arc;
use std::sync::Mutex;

use amalthea::comm::comm_channel::CommMsg;
use amalthea::comm::server_comm::ServerStartMessage;
use amalthea::comm::server_comm::ServerStartedMessage;
use amalthea::socket::comm::CommOutgoingTx;
use crossbeam::channel::bounded;
use crossbeam::channel::unbounded;
use crossbeam::channel::Receiver;
use crossbeam::channel::Sender;
use crossbeam::select;
use dap::base_message::Sendable;
use dap::errors::ServerError;
use dap::events::*;
use dap::prelude::*;
use dap::requests::*;
use dap::responses::*;
use dap::server::ServerOutput;
use dap::types::*;
use stdext::result::ResultExt;
use stdext::spawn;

use super::dap_state::Breakpoint;
use super::dap_state::BreakpointState;
use super::dap_state::Dap;
use super::dap_state::DapBackendEvent;
use super::dap_state::THREAD_ID;
use crate::console::Console;
use crate::console::FrameInfo;
use crate::console::FrameSource;
use crate::dap::dap_variables::object_variables;
use crate::dap::dap_variables::RVariable;
use crate::r_task;
use crate::r_task::RTask;
use crate::request::debug_request_command;
use crate::request::DebugRequest;
use crate::request::RRequest;
use crate::url::UrlId;

/// Sentinel expression sent by the frontend to notify the kernel that the user
/// selected a different stack frame in the debugger UI. Subsequent console
/// evaluations will run in that frame's environment.
const SELECTED_FRAME_EXPRESSION: &str = ".positron_selected_frame";

/// Events for the Console requested by a DAP handler. Either delivered by a
/// round-trip through the frontend (so users see the command) or directly to
/// the Console.
pub enum DapConsoleEvent {
    /// Send a debug step/quit command to R via the frontend.
    DebugCommand(DebugRequest),
    /// Interrupt R for a pause. No frontend round-trip.
    Interrupt,
    /// Request a session restart via the frontend.
    Restart,
}

/// The result of handling a single DAP request. The transport layer (TCP or
/// Jupyter) is responsible for delivering the response, DAP events, and console
/// events through the appropriate channel.
pub struct DapOutput {
    pub response: Response,
    pub dap_events: Vec<Event>,
    pub console_events: Vec<DapConsoleEvent>,
}

impl DapOutput {
    pub(crate) fn success(req: Request, output: DapHandlerOutput) -> Self {
        Self {
            response: req.success(output.body),
            dap_events: output.dap_events,
            console_events: output.console_events,
        }
    }

    pub fn error(req: Request, message: &str) -> Self {
        Self {
            response: req.error(message),
            dap_events: vec![],
            console_events: vec![],
        }
    }
}

/// The result of a handler method. Contains a `ResponseBody` (not a full
/// `Response`) plus any events. The dispatcher wraps the body with
/// `req.success()` or `req.error()` to produce a transport-ready `DapOutput`.
pub(crate) struct DapHandlerOutput {
    pub body: ResponseBody,
    pub dap_events: Vec<Event>,
    pub console_events: Vec<DapConsoleEvent>,
}

/// Transport-agnostic handler for DAP requests. Translates each request
/// into a [`DapOutput`] containing the protocol response, any DAP events,
/// and console events that the transport layer is responsible for delivering.
pub struct DapHandler {
    pub(crate) state: Arc<Mutex<Dap>>,
    pub(crate) r_request_tx: Sender<RRequest>,
}

impl DapHandler {
    pub fn new(state: Arc<Mutex<Dap>>, r_request_tx: Sender<RRequest>) -> Self {
        Self {
            state,
            r_request_tx,
        }
    }

    /// Dispatch a parsed DAP request to the appropriate handler.
    ///
    /// `Evaluate` is intentionally not handled here because it requires
    /// transport-specific async response delivery.
    pub fn dispatch(&self, req: Request) -> DapOutput {
        let cmd = req.command.clone();

        let result = match cmd {
            Command::Initialize(args) => self.handle_initialize(args),
            Command::Attach(args) => self.handle_attach(args),
            Command::Disconnect(args) => self.handle_disconnect(args),
            Command::Restart(args) => self.handle_restart(args),
            Command::Threads => self.handle_threads(),
            Command::SetBreakpoints(args) => self.handle_set_breakpoints(args),
            Command::SetExceptionBreakpoints(args) => self.handle_set_exception_breakpoints(args),
            Command::StackTrace(args) => self.handle_stacktrace(args),
            Command::Source(args) => self.handle_source(args),
            Command::Scopes(args) => self.handle_scopes(args),
            Command::Variables(args) => self.handle_variables(args),
            Command::Continue(args) => {
                let resp = ResponseBody::Continue(ContinueResponse {
                    all_threads_continued: Some(true),
                });
                self.handle_step(args, DebugRequest::Continue, resp)
            },
            Command::Next(args) => self.handle_step(args, DebugRequest::Next, ResponseBody::Next),
            Command::StepIn(args) => {
                self.handle_step(args, DebugRequest::StepIn, ResponseBody::StepIn)
            },
            Command::StepOut(args) => {
                self.handle_step(args, DebugRequest::StepOut, ResponseBody::StepOut)
            },
            Command::Pause(args) => self.handle_pause(args),
            Command::ConfigurationDone => Ok(DapHandlerOutput {
                body: ResponseBody::ConfigurationDone,
                dap_events: vec![],
                console_events: vec![],
            }),
            _ => {
                log::warn!("DAP: Unknown request: {cmd:?}");
                return DapOutput::error(req, "Ark DAP: Unknown request");
            },
        };

        match result {
            Ok(output) => DapOutput::success(req, output),
            Err(err) => DapOutput::error(req, &format!("{err}")),
        }
    }

    fn handle_initialize(&self, _args: InitializeArguments) -> anyhow::Result<DapHandlerOutput> {
        let body = ResponseBody::Initialize(types::Capabilities {
            supports_restart_request: Some(true),
            supports_exception_info_request: Some(false),
            exception_breakpoint_filters: Some(vec![
                types::ExceptionBreakpointsFilter {
                    filter: String::from("error"),
                    label: String::from("Errors"),
                    description: Some(String::from("Break on uncaught R errors")),
                    default: Some(false),
                    supports_condition: Some(false),
                    condition_description: None,
                },
                types::ExceptionBreakpointsFilter {
                    filter: String::from("warning"),
                    label: String::from("Warnings"),
                    description: Some(String::from("Break on R warnings")),
                    default: Some(false),
                    supports_condition: Some(false),
                    condition_description: None,
                },
                types::ExceptionBreakpointsFilter {
                    filter: String::from("interrupt"),
                    label: String::from("Interrupts"),
                    description: Some(String::from("Break when execution is interrupted")),
                    default: Some(false),
                    supports_condition: Some(false),
                    condition_description: None,
                },
            ]),
            supports_evaluate_for_hovers: Some(true),
            supports_conditional_breakpoints: Some(true),
            supports_hit_conditional_breakpoints: Some(true),
            supports_log_points: Some(true),
            supports_configuration_done_request: Some(true),
            ..Default::default()
        });
        Ok(DapHandlerOutput {
            body,
            dap_events: vec![Event::Initialized],
            console_events: vec![],
        })
    }

    // Handle SetBreakpoints requests from the frontend.
    //
    // Breakpoint state survives DAP server disconnections via document hashing.
    // Disconnections happen when the user uses the disconnect command (the
    // frontend automatically reconnects) or when the console session goes to
    // the background (the LSP is also disabled, so we don't receive document
    // change notifications). When we come back online, we compare the document
    // content against our stored hash to detect if breakpoints are now stale.
    //
    // Key implementation details:
    // - We use `original_line` for lookup since the frontend doesn't know about
    //   our line adjustments and always sends back the original line numbers.
    // - When a user unchecks a breakpoint, it appears as a deletion (omitted
    //   from the request). We preserve verified breakpoints as Disabled so we
    //   can restore their state when re-enabled without requiring re-sourcing.
    fn handle_set_breakpoints(
        &self,
        args: SetBreakpointsArguments,
    ) -> anyhow::Result<DapHandlerOutput> {
        let Some(path) = args.source.path.as_ref() else {
            return Err(anyhow::anyhow!("Missing a path to set breakpoints for"));
        };

        // We currently only support "path" URIs as Positron never sends URIs.
        // In principle the DAP frontend can negotiate whether it sends URIs or
        // file paths via the `pathFormat` field of the `Initialize` request.
        let uri = match UrlId::from_file_path(path) {
            Ok(uri) => uri,
            Err(err) => {
                log::warn!("Can't set breakpoints for non-file path: '{path}': {err}");
                return Ok(DapHandlerOutput {
                    body: ResponseBody::SetBreakpoints(SetBreakpointsResponse {
                        breakpoints: vec![],
                    }),
                    dap_events: vec![],
                    console_events: vec![],
                });
            },
        };

        // Read document content to compute hash. We currently assume UTF-8 even
        // though the frontend supports files with different encodings (but
        // UTF-8 is the default).
        let doc_content = match std::fs::read_to_string(path) {
            Ok(content) => content,
            Err(err) => {
                // TODO: What do we do with breakpoints in virtual documents?
                log::warn!("Failed to read file '{path}': {err:?}");

                let breakpoints = args
                    .breakpoints
                    .unwrap_or_default()
                    .iter()
                    .map(|bp| dap::types::Breakpoint {
                        id: Some(self.state.lock().unwrap().next_breakpoint_id()),
                        verified: false,
                        line: Some(bp.line),
                        message: Some(String::from("Can't read file '{path}'")),
                        ..Default::default()
                    })
                    .collect();

                return Ok(DapHandlerOutput {
                    body: ResponseBody::SetBreakpoints(SetBreakpointsResponse { breakpoints }),
                    dap_events: vec![],
                    console_events: vec![],
                });
            },
        };

        let args_breakpoints = args.breakpoints.unwrap_or_default();

        let mut state = self.state.lock().unwrap();
        let old_breakpoints = state.breakpoints.get(&uri).cloned();

        // Breakpoints are associated with this hash. If the document has
        // changed after a reconnection, the breakpoints are no longer valid.
        let doc_hash = blake3::hash(doc_content.as_bytes());
        let doc_changed = match &old_breakpoints {
            Some((existing_hash, _)) => existing_hash != &doc_hash,
            None => true,
        };

        let new_breakpoints = if doc_changed {
            log::trace!("DAP: Document changed for {uri}, discarding old breakpoints");

            // Replace all existing breakpoints by new, unverified ones
            args_breakpoints
                .iter()
                .map(|bp| {
                    let line = Breakpoint::from_dap_line(bp.line);
                    Breakpoint {
                        id: state.next_breakpoint_id(),
                        line,
                        original_line: line,
                        state: BreakpointState::Unverified,
                        injected: false,
                        condition: bp.condition.clone(),
                        log_message: bp.log_message.clone(),
                        hit_condition: bp.hit_condition.clone(),
                        hit_count: 0,
                    }
                })
                .collect()
        } else {
            log::trace!("DAP: Document unchanged for {uri}, preserving breakpoint states");

            // Unwrap Safety: `doc_changed` is false, so `old_breakpoints` is Some
            let (_, old_breakpoints) = old_breakpoints.unwrap();
            // Use original_line for lookup since that's what the frontend sends back
            let mut old_by_line: HashMap<u32, Breakpoint> = old_breakpoints
                .into_iter()
                .map(|bp| (bp.original_line, bp))
                .collect();

            let mut breakpoints: Vec<Breakpoint> = Vec::new();

            for bp in &args_breakpoints {
                let line = Breakpoint::from_dap_line(bp.line);

                if let Some(old_bp) = old_by_line.remove(&line) {
                    // Breakpoint already exists at this line
                    let (new_state, injected) = match old_bp.state {
                        // This breakpoint used to be verified, was disabled, and is now back
                        // online. Restore to Verified immediately.
                        BreakpointState::Disabled => (BreakpointState::Verified, old_bp.injected),
                        // Invalid breakpoints are reset to Unverified so they can be
                        // re-validated on next source.
                        BreakpointState::Invalid(_) => (BreakpointState::Unverified, false),
                        // We preserve other states (verified or unverified)
                        other => (other, old_bp.injected),
                    };

                    breakpoints.push(Breakpoint {
                        id: old_bp.id,
                        // Preserve the actual (anchored) line from previous verification
                        line: old_bp.line,
                        original_line: line,
                        state: new_state,
                        injected,
                        condition: bp.condition.clone(),
                        log_message: bp.log_message.clone(),
                        hit_condition: bp.hit_condition.clone(),
                        hit_count: 0,
                    });
                } else {
                    // New breakpoints always start as Unverified, until they get evaluated once
                    breakpoints.push(Breakpoint {
                        id: state.next_breakpoint_id(),
                        line,
                        original_line: line,
                        state: BreakpointState::Unverified,
                        injected: false,
                        condition: bp.condition.clone(),
                        log_message: bp.log_message.clone(),
                        hit_condition: bp.hit_condition.clone(),
                        hit_count: 0,
                    });
                }
            }

            // Remaining verified breakpoints need to be preserved in memory
            // when deleted. That's because when user unchecks a breakpoint on
            // the frontend, the breakpoint is actually deleted (i.e. omitted)
            // by a `SetBreakpoints()` request. When the user reenables the
            // breakpoint, we have to restore the verification state.
            // Unverified/Invalid breakpoints on the other hand are simply
            // dropped since there's no verified state that needs to be
            // preserved.
            for (original_line, old_bp) in old_by_line {
                if matches!(old_bp.state, BreakpointState::Verified) {
                    breakpoints.push(Breakpoint {
                        id: old_bp.id,
                        line: old_bp.line,
                        original_line,
                        state: BreakpointState::Disabled,
                        injected: true,
                        condition: old_bp.condition.clone(),
                        log_message: old_bp.log_message.clone(),
                        hit_condition: old_bp.hit_condition,
                        hit_count: 0,
                    });
                }
            }

            breakpoints
        };

        log::trace!(
            "DAP: URI {uri} now has {} breakpoints:\n{:#?}",
            new_breakpoints.len(),
            new_breakpoints
        );

        let response_breakpoints: Vec<dap::types::Breakpoint> = new_breakpoints
            .iter()
            .filter(|bp| !matches!(bp.state, BreakpointState::Disabled))
            .map(|bp| {
                let message = match &bp.state {
                    BreakpointState::Invalid(reason) => Some(reason.message().to_string()),
                    _ => None,
                };
                dap::types::Breakpoint {
                    id: Some(bp.id),
                    verified: matches!(bp.state, BreakpointState::Verified),
                    line: Some(Breakpoint::to_dap_line(bp.line)),
                    message,
                    ..Default::default()
                }
            })
            .collect();

        state.breakpoints.insert(uri, (doc_hash, new_breakpoints));

        drop(state);

        Ok(DapHandlerOutput {
            body: ResponseBody::SetBreakpoints(SetBreakpointsResponse {
                breakpoints: response_breakpoints,
            }),
            dap_events: vec![],
            console_events: vec![],
        })
    }

    fn handle_attach(&self, _args: AttachRequestArguments) -> anyhow::Result<DapHandlerOutput> {
        self.state.lock().unwrap().is_connected = true;

        Ok(DapHandlerOutput {
            body: ResponseBody::Attach,
            dap_events: vec![Event::Thread(ThreadEventBody {
                reason: ThreadEventReason::Started,
                thread_id: THREAD_ID,
            })],
            console_events: vec![],
        })
    }

    fn handle_disconnect(&self, _args: DisconnectArguments) -> anyhow::Result<DapHandlerOutput> {
        let mut state = self.state.lock().unwrap();
        let is_debugging = state.is_debugging;
        state.is_connected = false;
        drop(state);

        let console_events = if is_debugging {
            vec![DapConsoleEvent::DebugCommand(DebugRequest::Quit)]
        } else {
            vec![]
        };

        Ok(DapHandlerOutput {
            body: ResponseBody::Disconnect,
            dap_events: vec![],
            console_events,
        })
    }

    fn handle_restart<T>(&self, _args: T) -> anyhow::Result<DapHandlerOutput> {
        Ok(DapHandlerOutput {
            body: ResponseBody::Restart,
            dap_events: vec![],
            console_events: vec![DapConsoleEvent::Restart],
        })
    }

    // All servers must respond to `Threads` requests, possibly with
    // a dummy thread as is the case here
    fn handle_threads(&self) -> anyhow::Result<DapHandlerOutput> {
        Ok(DapHandlerOutput {
            body: ResponseBody::Threads(ThreadsResponse {
                threads: vec![Thread {
                    id: THREAD_ID,
                    name: String::from("R console"),
                }],
            }),
            dap_events: vec![],
            console_events: vec![],
        })
    }

    fn handle_set_exception_breakpoints(
        &self,
        args: SetExceptionBreakpointsArguments,
    ) -> anyhow::Result<DapHandlerOutput> {
        {
            let mut state = self.state.lock().unwrap();
            state.exception_breakpoint_filters = args.filters;
        }
        Ok(DapHandlerOutput {
            body: ResponseBody::SetExceptionBreakpoints(SetExceptionBreakpointsResponse {
                // This field is only useful for reporting problems with
                // individual filters. Since we always accept all filters,
                // `None` is fine here.
                breakpoints: None,
            }),
            dap_events: vec![],
            console_events: vec![],
        })
    }

    fn handle_stacktrace(&self, args: StackTraceArguments) -> anyhow::Result<DapHandlerOutput> {
        let stack = {
            let state = self.state.lock().unwrap();
            let fallback_sources = &state.fallback_sources;
            match &state.stack {
                Some(stack) => stack
                    .iter()
                    .map(|frame| into_dap_frame(frame, fallback_sources))
                    .collect(),
                _ => vec![],
            }
        };

        // Slice the stack as requested
        let n_usize = stack.len();

        let start_frame = args.start_frame.unwrap_or(0);
        let Ok(start) = usize::try_from(start_frame) else {
            return Err(anyhow::anyhow!("Invalid start_frame: {start_frame}"));
        };
        let start = std::cmp::min(start, n_usize);

        let end = if let Some(levels) = args.levels {
            let Ok(levels) = usize::try_from(levels) else {
                return Err(anyhow::anyhow!("Invalid levels: {levels}"));
            };
            std::cmp::min(start.saturating_add(levels), n_usize)
        } else {
            n_usize
        };

        let Ok(total_frames) = i64::try_from(n_usize) else {
            return Err(anyhow::anyhow!(
                "Stack frame count overflows i64: {n_usize}"
            ));
        };
        let stack = stack[start..end].to_vec();

        Ok(DapHandlerOutput {
            body: ResponseBody::StackTrace(StackTraceResponse {
                stack_frames: stack,
                total_frames: Some(total_frames),
            }),
            dap_events: vec![],
            console_events: vec![],
        })
    }

    fn handle_source(&self, _args: SourceArguments) -> anyhow::Result<DapHandlerOutput> {
        Err(anyhow::anyhow!("Unsupported `source` request"))
    }

    fn handle_scopes(&self, args: ScopesArguments) -> anyhow::Result<DapHandlerOutput> {
        let state = self.state.lock().unwrap();
        let frame_id_to_variables_reference = &state.frame_id_to_variables_reference;

        // Entirely possible that the requested `frame_id` doesn't have any
        // variables (like the top most frame where the call was made). We send
        // back `0` in those cases, which is an indication of "no variables".
        let variables_reference = frame_id_to_variables_reference
            .get(&args.frame_id)
            .copied()
            .unwrap_or(0);

        // Only 1 overarching scope for now
        let scopes = vec![Scope {
            name: String::from("Locals"),
            presentation_hint: Some(ScopePresentationhint::Locals),
            variables_reference,
            named_variables: None,
            indexed_variables: None,
            expensive: false,
            source: None,
            line: None,
            column: None,
            end_line: None,
            end_column: None,
        }];

        drop(state);
        Ok(DapHandlerOutput {
            body: ResponseBody::Scopes(ScopesResponse { scopes }),
            dap_events: vec![],
            console_events: vec![],
        })
    }

    fn handle_variables(&self, args: VariablesArguments) -> anyhow::Result<DapHandlerOutput> {
        let variables_reference = args.variables_reference;
        let variables = self.collect_r_variables(variables_reference);
        let variables = self.make_variables(variables);
        Ok(DapHandlerOutput {
            body: ResponseBody::Variables(VariablesResponse { variables }),
            dap_events: vec![],
            console_events: vec![],
        })
    }

    fn collect_r_variables(&self, variables_reference: i64) -> Vec<RVariable> {
        // Wait until we're in the `r_task()` to lock
        // See https://github.com/posit-dev/positron/issues/5024
        let state = self.state.clone();

        let variables = r_task(move || {
            let state = state.lock().unwrap();
            let variables_reference_to_r_object = &state.variables_reference_to_r_object;

            let Some(object) = variables_reference_to_r_object.get(&variables_reference) else {
                log::error!(
                    "Failed to locate R object for `variables_reference` {variables_reference}."
                );
                return Vec::new();
            };

            let object = object.get();
            object_variables(object.sexp)
        });

        variables
    }

    // `make_variables()` doesn't require R, so locking outside of an
    // `r_task()` is fine here, unlike in `collect_r_variables()`.
    fn make_variables(&self, variables: Vec<RVariable>) -> Vec<Variable> {
        self.state.lock().unwrap().make_variables(variables)
    }

    fn handle_step<A>(
        &self,
        _args: A,
        cmd: DebugRequest,
        resp: ResponseBody,
    ) -> anyhow::Result<DapHandlerOutput> {
        Ok(DapHandlerOutput {
            body: resp,
            dap_events: vec![],
            console_events: vec![DapConsoleEvent::DebugCommand(cmd)],
        })
    }

    fn handle_pause(&self, _args: PauseArguments) -> anyhow::Result<DapHandlerOutput> {
        self.state.lock().unwrap().is_interrupting_for_debugger = true;

        log::info!("DAP: Received request to pause R, sending interrupt");

        Ok(DapHandlerOutput {
            body: ResponseBody::Pause,
            dap_events: vec![],
            console_events: vec![DapConsoleEvent::Interrupt],
        })
    }
}

// TODO: Handle comm close to shut down the DAP server thread.
//
// The DAP comm is allowed to persist across TCP sessions. This supports session
// switching on the frontend. Ideally the frontend would be allowed to close the
// DAP comm in addition to the DAP TCP connection, which would shut down the DAP
// server. To achive this, the DAP server, once disconnected should wait for both
// the connection becoming ready and a channel event signalling comm close. If
// the latter fires, shut the server down.

pub fn start_dap(
    state: Arc<Mutex<Dap>>,
    server_start: ServerStartMessage,
    server_started_tx: Sender<ServerStartedMessage>,
    r_request_tx: Sender<RRequest>,
    comm_tx: CommOutgoingTx,
) {
    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",)).unwrap();

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

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

    log::trace!("DAP: 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))
        .log_err();

    loop {
        log::trace!("DAP: Waiting for client");

        let stream = match listener.accept() {
            Ok((stream, addr)) => {
                log::info!("DAP: Connected to client {addr:?}");

                let mut state = state.lock().unwrap();
                state.is_connected = true;

                stream
            },
            Err(e) => {
                log::error!("DAP: Can't get client: {e:?}");
                continue;
            },
        };

        let reader = BufReader::new(&stream);
        let writer = BufWriter::new(&stream);
        let (responses_tx, responses_rx) = unbounded::<Response>();

        let mut server = DapServer::new(
            reader,
            writer,
            state.clone(),
            r_request_tx.clone(),
            comm_tx.clone(),
            responses_tx,
        );

        let (backend_events_tx, backend_events_rx) = unbounded::<DapBackendEvent>();
        let (done_tx, done_rx) = bounded::<bool>(0);
        let output_clone = server.output.clone();

        // We need a scope to let the borrow checker know that
        // `output_clone` drops before the next iteration (it gets tangled
        // to the stack variable `stream` through `server`)
        let _ = crossbeam::thread::scope(|scope| {
            spawn!(scope, "ark-dap-events", {
                move |_| listen_dap_events(output_clone, backend_events_rx, responses_rx, done_rx)
            });

            // Connect the backend to the events thread
            state.lock().unwrap().backend_events_tx = Some(backend_events_tx);

            loop {
                // If disconnected, break and accept a new connection to create a new server
                if !server.serve() {
                    log::trace!("DAP: Disconnected from client");
                    let mut state = state.lock().unwrap();
                    state.is_connected = false;
                    break;
                }
            }

            // Terminate the events thread
            let _ = done_tx.send(true);
        });
    }
}

// Thread that listens for events sent by the backend, usually the
// `ReadConsole()` method. These are forwarded to the DAP client.
fn listen_dap_events<W: Write>(
    output: Arc<Mutex<ServerOutput<W>>>,
    backend_events_rx: Receiver<DapBackendEvent>,
    responses_rx: Receiver<Response>,
    done_rx: Receiver<bool>,
) {
    loop {
        select!(
            recv(backend_events_rx) -> event => {
                let event = match event {
                    Ok(event) => event,
                    Err(err) => {
                        // Channel closed, sender dropped
                        log::info!("DAP: Event channel closed: {err:?}");
                        return;
                    },
                };

                log::trace!("DAP: Got event from backend: {:?}", event);

                let event = event.into_dap_event();

                let mut output = output.lock().unwrap();
                if let Err(err) = output.send_event(event) {
                    log::warn!("DAP: Failed to send event, closing: {err:?}");
                    return;
                }
            },

            recv(responses_rx) -> response => {
                let response = match response {
                    Ok(response) => response,
                    Err(err) => {
                        log::info!("DAP: Responses channel closed: {err:?}");
                        return;
                    },
                };

                log::trace!("DAP: Sending async response: {:?}", response);

                let mut output = output.lock().unwrap();
                if let Err(err) = output.send(Sendable::Response(response)) {
                    log::warn!("DAP: Failed to send response, closing: {err:?}");
                    return;
                }
            },

            // Break the loop and terminate the thread
            recv(done_rx) -> _ => { return; },
        )
    }
}

pub struct DapServer<R: Read, W: Write> {
    server: Server<R, W>,
    pub output: Arc<Mutex<ServerOutput<W>>>,
    pub(crate) handler: DapHandler,
    comm_tx: Option<CommOutgoingTx>,
    responses_tx: Sender<Response>,
}

impl<R: Read, W: Write> DapServer<R, W> {
    pub fn new(
        reader: BufReader<R>,
        writer: BufWriter<W>,
        state: Arc<Mutex<Dap>>,
        r_request_tx: Sender<RRequest>,
        comm_tx: CommOutgoingTx,
        responses_tx: Sender<Response>,
    ) -> Self {
        let server = Server::new(reader, writer);
        let output = server.output.clone();
        let handler = DapHandler::new(state, r_request_tx);
        Self {
            server,
            output,
            handler,
            comm_tx: Some(comm_tx),
            responses_tx,
        }
    }

    pub fn serve(&mut self) -> bool {
        log::trace!("DAP: Polling");
        let req = match self.server.poll_request() {
            Ok(Some(req)) => req,
            Ok(None) => return false,
            Err(err) => {
                log::warn!("DAP: Connection closed: {err:?}");
                return false;
            },
        };
        log::trace!("DAP: Got request: {:#?}", req);

        // Evaluate is async: the response is sent later via `responses_tx`.
        // It is the only command that needs transport-specific handling.
        if let Command::Evaluate(args) = &req.command {
            let args = args.clone();
            if let Err(err) = self.handle_evaluate(req, args) {
                log::warn!("DAP: Handler failed, closing connection: {err:?}");
                return false;
            }
            return true;
        }

        let output = self.handler.dispatch(req);
        self.deliver(output)
    }

    fn deliver(&mut self, output: DapOutput) -> bool {
        for event in output.console_events {
            self.handle_console_event(event);
        }

        if self.respond(output.response).log_err().is_none() {
            return false;
        }

        for event in output.dap_events {
            if self.send_event(event).log_err().is_none() {
                return false;
            }
        }

        true
    }

    fn handle_console_event(&mut self, event: DapConsoleEvent) {
        match event {
            DapConsoleEvent::DebugCommand(cmd) => {
                if let Some(tx) = &self.comm_tx {
                    // If we have a comm channel (always the case as of this
                    // writing) we are connected to Positron or similar. Send
                    // control events so that the IDE can execute these as if they
                    // were sent by the user. This ensures prompts are updated.
                    let msg = amalthea::comm_rpc_message!(
                        "execute",
                        command = debug_request_command(cmd)
                    );
                    tx.send(msg).log_err();
                } else {
                    self.handler
                        .r_request_tx
                        .send(RRequest::DebugCommand(cmd))
                        .log_err();
                }
            },
            DapConsoleEvent::Interrupt => {
                crate::sys::control::handle_interrupt_request();
            },
            DapConsoleEvent::Restart => {
                // If connected to Positron, forward the restart command to
                // the frontend. Otherwise ignore it.
                if let Some(tx) = &self.comm_tx {
                    let msg = amalthea::comm_rpc_message!("restart");
                    tx.send(msg).log_err();
                }
            },
        }
    }

    fn respond(&mut self, rsp: Response) -> Result<(), ServerError> {
        log::trace!("DAP: Sending response: {rsp:#?}");
        self.server.respond(rsp)
    }

    fn send_event(&mut self, event: Event) -> Result<(), ServerError> {
        log::trace!("DAP: Sending event: {event:#?}");
        self.server.send_event(event)
    }

    // Tied to the TCP transport for now. For Jupyter we need to figure out how
    // to do async responses with Jupyter's Control channel.
    fn handle_evaluate(
        &mut self,
        req: Request,
        args: EvaluateArguments,
    ) -> Result<(), ServerError> {
        let expression = args.expression;
        let frame_id = args.frame_id;
        let state = self.handler.state.clone();
        let responses_tx = self.responses_tx.clone();

        log::trace!("DAP: Spawning idle task for evaluate");
        r_task::spawn(RTask::send_idle_any_prompt(async move |mut capture| {
            log::trace!("DAP: Idle task started for evaluate");

            // If expression starts with "/print ", evaluate and print result
            let (expr, print) = match expression.strip_prefix("/print ") {
                Some(expr) => (expr, true),
                None => (expression.as_str(), false),
            };

            let rsp = if expression == SELECTED_FRAME_EXPRESSION {
                log::trace!("DAP: Received frame selection sentinel, frame_id: {frame_id:?}");
                Console::get_mut().set_debug_selected_frame_id(frame_id);
                req.success(ResponseBody::Evaluate(EvaluateResponse {
                    result: String::new(),
                    type_field: None,
                    presentation_hint: None,
                    variables_reference: 0,
                    named_variables: None,
                    indexed_variables: None,
                    memory_reference: None,
                }))
            } else {
                let capture = if print { Some(&mut capture) } else { None };
                let result = state.lock().unwrap().evaluate(expr, frame_id, capture);
                log::trace!("DAP: Evaluate completed, success: {}", result.is_ok());

                match result {
                    Ok(variable) => {
                        let response = state.lock().unwrap().into_evaluate_response(variable);
                        req.success(ResponseBody::Evaluate(response))
                    },
                    Err(err) => req.error(&format!("Error: {err}")),
                }
            };

            responses_tx.send(rsp).log_err();
        }));

        Ok(())
    }
}

fn into_dap_frame(frame: &FrameInfo, fallback_sources: &HashMap<String, String>) -> StackFrame {
    let id = frame.id;
    let source_name = frame.source_name.clone();
    let frame_name = frame.frame_name.clone();
    let source = frame.source.clone();
    let start_line = frame.start_line;
    let start_column = frame.start_column;
    let end_line = frame.end_line;
    let end_column = frame.end_column;

    // Retrieve either `path` or `source_reference` depending on the `source` type.
    // In the `Text` case, a `source_reference` should always exist because we loaded
    // the map with all possible text values in `start_debug()`.
    let path = match source {
        FrameSource::File(path) => Some(path),
        FrameSource::Text(source) => fallback_sources.get(&source).cloned().or_else(|| {
            log::error!("Failed to find a source reference for source text: '{source}'");
            None
        }),
    };

    let src = Source {
        name: Some(source_name),
        path,
        source_reference: None,
        presentation_hint: None,
        origin: None,
        sources: None,
        adapter_data: None,
        checksums: None,
    };

    StackFrame {
        id,
        name: frame_name,
        source: Some(src),
        line: start_line,
        column: start_column,
        end_line: Some(end_line),
        end_column: Some(end_column),
        can_restart: None,
        instruction_pointer_reference: None,
        module_id: None,
        presentation_hint: None,
    }
}
