use std::fmt;

pub const HEADER_LEN: usize = 8;
pub const SERIAL_QUERY_LEN: usize = 12;
pub const MAX_PDU_LEN: u32 = 65_535;

pub const IPV4_PREFIX_BODY_LEN: usize = 12;
pub const IPV6_PREFIX_BODY_LEN: usize = 24;
pub const END_OF_DATA_V0_BODY_LEN: usize = 4;
pub const END_OF_DATA_V1_BODY_LEN: usize = 16;
pub const ROUTER_KEY_FIXED_BODY_LEN: usize = 24;
pub const ASPA_FIXED_BODY_LEN: usize = 4;

#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum QueryMode {
    Reset,
    Serial { session_id: u16, serial: u32 },
}

#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum PduType {
    SerialNotify,
    SerialQuery,
    ResetQuery,
    CacheResponse,
    Ipv4Prefix,
    Ipv6Prefix,
    RouterKey,
    EndOfData,
    CacheReset,
    ErrorReport,
    Aspa,
    Unknown(u8),
}

impl PduType {
    pub fn code(self) -> u8 {
        match self {
            Self::SerialNotify => 0,
            Self::SerialQuery => 1,
            Self::ResetQuery => 2,
            Self::CacheResponse => 3,
            Self::Ipv4Prefix => 4,
            Self::Ipv6Prefix => 6,
            Self::RouterKey => 9,
            Self::EndOfData => 7,
            Self::CacheReset => 8,
            Self::ErrorReport => 10,
            Self::Aspa => 11,
            Self::Unknown(v) => v,
        }
    }

    pub fn name(self) -> &'static str {
        match self {
            Self::SerialNotify => "Serial Notify",
            Self::SerialQuery => "Serial Query",
            Self::ResetQuery => "Reset Query",
            Self::CacheResponse => "Cache Response",
            Self::Ipv4Prefix => "IPv4 Prefix",
            Self::Ipv6Prefix => "IPv6 Prefix",
            Self::RouterKey => "Router Key",
            Self::EndOfData => "End of Data",
            Self::CacheReset => "Cache Reset",
            Self::ErrorReport => "Error Report",
            Self::Aspa => "ASPA",
            Self::Unknown(_) => "Unknown",
        }
    }
}

impl From<u8> for PduType {
    fn from(value: u8) -> Self {
        match value {
            0 => Self::SerialNotify,
            1 => Self::SerialQuery,
            2 => Self::ResetQuery,
            3 => Self::CacheResponse,
            4 => Self::Ipv4Prefix,
            6 => Self::Ipv6Prefix,
            9 => Self::RouterKey,
            7 => Self::EndOfData,
            8 => Self::CacheReset,
            10 => Self::ErrorReport,
            11 => Self::Aspa,
            x => Self::Unknown(x),
        }
    }
}

impl fmt::Display for PduType {
    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
        match self {
            Self::Unknown(v) => write!(f, "{} ({})", self.name(), v),
            _ => write!(f, "{} ({})", self.name(), self.code()),
        }
    }
}

#[derive(Debug, Clone)]
pub struct PduHeader {
    pub version: u8,
    pub pdu_type_raw: u8,
    pub field1: u16,
    pub length: u32,
}

impl PduHeader {
    pub fn from_bytes(buf: [u8; HEADER_LEN]) -> Self {
        Self {
            version: buf[0],
            pdu_type_raw: buf[1],
            field1: u16::from_be_bytes([buf[2], buf[3]]),
            length: u32::from_be_bytes([buf[4], buf[5], buf[6], buf[7]]),
        }
    }

    pub fn pdu_type(&self) -> PduType {
        self.pdu_type_raw.into()
    }

    pub fn session_id(&self) -> u16 {
        self.field1
    }

    pub fn error_code(&self) -> u16 {
        self.field1
    }

    pub fn flags(&self) -> u8 {
        self.field1.to_be_bytes()[0]
    }

    pub fn zero(&self) -> u8 {
        self.field1.to_be_bytes()[1]
    }
}

#[derive(Debug, Clone)]
pub struct RawPdu {
    pub header: PduHeader,
    pub body: Vec<u8>,
}

pub fn flag_meaning(flags: u8) -> &'static str {
    if flags & 0x01 == 0x01 {
        "announcement"
    } else {
        "withdrawal"
    }
}

pub fn hex_bytes(data: &[u8]) -> String {
    if data.is_empty() {
        return "<empty>".to_string();
    }

    let mut out = String::with_capacity(data.len() * 3 - 1);
    for (idx, b) in data.iter().enumerate() {
        if idx > 0 {
            out.push(' ');
        }
        use std::fmt::Write as _;
        let _ = write!(out, "{:02x}", b);
    }
    out
}
