use futures::{SinkExt, StreamExt};
use rootcause::prelude::*;

pub enum WsMessage {
    Text(String),
    Close,
}

pub struct AppWebSocket {
    #[cfg(not(feature = "wasm"))]
    inner: tokio_tungstenite::WebSocketStream<
        tokio_tungstenite::MaybeTlsStream<tokio::net::TcpStream>,
    >,
    #[cfg(feature = "wasm")]
    inner: ws_stream_wasm::WsStream,
    #[cfg(feature = "wasm")]
    meta: ws_stream_wasm::WsMeta,
}

impl AppWebSocket {
    pub async fn connect(url: &str) -> Result<Self, Report> {
        #[cfg(not(feature = "wasm"))]
        {
            use tokio::time::{Duration, timeout};

            let (stream, _) = timeout(
                Duration::from_secs(30),
                tokio_tungstenite::connect_async(url),
            )
            .await?
            .context(
                "Timed out connecting to provisioning socket. Try a different anisette server.",
            )?;
            Ok(Self { inner: stream })
        }
        #[cfg(feature = "wasm")]
        {
            let proxied = format!(
                "https://worker.nabdev.workers.dev/?url={}",
                urlencoding::encode(url)
            )
            .replace("https://", "wss://");

            let (meta, stream) = ws_stream_wasm::WsMeta::connect(&proxied, None)
                .await
                .map_err(|e| report!("WS connect failed: {e:?}"))?;
            Ok(Self {
                meta,
                inner: stream,
            })
        }
    }

    pub async fn send_text(&mut self, text: String) -> Result<(), Report> {
        #[cfg(not(feature = "wasm"))]
        self.inner
            .send(tokio_tungstenite::tungstenite::Message::Text(text.into()))
            .await?;
        #[cfg(feature = "wasm")]
        self.inner
            .send(ws_stream_wasm::WsMessage::Text(text))
            .await
            .map_err(|e| report!("WS send failed: {e:?}"))?;
        Ok(())
    }

    pub async fn next(&mut self) -> Option<Result<WsMessage, Report>> {
        #[cfg(not(feature = "wasm"))]
        {
            let msg = self.inner.next().await?;
            Some(msg.map_err(Into::into).map(|m| match m {
                tokio_tungstenite::tungstenite::Message::Text(t) => WsMessage::Text(t.to_string()),
                tokio_tungstenite::tungstenite::Message::Close(_) => WsMessage::Close,
                _ => WsMessage::Close,
            }))
        }
        #[cfg(feature = "wasm")]
        {
            let msg = self.inner.next().await?;
            Some(Ok(match msg {
                ws_stream_wasm::WsMessage::Text(t) => WsMessage::Text(t),
                ws_stream_wasm::WsMessage::Binary(_) => WsMessage::Close,
            }))
        }
    }

    pub async fn close(&mut self) -> Result<(), Report> {
        #[cfg(not(feature = "wasm"))]
        self.inner.close(None).await?;
        #[cfg(feature = "wasm")]
        self.meta
            .close()
            .await
            .map_err(|e| report!("WS close failed: {e:?}"))?;
        Ok(())
    }
}
