// SPDX-FileCopyrightText: 2026 Stan Grams <sjg@haxx.space>
//
// SPDX-License-Identifier: GPL-2.0-or-later

//! Audio streaming protocol types and framing helpers.
//!
//! Wire format: `[1 byte type][4 bytes BE length N][N bytes payload]`

use uuid::Uuid;

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

pub const AUDIO_MSG_STREAM_INFO: u8 = 0x00;
pub const AUDIO_MSG_RX_FRAME: u8 = 0x01;
pub const AUDIO_MSG_TX_FRAME: u8 = 0x02;
pub const AUDIO_MSG_APRS_DECODE: u8 = 0x03;
pub const AUDIO_MSG_CW_DECODE: u8 = 0x04;
pub const AUDIO_MSG_FT8_DECODE: u8 = 0x05;
pub const AUDIO_MSG_WSPR_DECODE: u8 = 0x06;
pub const AUDIO_MSG_AIS_DECODE: u8 = 0x07;
pub const AUDIO_MSG_VDES_DECODE: u8 = 0x08;
pub const AUDIO_MSG_HF_APRS_DECODE: u8 = 0x09;
/// Compressed history blob: payload is a gzip-compressed sequence of normal
/// framed messages (each: `[1 byte type][4 bytes BE length][payload]`).
pub const AUDIO_MSG_HISTORY_COMPRESSED: u8 = 0x0a;

// ---------------------------------------------------------------------------
// Virtual-channel audio multiplexing (server → client)
// ---------------------------------------------------------------------------

/// Per-virtual-channel Opus frame: `[16 B UUID][opus_len B Opus]`.
/// Sent by the server for each virtual channel the client has subscribed to.
pub const AUDIO_MSG_RX_FRAME_CH: u8 = 0x0b;
/// Server → client: virtual channel audio subscription acknowledged.
/// Payload: 16-byte UUID of the newly activated channel slot.
pub const AUDIO_MSG_VCHAN_ALLOCATED: u8 = 0x0c;

// ---------------------------------------------------------------------------
// Virtual-channel audio multiplexing (client → server)
// ---------------------------------------------------------------------------

/// Client → server: create-or-subscribe to a virtual channel's audio.
/// Payload: JSON `{"uuid":"<uuid>","freq_hz":<u64>,"mode":"<mode>"}`.
/// If a channel with the given UUID already exists the server just subscribes;
/// otherwise it creates a new DSP pipeline at the given frequency/mode first.
pub const AUDIO_MSG_VCHAN_SUB: u8 = 0x0d;
/// Client → server: unsubscribe from a virtual channel's audio.
/// Payload: 16-byte UUID of the virtual channel on the server.
pub const AUDIO_MSG_VCHAN_UNSUB: u8 = 0x0e;
/// Client → server: update the dial frequency of a virtual channel.
/// Payload: JSON `{"uuid":"<uuid>","freq_hz":<u64>}`.
pub const AUDIO_MSG_VCHAN_FREQ: u8 = 0x0f;
/// Client → server: update the demodulation mode of a virtual channel.
/// Payload: JSON `{"uuid":"<uuid>","mode":"<mode>"}`.
pub const AUDIO_MSG_VCHAN_MODE: u8 = 0x10;
/// Client → server: remove a virtual channel (stops encoding and destroys the DSP pipeline).
/// Payload: 16-byte UUID of the virtual channel on the server.
pub const AUDIO_MSG_VCHAN_REMOVE: u8 = 0x11;
/// Server → client: a virtual channel was destroyed server-side (e.g. went out of bandwidth).
/// Payload: 16-byte UUID of the destroyed channel.
pub const AUDIO_MSG_VCHAN_DESTROYED: u8 = 0x12;
/// Client → server: update the audio filter bandwidth of an existing virtual channel.
/// Payload: JSON `{"uuid": "<uuid>", "bandwidth_hz": <u32>}`.
pub const AUDIO_MSG_VCHAN_BW: u8 = 0x13;
/// Server → client: FT4 decoded message (JSON `DecodedMessage::Ft4`).
pub const AUDIO_MSG_FT4_DECODE: u8 = 0x14;
/// Server → client: FT2 decoded message (JSON `DecodedMessage::Ft2`).
pub const AUDIO_MSG_FT2_DECODE: u8 = 0x15;
/// Server → client: Meteor-M LRPT image complete (JSON `DecodedMessage::LrptImage`).
pub const AUDIO_MSG_LRPT_IMAGE: u8 = 0x17;
/// Server → client: LRPT decode progress update (JSON `DecodedMessage::LrptProgress`).
pub const AUDIO_MSG_LRPT_PROGRESS: u8 = 0x18;
/// Server → client: WEFAX completed image (JSON `DecodedMessage::Wefax`).
pub const AUDIO_MSG_WEFAX_DECODE: u8 = 0x19;
/// Server → client: WEFAX decode progress (JSON `DecodedMessage::WefaxProgress`).
pub const AUDIO_MSG_WEFAX_PROGRESS: u8 = 0x1A;

