//! Network protocol types and frame I/O.
//!
//! Messages are serialized with postcard using length-delimited framing:
//! each frame is a `u32` LE length prefix followed by the postcard payload.
//! The postcard serialization of the enum variant acts as the opcode.

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

/// Protocol version. Client and server must match exactly.
pub const PROTOCOL_VERSION: u32 = 1;

const MAX_FRAME_SIZE: usize = 1024 * 1024;

/// Messages sent from client to server.
#[derive(Serialize, Deserialize, Debug, Clone)]
pub enum ClientMessage {
    Hello { protocol_version: u32 },
    SetName { name: String },
    Chat { content: String },
}

/// Messages sent from server to client.
#[derive(Serialize, Deserialize, Debug, Clone)]
pub enum ServerMessage {
    Welcome,
    Chat { sender: String, content: String },
    System { content: String },
    ProtocolError { reason: String },
}

/// Write a length-delimited postcard frame.
pub async fn write_frame<W: AsyncWrite + Unpin, T: Serialize>(
    writer: &mut W,
    msg: &T,
) -> std::io::Result<()> {
    let bytes =
        postcard::to_allocvec(msg).map_err(|e| std::io::Error::new(std::io::ErrorKind::Other, e))?;
    let len = bytes.len() as u32;
    writer.write_all(&len.to_le_bytes()).await?;
    writer.write_all(&bytes).await?;
    writer.flush().await?;
    Ok(())
}

/// Read a length-delimited postcard frame.
pub async fn read_frame<R: AsyncRead + Unpin, T: for<'de> Deserialize<'de>>(
    reader: &mut R,
) -> std::io::Result<T> {
    let mut len_buf = [0u8; 4];
    reader.read_exact(&mut len_buf).await?;
    let len = u32::from_le_bytes(len_buf) as usize;

    if len > MAX_FRAME_SIZE {
        return Err(std::io::Error::new(
            std::io::ErrorKind::InvalidData,
            "frame too large",
        ));
    }

    let mut buf = vec![0u8; len];
    reader.read_exact(&mut buf).await?;
    postcard::from_bytes(&buf)
        .map_err(|e| std::io::Error::new(std::io::ErrorKind::InvalidData, e))
}
