use crate::{RpcHandler, error::RequestError, handler::handle_request};
use anvil_rpc::{
    error::RpcError,
    request::Request,
    response::{Response, ResponseResult},
};

use futures::{FutureExt, Sink, SinkExt, Stream, StreamExt};
use parking_lot::Mutex;
use serde::de::DeserializeOwned;
use std::{
    collections::VecDeque,
    fmt,
    hash::Hash,
    pin::Pin,
    sync::Arc,
    task::{Context, Poll},
};

/// The general purpose trait for handling RPC requests and subscriptions
#[async_trait::async_trait]
pub trait PubSubRpcHandler: Clone + Send + Sync + Unpin + 'static {
    /// The request type to expect
    type Request: DeserializeOwned + Send + Sync + fmt::Debug;
    /// The identifier to use for subscriptions
    type SubscriptionId: Hash + PartialEq + Eq + Send + Sync + fmt::Debug;
    /// The subscription type this handle may create
    type Subscription: Stream<Item = serde_json::Value> + Send + Sync + Unpin;

    /// Invoked when the request was received
    async fn on_request(&self, request: Self::Request, cx: PubSubContext<Self>) -> ResponseResult;
}

type Subscriptions<SubscriptionId, Subscription> = Arc<Mutex<Vec<(SubscriptionId, Subscription)>>>;

/// Contains additional context and tracks subscriptions
pub struct PubSubContext<Handler: PubSubRpcHandler> {
    /// all active subscriptions `id -> Stream`
    subscriptions: Subscriptions<Handler::SubscriptionId, Handler::Subscription>,
}

impl<Handler: PubSubRpcHandler> PubSubContext<Handler> {
    /// Adds new active subscription
    ///
    /// Returns the previous subscription, if any
    pub fn add_subscription(
        &self,
        id: Handler::SubscriptionId,
        subscription: Handler::Subscription,
    ) -> Option<Handler::Subscription> {
        let mut subscriptions = self.subscriptions.lock();
        let mut removed = None;
        if let Some(idx) = subscriptions.iter().position(|(i, _)| id == *i) {
            trace!(target: "rpc", ?id,  "removed subscription");
            removed = Some(subscriptions.swap_remove(idx).1);
        }
        trace!(target: "rpc", ?id,  "added subscription");
        subscriptions.push((id, subscription));
        removed
    }

    /// Removes an existing subscription
    pub fn remove_subscription(
        &self,
        id: &Handler::SubscriptionId,
    ) -> Option<Handler::Subscription> {
        let mut subscriptions = self.subscriptions.lock();
        if let Some(idx) = subscriptions.iter().position(|(i, _)| id == i) {
            trace!(target: "rpc", ?id,  "removed subscription");
            return Some(subscriptions.swap_remove(idx).1);
        }
        None
    }
}

impl<Handler: PubSubRpcHandler> Clone for PubSubContext<Handler> {
    fn clone(&self) -> Self {
        Self { subscriptions: Arc::clone(&self.subscriptions) }
    }
}

impl<Handler: PubSubRpcHandler> Default for PubSubContext<Handler> {
    fn default() -> Self {
        Self { subscriptions: Arc::new(Mutex::new(Vec::new())) }
    }
}

/// A compatibility helper type to use common `RpcHandler` functions
struct ContextAwareHandler<Handler: PubSubRpcHandler> {
    handler: Handler,
    context: PubSubContext<Handler>,
}

impl<Handler: PubSubRpcHandler> Clone for ContextAwareHandler<Handler> {
    fn clone(&self) -> Self {
        Self { handler: self.handler.clone(), context: self.context.clone() }
    }
}

#[async_trait::async_trait]
impl<Handler: PubSubRpcHandler> RpcHandler for ContextAwareHandler<Handler> {
    type Request = Handler::Request;

    async fn on_request(&self, request: Self::Request) -> ResponseResult {
        self.handler.on_request(request, self.context.clone()).await
    }
}

/// Represents a connection to a client via websocket
///
/// Contains the state for the entire connection
pub struct PubSubConnection<Handler: PubSubRpcHandler, Connection> {
    /// the handler for the websocket connection
    handler: Handler,
    /// contains all the subscription related context
    context: PubSubContext<Handler>,
    /// The established connection
    connection: Connection,
    /// currently in progress requests
    processing: Vec<Pin<Box<dyn Future<Output = Option<Response>> + Send>>>,
    /// pending messages to send
    pending: VecDeque<String>,
}

impl<Handler: PubSubRpcHandler, Connection> PubSubConnection<Handler, Connection> {
    pub fn new(connection: Connection, handler: Handler) -> Self {
        Self {
            connection,
            handler,
            context: Default::default(),
            pending: Default::default(),
            processing: Default::default(),
        }
    }

    fn process_request(&mut self, req: serde_json::Result<Request>) {
        let handler =
            ContextAwareHandler { handler: self.handler.clone(), context: self.context.clone() };
        self.processing.push(Box::pin(async move {
            match req {
                Ok(req) => handle_request(req, handler).await,
                Err(err) => {
                    error!(target: "rpc", ?err, "invalid request");
                    Some(Response::error(RpcError::invalid_request()))
                }
            }
        }));
    }
}