/// Maximum payload size for normal messages (1 MB).
const MAX_PAYLOAD_SIZE: u32 = 1_048_576;
/// Maximum payload size for the compressed history blob (16 MB).
/// A compressed 24-hour history on a busy channel can reach several MB.
const MAX_HISTORY_PAYLOAD_SIZE: u32 = 16_777_216;

#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
pub struct AudioStreamInfo {
    pub sample_rate: u32,
    pub channels: u8,
    pub frame_duration_ms: u16,
    #[serde(default, skip_serializing_if = "is_zero_u32")]
    pub bitrate_bps: u32,
}

fn is_zero_u32(v: &u32) -> bool {
    *v == 0
}

/// Write a length-prefixed audio message.
pub async fn write_audio_msg_buffered<W: AsyncWrite + Unpin>(
    writer: &mut W,
    msg_type: u8,
    payload: &[u8],
) -> std::io::Result<()> {
    let len = payload.len() as u32;
    writer.write_u8(msg_type).await?;
    writer.write_u32(len).await?;
    writer.write_all(payload).await?;
    Ok(())
}

/// Write a length-prefixed audio message and flush the writer.
pub async fn write_audio_msg<W: AsyncWrite + Unpin>(
    writer: &mut W,
    msg_type: u8,
    payload: &[u8],
) -> std::io::Result<()> {
    write_audio_msg_buffered(writer, msg_type, payload).await?;
    writer.flush().await?;
    Ok(())
}

/// Read one length-prefixed audio message, returning `(type, payload)`.
pub async fn read_audio_msg<R: AsyncRead + Unpin>(
    reader: &mut R,
) -> std::io::Result<(u8, Vec<u8>)> {
    let msg_type = reader.read_u8().await?;
    let len = reader.read_u32().await?;
    let limit = if msg_type == AUDIO_MSG_HISTORY_COMPRESSED {
        MAX_HISTORY_PAYLOAD_SIZE
    } else {
        MAX_PAYLOAD_SIZE
    };
    if len > limit {
        return Err(std::io::Error::new(
            std::io::ErrorKind::InvalidData,
            format!(
                "audio frame too large: {} bytes (type={:#04x})",
                len, msg_type
            ),
        ));
    }
    let mut payload = vec![0u8; len as usize];
    reader.read_exact(&mut payload).await?;
    Ok((msg_type, payload))
}

// ---------------------------------------------------------------------------
// Virtual-channel frame helpers
// ---------------------------------------------------------------------------

/// Write a virtual-channel control frame (16-byte UUID payload only).
/// Used for `AUDIO_MSG_VCHAN_SUB`, `AUDIO_MSG_VCHAN_UNSUB`, and
/// `AUDIO_MSG_VCHAN_ALLOCATED`.
pub async fn write_vchan_uuid_msg<W: AsyncWrite + Unpin>(
    writer: &mut W,
    msg_type: u8,
    uuid: Uuid,
) -> std::io::Result<()> {
    write_audio_msg(writer, msg_type, uuid.as_bytes()).await
}

