use std::io;
use std::sync::Arc;

use bytes::{Buf, BufMut, Bytes, BytesMut};
use tokio_util::codec::{Decoder, Encoder};

use crate::types::Envelope;

use super::MAX_PAYLOAD_SIZE;

#[derive(Debug, Clone)]
pub struct EnvelopeFrame {
    pub envelope: Envelope,
    pub raw: Arc<Bytes>,
}

impl EnvelopeFrame {
    pub fn from_envelope(envelope: Envelope) -> Self {
        Self {
            envelope,
            raw: Arc::new(Bytes::new()),
        }
    }
}

#[derive(Debug, Clone, Copy)]
pub struct TransportCodec {
    max_payload_size: u32,
}

impl Default for TransportCodec {
    fn default() -> Self {
        Self {
            max_payload_size: MAX_PAYLOAD_SIZE,
        }
    }
}

impl TransportCodec {
    pub fn new(max_payload_size: u32) -> Self {
        Self {
            max_payload_size: max_payload_size.min(MAX_PAYLOAD_SIZE),
        }
    }
}

impl Decoder for TransportCodec {
    type Item = EnvelopeFrame;
    type Error = io::Error;

    fn decode(&mut self, src: &mut BytesMut) -> Result<Option<Self::Item>, Self::Error> {
        if src.len() < 4 {
            return Ok(None);
        }

        let len = u32::from_be_bytes([src[0], src[1], src[2], src[3]]);
        if len > self.max_payload_size {
            return Err(io::Error::new(
                io::ErrorKind::InvalidData,
                format!(
                    "message too large: {len} bytes (max {})",
                    self.max_payload_size
                ),
            ));
        }

        let frame_len = 4usize + len as usize;
        if src.len() < frame_len {
            return Ok(None);
        }

        src.advance(4);
        let raw = Arc::new(src.split_to(len as usize).freeze());

        let envelope: Envelope = ciborium::from_reader(raw.as_ref().as_ref()).map_err(|e| {
            io::Error::new(
                io::ErrorKind::InvalidData,
                format!("CBOR decode error: {e}"),
            )
        })?;

        Ok(Some(EnvelopeFrame { envelope, raw }))
    }
}

impl Encoder<EnvelopeFrame> for TransportCodec {
    type Error = io::Error;

    fn encode(&mut self, item: EnvelopeFrame, dst: &mut BytesMut) -> Result<(), Self::Error> {
        if !item.raw.is_empty() {
            let len: u32 = item.raw.len().try_into().map_err(|_| {
                io::Error::new(
                    io::ErrorKind::InvalidData,
                    "message too large to length-prefix",
                )
            })?;

            if len > self.max_payload_size {
                return Err(io::Error::new(
                    io::ErrorKind::InvalidData,
                    format!(
                        "message too large: {len} bytes (max {})",
                        self.max_payload_size
                    ),
                ));
            }

            dst.reserve(4 + item.raw.len());
            dst.put_u32(len);
            dst.put_slice(item.raw.as_ref());
            return Ok(());
        }

        let frame_start = dst.len();
        dst.reserve(4);
        dst.put_u32(0);
        let payload_start = dst.len();

        let encode_result = {
            let mut writer = dst.writer();
            ciborium::into_writer(&item.envelope, &mut writer)
        };

        if let Err(err) = encode_result {
            dst.truncate(frame_start);
            return Err(io::Error::other(format!("CBOR encode error: {err}")));
        }

        let payload_len = dst.len().saturating_sub(payload_start);
        let len: u32 = payload_len.try_into().map_err(|_| {
            dst.truncate(frame_start);
            io::Error::new(
                io::ErrorKind::InvalidData,
                "message too large to length-prefix",
            )
        })?;

        if len > self.max_payload_size {
            dst.truncate(frame_start);
            return Err(io::Error::new(
                io::ErrorKind::InvalidData,
                format!(
                    "message too large: {len} bytes (max {})",
                    self.max_payload_size
                ),
            ));
        }

        dst[frame_start..frame_start + 4].copy_from_slice(&len.to_be_bytes());
        Ok(())
    }
}
