use super::Transport;
use crate::connection::CmServer;
use crate::connection::CmServerAddr;
use crate::connection::framing;
use crate::error::ConnectionError;
use crate::error::Error;
use bytes::Bytes;
use std::net::SocketAddr;
use std::pin::Pin;
use tokio::io::AsyncReadExt;
use tokio::io::AsyncWriteExt;
use tokio::net::TcpStream;
use tokio::sync::Mutex;

pub struct TcpTransport {
    reader: Mutex<tokio::io::ReadHalf<TcpStream>>,
    writer: Mutex<tokio::io::WriteHalf<TcpStream>>,
}

impl TcpTransport {
    pub async fn connect(server: &CmServer) -> Result<Self, Error> {
        let addr = match &server.addr {
            CmServerAddr::Resolved(addr) => *addr,
            CmServerAddr::Dns { host, port } => {
                let addrs: Vec<SocketAddr> = tokio::net::lookup_host(format!("{host}:{port}"))
                    .await
                    .map_err(|_| ConnectionError::DnsResolutionFailed)?
                    .collect();
                *addrs.first().ok_or(ConnectionError::DnsResolutionFailed)?
            }
        };

        let stream = TcpStream::connect(addr)
            .await
            .map_err(ConnectionError::Io)?;

        let (reader, writer) = tokio::io::split(stream);
        Ok(Self {
            reader: Mutex::new(reader),
            writer: Mutex::new(writer),
        })
    }
}

impl Transport for TcpTransport {
    fn send(
        &self,
        payload: &[u8],
    ) -> Pin<Box<dyn std::future::Future<Output = Result<(), Error>> + Send + '_>> {
        let payload_len = payload.len();
        let frame = framing::Frame::encode(payload);
        Box::pin(async move {
            tracing::debug!(
                "tcp send: {} bytes payload, {} bytes frame",
                payload_len,
                frame.len()
            );
            let mut writer = self.writer.lock().await;
            writer
                .write_all(&frame)
                .await
                .map_err(ConnectionError::Io)?;
            writer.flush().await.map_err(ConnectionError::Io)?;
            Ok(())
        })
    }

    fn recv(&self) -> Pin<Box<dyn std::future::Future<Output = Result<Bytes, Error>> + Send + '_>> {
        Box::pin(async move {
            let mut reader = self.reader.lock().await;
            let mut header = [0u8; 8];
            tracing::trace!("tcp recv: waiting for header...");
            reader.read_exact(&mut header).await.map_err(|e| {
                tracing::error!("tcp recv header failed: {e}");
                ConnectionError::Disconnected
            })?;
            tracing::trace!("tcp recv: got header {:02x?}", header);
            let length = u32::from_le_bytes(header[..4].try_into().unwrap());
            let magic = &header[4..8];
            if magic != framing::MAGIC {
                return Err(ConnectionError::BadMagic(u32::from_le_bytes(
                    magic.try_into().unwrap(),
                ))
                .into());
            }
            let mut payload = vec![0u8; length as usize];
            reader
                .read_exact(&mut payload)
                .await
                .map_err(|_| ConnectionError::Disconnected)?;
            Ok(Bytes::from(payload))
        })
    }
}