/// Write an `AUDIO_MSG_RX_FRAME_CH` frame: 16-byte UUID followed by Opus payload.
pub async fn write_vchan_audio_frame<W: AsyncWrite + Unpin>(
    writer: &mut W,
    uuid: Uuid,
    opus: &[u8],
) -> std::io::Result<()> {
    let mut payload = Vec::with_capacity(16 + opus.len());
    payload.extend_from_slice(uuid.as_bytes());
    payload.extend_from_slice(opus);
    write_audio_msg(writer, AUDIO_MSG_RX_FRAME_CH, &payload).await
}

/// Parse a virtual-channel audio frame payload (`AUDIO_MSG_RX_FRAME_CH`).
/// Returns `(uuid, opus_bytes)` or an error if the payload is too short.
pub fn parse_vchan_audio_frame(payload: &[u8]) -> std::io::Result<(Uuid, &[u8])> {
    if payload.len() < 16 {
        return Err(std::io::Error::new(
            std::io::ErrorKind::InvalidData,
            "vchan audio frame payload too short",
        ));
    }
    let uuid = Uuid::from_bytes(payload[..16].try_into().unwrap());
    Ok((uuid, &payload[16..]))
}

/// Parse a 16-byte UUID control frame (SUB / UNSUB / ALLOCATED).
pub fn parse_vchan_uuid_msg(payload: &[u8]) -> std::io::Result<Uuid> {
    if payload.len() < 16 {
        return Err(std::io::Error::new(
            std::io::ErrorKind::InvalidData,
            "vchan uuid frame payload too short",
        ));
    }
    Ok(Uuid::from_bytes(payload[..16].try_into().unwrap()))
}

#[cfg(test)]
mod tests {
    use super::*;
    use tokio::io::BufReader;

    #[tokio::test]
    async fn write_then_read_round_trip_preserves_type_and_payload() {
        let mut buf: Vec<u8> = Vec::new();
        let payload = b"hello, world";
        write_audio_msg(&mut buf, AUDIO_MSG_FT8_DECODE, payload)
            .await
            .unwrap();
        // Wire bytes: 1 byte type + 4 bytes BE length + payload.
        assert_eq!(buf.len(), 1 + 4 + payload.len());
        assert_eq!(buf[0], AUDIO_MSG_FT8_DECODE);
        assert_eq!(
            u32::from_be_bytes([buf[1], buf[2], buf[3], buf[4]]),
            payload.len() as u32
        );
        assert_eq!(&buf[5..], payload);

        let mut reader = BufReader::new(&buf[..]);
        let (msg_type, got) = read_audio_msg(&mut reader).await.unwrap();
        assert_eq!(msg_type, AUDIO_MSG_FT8_DECODE);
        assert_eq!(got, payload);
    }

    #[tokio::test]
    async fn write_then_read_handles_empty_payload() {
        let mut buf: Vec<u8> = Vec::new();
        write_audio_msg(&mut buf, AUDIO_MSG_RX_FRAME, &[])
            .await
            .unwrap();
        let mut reader = BufReader::new(&buf[..]);
        let (msg_type, got) = read_audio_msg(&mut reader).await.unwrap();
        assert_eq!(msg_type, AUDIO_MSG_RX_FRAME);
        assert!(got.is_empty());
    }

    #[tokio::test]
    async fn read_audio_msg_decodes_consecutive_frames() {
        let mut buf: Vec<u8> = Vec::new();
        write_audio_msg(&mut buf, AUDIO_MSG_FT8_DECODE, b"a")
            .await
            .unwrap();
        write_audio_msg(&mut buf, AUDIO_MSG_FT4_DECODE, b"bb")
            .await
            .unwrap();
        write_audio_msg(&mut buf, AUDIO_MSG_AIS_DECODE, b"ccc")
            .await
            .unwrap();

        let mut reader = BufReader::new(&buf[..]);
        let (t, p) = read_audio_msg(&mut reader).await.unwrap();
        assert_eq!((t, p.as_slice()), (AUDIO_MSG_FT8_DECODE, b"a".as_slice()));
        let (t, p) = read_audio_msg(&mut reader).await.unwrap();
        assert_eq!((t, p.as_slice()), (AUDIO_MSG_FT4_DECODE, b"bb".as_slice()));
        let (t, p) = read_audio_msg(&mut reader).await.unwrap();
        assert_eq!((t, p.as_slice()), (AUDIO_MSG_AIS_DECODE, b"ccc".as_slice()));
    }

