//! Wire decoding.

use plexus_core::{
    AddressHash, ContextByte, DestinationType, Error, HeaderType, PacketHeader, PacketType,
    TransportType,
};

const ADDR_LEN: usize = 16;

/// Decode a packet header from the front of `bytes`.
///
/// Returns the parsed header and the number of bytes consumed, so the caller can
/// continue parsing the payload that follows. IFAC-authenticated framing is not
/// parsed in v0: a set IFAC bit is surfaced in the header, but no IFAC body is
/// skipped.
///
/// # Errors
/// [`Error::Truncated`] if `bytes` is shorter than the header it describes.
pub fn decode_header(bytes: &[u8]) -> Result<(PacketHeader, usize), Error> {
    if bytes.len() < 2 {
        return Err(Error::Truncated);
    }
    let flags = bytes[0];
    let hops = bytes[1];

    let ifac = flags & 0b1000_0000 != 0;
    let header_type = if flags & 0b0100_0000 != 0 {
        HeaderType::H2
    } else {
        HeaderType::H1
    };
    let context_flag = flags & 0b0010_0000 != 0;
    let transport_type = if flags & 0b0001_0000 != 0 {
        TransportType::Transport
    } else {
        TransportType::Broadcast
    };
    // Both fields are exhaustive over their 2-bit domains, so no error case.
    let destination_type = match (flags >> 2) & 0b11 {
        0 => DestinationType::Single,
        1 => DestinationType::Group,
        2 => DestinationType::Plain,
        _ => DestinationType::Link,
    };
    let packet_type = match flags & 0b11 {
        0 => PacketType::Data,
        1 => PacketType::Announce,
        2 => PacketType::LinkRequest,
        _ => PacketType::Proof,
    };

    let mut off = 2;
    let transport_id = if header_type == HeaderType::H2 {
        let end = off + ADDR_LEN;
        if bytes.len() < end {
            return Err(Error::Truncated);
        }
        let id = AddressHash::new(
            bytes[off..end]
                .try_into()
                .expect("slice of exactly ADDR_LEN bytes"),
        );
        off = end;
        Some(id)
    } else {
        None
    };

    let end = off + ADDR_LEN;
    if bytes.len() < end + 1 {
        return Err(Error::Truncated);
    }
    let destination = AddressHash::new(
        bytes[off..end]
            .try_into()
            .expect("slice of exactly ADDR_LEN bytes"),
    );
    off = end;
    let context = ContextByte(bytes[off]);
    off += 1;

    Ok((
        PacketHeader {
            ifac,
            header_type,
            context_flag,
            transport_type,
            destination_type,
            packet_type,
            hops,
            transport_id,
            destination,
            context,
        },
        off,
    ))
}

#[cfg(test)]
mod tests {
    use super::decode_header;
    use crate::encode::encode_header;
    use alloc::vec::Vec;
    use plexus_core::{
        AddressHash, ContextByte, DestinationType, HeaderType, PacketHeader, PacketType,
        TransportType, HEADER_SIZE_H1, HEADER_SIZE_H2,
    };

    fn header_h1() -> PacketHeader {
        PacketHeader {
            ifac: false,
            header_type: HeaderType::H1,
            context_flag: false,
            transport_type: TransportType::Broadcast,
            destination_type: DestinationType::Single,
            packet_type: PacketType::Announce,
            hops: 0,
            transport_id: None,
            destination: AddressHash::new([0x11; 16]),
            context: ContextByte::NONE,
        }
    }

    #[test]
    fn known_flags_byte_h1_announce() {
        // ifac=0 hdr=H1 ctx=0 transport=broadcast dst=single pkt=announce → 0x01
        let mut out = Vec::new();
        encode_header(&header_h1(), &mut out).unwrap();
        assert_eq!(out[0], 0x01, "flags byte");
        assert_eq!(out[1], 0x00, "hops");
        assert_eq!(out.len(), HEADER_SIZE_H1);
    }

    #[test]
    fn known_flags_byte_h2_data_link_transport_ctx() {
        // hdr=H2(0x40) ctx=1(0x20) transport=1(0x10) dst=link(0x0C) pkt=data(0) → 0x7C
        let h = PacketHeader {
            ifac: false,
            header_type: HeaderType::H2,
            context_flag: true,
            transport_type: TransportType::Transport,
            destination_type: DestinationType::Link,
            packet_type: PacketType::Data,
            hops: 7,
            transport_id: Some(AddressHash::new([0xAB; 16])),
            destination: AddressHash::new([0xCD; 16]),
            context: ContextByte::KEEPALIVE,
        };
        let mut out = Vec::new();
        encode_header(&h, &mut out).unwrap();
        assert_eq!(out[0], 0x7C, "flags byte");
        assert_eq!(out[1], 7, "hops");
        assert_eq!(out.len(), HEADER_SIZE_H2);
    }

    #[test]
    fn round_trip_h1() {
        let h = header_h1();
        let mut out = Vec::new();
        encode_header(&h, &mut out).unwrap();
        let (decoded, consumed) = decode_header(&out).unwrap();
        assert_eq!(decoded, h);
        assert_eq!(consumed, out.len());
    }

    #[test]
    fn round_trip_h2_all_flags() {
        let h = PacketHeader {
            ifac: true,
            header_type: HeaderType::H2,
            context_flag: true,
            transport_type: TransportType::Transport,
            destination_type: DestinationType::Plain,
            packet_type: PacketType::Proof,
            hops: 42,
            transport_id: Some(AddressHash::new([0x01; 16])),
            destination: AddressHash::new([0x02; 16]),
            context: ContextByte::LRPROOF,
        };
        let mut out = Vec::new();
        encode_header(&h, &mut out).unwrap();
        let (decoded, consumed) = decode_header(&out).unwrap();
        assert_eq!(decoded, h);
        assert_eq!(consumed, out.len());
    }

    #[test]
    fn truncated_is_rejected() {
        assert_eq!(decode_header(&[0x00]), Err(plexus_core::Error::Truncated));
        // H2 flags but only the 2-byte prefix present.
        assert_eq!(
            decode_header(&[0x40, 0x00]),
            Err(plexus_core::Error::Truncated)
        );
    }
}
