use std::io;

use tokio::io::{AsyncRead, AsyncReadExt, AsyncWrite, AsyncWriteExt};

use crate::protocol::{HEADER_LEN, MAX_PDU_LEN, PduHeader, PduType, RawPdu, SERIAL_QUERY_LEN};

pub async fn send_reset_query<S>(stream: &mut S, version: u8) -> io::Result<()>
where
    S: AsyncWrite + Unpin,
{
    let mut buf = [0u8; HEADER_LEN];
    buf[0] = version;
    buf[1] = PduType::ResetQuery.code();
    buf[2..4].copy_from_slice(&0u16.to_be_bytes());
    buf[4..8].copy_from_slice(&(HEADER_LEN as u32).to_be_bytes());
    stream.write_all(&buf).await?;
    stream.flush().await
}

pub async fn send_serial_query<S>(
    stream: &mut S,
    version: u8,
    session_id: u16,
    serial: u32,
) -> io::Result<()>
where
    S: AsyncWrite + Unpin,
{
    let mut buf = [0u8; SERIAL_QUERY_LEN];
    buf[0] = version;
    buf[1] = PduType::SerialQuery.code();
    buf[2..4].copy_from_slice(&session_id.to_be_bytes());
    buf[4..8].copy_from_slice(&(SERIAL_QUERY_LEN as u32).to_be_bytes());
    buf[8..12].copy_from_slice(&serial.to_be_bytes());
    stream.write_all(&buf).await?;
    stream.flush().await
}

pub async fn read_header<S>(stream: &mut S) -> io::Result<PduHeader>
where
    S: AsyncRead + Unpin,
{
    let mut buf = [0u8; HEADER_LEN];
    stream.read_exact(&mut buf).await?;
    Ok(PduHeader::from_bytes(buf))
}

pub async fn read_pdu<S>(stream: &mut S) -> io::Result<RawPdu>
where
    S: AsyncRead + Unpin,
{
    let header = read_header(stream).await?;

    if header.length < HEADER_LEN as u32 {
        return Err(io::Error::new(
            io::ErrorKind::InvalidData,
            format!("invalid PDU length {} < {}", header.length, HEADER_LEN),
        ));
    }

    if header.length > MAX_PDU_LEN {
        return Err(io::Error::new(
            io::ErrorKind::InvalidData,
            format!(
                "PDU length {} exceeds max allowed {}",
                header.length, MAX_PDU_LEN
            ),
        ));
    }

    let body_len = header.length as usize - HEADER_LEN;
    let mut body = vec![0u8; body_len];
    stream.read_exact(&mut body).await?;

    Ok(RawPdu { header, body })
}