    #[tokio::test]
    async fn read_audio_msg_eof_before_header_returns_unexpected_eof() {
        let buf: Vec<u8> = Vec::new();
        let mut reader = BufReader::new(&buf[..]);
        let err = read_audio_msg(&mut reader).await.unwrap_err();
        assert_eq!(err.kind(), std::io::ErrorKind::UnexpectedEof);
    }

    #[tokio::test]
    async fn read_audio_msg_eof_mid_payload_returns_unexpected_eof() {
        // Header claims 16 bytes; only 4 follow.
        let mut buf: Vec<u8> = vec![AUDIO_MSG_FT8_DECODE];
        buf.extend_from_slice(&16u32.to_be_bytes());
        buf.extend_from_slice(&[0xAA; 4]);
        let mut reader = BufReader::new(&buf[..]);
        let err = read_audio_msg(&mut reader).await.unwrap_err();
        assert_eq!(err.kind(), std::io::ErrorKind::UnexpectedEof);
    }

    #[tokio::test]
    async fn read_audio_msg_rejects_oversize_normal_frame() {
        // Type ≠ HISTORY_COMPRESSED → cap is MAX_PAYLOAD_SIZE (1 MiB). Claim
        // 2 MiB and leave the body absent — we should fail before reading.
        let mut buf: Vec<u8> = vec![AUDIO_MSG_FT8_DECODE];
        buf.extend_from_slice(&(2 * 1024 * 1024u32).to_be_bytes());
        let mut reader = BufReader::new(&buf[..]);
        let err = read_audio_msg(&mut reader).await.unwrap_err();
        assert_eq!(err.kind(), std::io::ErrorKind::InvalidData);
    }

    #[tokio::test]
    async fn read_audio_msg_history_frame_allows_larger_payload() {
        // 2 MiB exceeds MAX_PAYLOAD_SIZE but is below MAX_HISTORY_PAYLOAD_SIZE.
        // Reading should succeed when the type is HISTORY_COMPRESSED.
        let payload = vec![0xCDu8; 2 * 1024 * 1024];
        let mut buf: Vec<u8> = Vec::with_capacity(5 + payload.len());
        write_audio_msg(&mut buf, AUDIO_MSG_HISTORY_COMPRESSED, &payload)
            .await
            .unwrap();
        let mut reader = BufReader::new(&buf[..]);
        let (msg_type, got) = read_audio_msg(&mut reader).await.unwrap();
        assert_eq!(msg_type, AUDIO_MSG_HISTORY_COMPRESSED);
        assert_eq!(got.len(), payload.len());
    }

    #[tokio::test]
    async fn read_audio_msg_history_frame_rejects_above_history_cap() {
        // Claim 32 MiB, body absent. Should still fail (cap enforced even for
        // history-compressed type).
        let mut buf: Vec<u8> = vec![AUDIO_MSG_HISTORY_COMPRESSED];
        buf.extend_from_slice(&(32 * 1024 * 1024u32).to_be_bytes());
        let mut reader = BufReader::new(&buf[..]);
        let err = read_audio_msg(&mut reader).await.unwrap_err();
        assert_eq!(err.kind(), std::io::ErrorKind::InvalidData);
    }

    #[tokio::test]
    async fn vchan_uuid_msg_round_trip() {
        let uuid = Uuid::new_v4();
        let mut buf: Vec<u8> = Vec::new();
        write_vchan_uuid_msg(&mut buf, AUDIO_MSG_VCHAN_SUB, uuid)
            .await
            .unwrap();
        let mut reader = BufReader::new(&buf[..]);
        let (msg_type, payload) = read_audio_msg(&mut reader).await.unwrap();
        assert_eq!(msg_type, AUDIO_MSG_VCHAN_SUB);
        let got = parse_vchan_uuid_msg(&payload).unwrap();
        assert_eq!(got, uuid);
    }