impl<Handler, Connection> Future for PubSubConnection<Handler, Connection>
where
    Handler: PubSubRpcHandler,
    Connection: Sink<String> + Stream<Item = Result<Option<Request>, RequestError>> + Unpin,
    <Connection as Sink<String>>::Error: fmt::Debug,
{
    type Output = ();

    fn poll(self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<Self::Output> {
        let pin = self.get_mut();
        loop {
            // drive the websocket
            while matches!(pin.connection.poll_ready_unpin(cx), Poll::Ready(Ok(()))) {
                // only start sending if socket is ready
                if let Some(msg) = pin.pending.pop_front() {
                    if let Err(err) = pin.connection.start_send_unpin(msg) {
                        error!(target: "rpc", ?err, "Failed to send message");
                    }
                } else {
                    break;
                }
            }

            // Ensure any pending messages are flushed
            // this needs to be called manually for tungsenite websocket: <https://github.com/foundry-rs/foundry/issues/6345>
            if let Poll::Ready(Err(err)) = pin.connection.poll_flush_unpin(cx) {
                trace!(target: "rpc", ?err, "websocket err");
                // close the connection
                return Poll::Ready(());
            }

            loop {
                match pin.connection.poll_next_unpin(cx) {
                    Poll::Ready(Some(req)) => match req {
                        Ok(Some(req)) => {
                            pin.process_request(Ok(req));
                        }
                        Err(err) => match err {
                            RequestError::Axum(err) => {
                                trace!(target: "rpc", ?err, "client disconnected");
                                return Poll::Ready(());
                            }
                            RequestError::Io(err) => {
                                trace!(target: "rpc", ?err, "client disconnected");
                                return Poll::Ready(());
                            }
                            RequestError::Serde(err) => {
                                pin.process_request(Err(err));
                            }
                            RequestError::Disconnect => {
                                trace!(target: "rpc", "client disconnected");
                                return Poll::Ready(());
                            }
                        },
                        _ => {}
                    },
                    Poll::Ready(None) => {
                        trace!(target: "rpc", "socket connection finished");
                        return Poll::Ready(());
                    }
                    Poll::Pending => break,
                }
            }

            let mut progress = false;
            for n in (0..pin.processing.len()).rev() {
                let mut req = pin.processing.swap_remove(n);
                #[allow(clippy::collapsible_match)]
                match req.poll_unpin(cx) {
                    Poll::Ready(Some(resp)) => {
                        if let Ok(text) = serde_json::to_string(&resp) {
                            pin.pending.push_back(text);
                            progress = true;
                        }
                    }
                    Poll::Ready(None) => {}
                    Poll::Pending => pin.processing.push(req),
                }
            }

            {
                // process subscription events
                let mut subscriptions = pin.context.subscriptions.lock();
                'outer: for n in (0..subscriptions.len()).rev() {
                    let (id, mut sub) = subscriptions.swap_remove(n);
                    'inner: loop {
                        #[allow(clippy::collapsible_match)]
                        match sub.poll_next_unpin(cx) {
                            Poll::Ready(Some(res)) => {
                                if let Ok(text) = serde_json::to_string(&res) {
                                    pin.pending.push_back(text);
                                    progress = true;
                                }
                            }
                            Poll::Ready(None) => continue 'outer,
                            Poll::Pending => break 'inner,
                        }
                    }

                    subscriptions.push((id, sub));
                }
            }

            if !progress {
                return Poll::Pending;
            }
        }
    }
}

#[cfg(test)]
mod tests {
    use super::*;
    use anvil_rpc::{
        request::{RequestParams, RpcCall, RpcNotification, Version},
        response::RpcResponse,
    };
    use std::{pin::pin, task::Waker};

    #[derive(Clone)]
    struct TestHandler;

    #[async_trait::async_trait]
    impl PubSubRpcHandler for TestHandler {
        type Request = serde_json::Value;
        type SubscriptionId = u64;
        type Subscription = futures::stream::Empty<serde_json::Value>;

        async fn on_request(
            &self,
            _request: Self::Request,
            _cx: PubSubContext<Self>,
        ) -> ResponseResult {
            ResponseResult::success(serde_json::Value::Null)
        }
    }

    fn notification() -> RpcCall {
        RpcCall::Notification(RpcNotification {
            jsonrpc: Some(Version::V2),
            method: "eth_subscribe".to_owned(),
            params: RequestParams::None,
        })
    }

    fn run_ready<F: Future>(future: F) -> F::Output {
        let waker = Waker::noop();
        let mut cx = Context::from_waker(waker);
        let mut future = pin!(future);
        match future.as_mut().poll(&mut cx) {
            Poll::Ready(output) => output,
            Poll::Pending => panic!("future unexpectedly pending"),
        }
    }

    #[test]
    fn process_request_keeps_empty_batch_invalid() {
        let mut connection = PubSubConnection::new((), TestHandler);
        connection.process_request(Ok(Request::Batch(vec![])));

        let response = run_ready(connection.processing.pop().unwrap());
        assert_eq!(
            response,
            Some(Response::Single(RpcResponse::from(RpcError::invalid_request())))
        );
    }

    #[test]
    fn process_request_skips_notification_only_batch_response() {
        let mut connection = PubSubConnection::new((), TestHandler);
        connection.process_request(Ok(Request::Batch(vec![notification()])));

        let response = run_ready(connection.processing.pop().unwrap());
        assert_eq!(response, None);
    }
}