    #[test]
    fn parse_vchan_uuid_msg_rejects_short_payload() {
        let err = parse_vchan_uuid_msg(&[0u8; 8]).unwrap_err();
        assert_eq!(err.kind(), std::io::ErrorKind::InvalidData);
        // Empty payload also rejected.
        let err = parse_vchan_uuid_msg(&[]).unwrap_err();
        assert_eq!(err.kind(), std::io::ErrorKind::InvalidData);
    }

    #[tokio::test]
    async fn vchan_audio_frame_round_trip() {
        let uuid = Uuid::new_v4();
        let opus = b"\x80\x81\x82 fake opus payload";
        let mut buf: Vec<u8> = Vec::new();
        write_vchan_audio_frame(&mut buf, uuid, opus).await.unwrap();
        let mut reader = BufReader::new(&buf[..]);
        let (msg_type, payload) = read_audio_msg(&mut reader).await.unwrap();
        assert_eq!(msg_type, AUDIO_MSG_RX_FRAME_CH);
        let (got_uuid, got_opus) = parse_vchan_audio_frame(&payload).unwrap();
        assert_eq!(got_uuid, uuid);
        assert_eq!(got_opus, opus);
    }

    #[test]
    fn parse_vchan_audio_frame_rejects_short_payload() {
        let err = parse_vchan_audio_frame(&[0u8; 8]).unwrap_err();
        assert_eq!(err.kind(), std::io::ErrorKind::InvalidData);
    }

    #[test]
    fn parse_vchan_audio_frame_handles_empty_opus() {
        // Exactly 16 bytes: UUID with no opus payload.
        let uuid = Uuid::new_v4();
        let payload = uuid.as_bytes().to_vec();
        let (got_uuid, got_opus) = parse_vchan_audio_frame(&payload).unwrap();
        assert_eq!(got_uuid, uuid);
        assert!(got_opus.is_empty());
    }

    #[tokio::test]
    async fn audio_stream_info_serialises_round_trip() {
        let info = AudioStreamInfo {
            sample_rate: 48_000,
            channels: 2,
            frame_duration_ms: 20,
            bitrate_bps: 64_000,
        };
        let json = serde_json::to_vec(&info).unwrap();
        let mut buf: Vec<u8> = Vec::new();
        write_audio_msg(&mut buf, AUDIO_MSG_STREAM_INFO, &json)
            .await
            .unwrap();
        let mut reader = BufReader::new(&buf[..]);
        let (msg_type, payload) = read_audio_msg(&mut reader).await.unwrap();
        assert_eq!(msg_type, AUDIO_MSG_STREAM_INFO);
        let parsed: AudioStreamInfo = serde_json::from_slice(&payload).unwrap();
        assert_eq!(parsed.sample_rate, info.sample_rate);
        assert_eq!(parsed.channels, info.channels);
        assert_eq!(parsed.frame_duration_ms, info.frame_duration_ms);
        assert_eq!(parsed.bitrate_bps, info.bitrate_bps);
    }

    #[tokio::test]
    async fn write_audio_msg_buffered_does_not_flush() {
        // Smoke test that the buffered variant produces equivalent bytes to
        // the flushed variant — Vec<u8> doesn't actually "buffer" so this is
        // mostly a behavioural check that no extra padding/headers slip in.
        let mut a: Vec<u8> = Vec::new();
        let mut b: Vec<u8> = Vec::new();
        write_audio_msg_buffered(&mut a, AUDIO_MSG_FT8_DECODE, b"x")
            .await
            .unwrap();
        write_audio_msg(&mut b, AUDIO_MSG_FT8_DECODE, b"x")
            .await
            .unwrap();
        assert_eq!(a, b);
    }
}
