use crate::io::{ReadCursor, Readable, UserProperty, VariableByteInteger, Writable, WriteCursor};
use crate::types::*;
use pyo3::exceptions::PyValueError;
use pyo3::prelude::*;
use pyo3::types::{PyBytes, PyList, PyString};
use pyo3::PyResult;

const PROTOCOL_NAME: &str = "MQTT";
const PROTOCOL_VERSION: u8 = 5;

macro_rules! read_properties {
    ($packet_name:literal, $cursor:expr, {
        $($property_type:path => $field:ident: $field_type:tt = $default:expr),* $(,)?
    }) => {
        $(
            #[allow(unused_mut)]
            let mut $field = $default;
        )*
        if $cursor.index < $cursor.buffer.len() {
            let properties_remaining_length = VariableByteInteger::read($cursor)?;
            let properties_start_index = $cursor.index;
            let mut seen = 0u64;
            while $cursor.index - properties_start_index < properties_remaining_length.value() as usize {
                let property_type = PropertyType::new(u8::read($cursor)?)?;
                // Check for duplicates
                if property_type != PropertyType::UserProperty
                    && !(property_type == PropertyType::SubscriptionId && $packet_name == "PublishPacket") {
                    let bit = 1u64 << (property_type as u8);
                    if seen & bit != 0 {
                        return Err(PyValueError::new_err(format!(
                            "Duplicate value for unique property type: {:?}", property_type
                        )));
                    }
                    seen |= bit;
                }
                match property_type {
                    $(
                        $property_type => {
                            read_properties!(@read, $cursor, $field, $field_type);
                        }
                    )*
                    _ => {
                        return Err(PyValueError::new_err(format!(
                            "Invalid property type for {}: {:?}", $packet_name, property_type
                        )));
                    }
                }
            }
        // Property length is optional only for packets that cannot have a payload
        } else if !matches!($packet_name,
            "PubAckPacket" | "PubRecPacket" | "PubRelPacket" | "PubCompPacket" | "DisconnectPacket"
        ) {
            return Err(PyValueError::new_err(format!(
                "Property length is required for {}", $packet_name
            )));
        }
    };

    (@read, $cursor:expr, $field:ident, (Py<PyList<$inner:ty>>)) => {
        $field.append(<$inner>::read($cursor)?)?
    };
    (@read, $cursor:expr, $field:ident, (Option<$inner:ty>)) => {
        $field = Some(<$inner>::read($cursor)?)
    };
    (@read, $cursor:expr, $field:ident, $field_type:ty) => {
        $field = <$field_type>::read($cursor)?
    };
}

macro_rules! write_properties {
    ($cursor:expr, $context:expr, {
        $($property_type:path => $field:ident: $field_type:tt = $default:expr),* $(,)?
    }) => {
        $(
            write_properties!(@write, $cursor, $context, $property_type, $field, $field_type, $default);
        )*
    };

    (@write, $cursor:expr, $context:expr, $property_type:path, $field:ident, (Py<PyList<$inner:ty>>), $default:expr) => {
        Python::attach(|py| {
            if !$context.$field.bind(py).is_empty() {
                for item in $context.$field.bind(py).iter() {
                    ($property_type as u8).write($cursor);
                    let value: $inner = item.extract().unwrap();
                    value.write($cursor);
                }
            }
        });
    };
    (@write, $cursor:expr, $context:expr, $property_type:path, $field:ident, (Option<$inner:ty>), $default:expr) => {
        if $context.$field.is_some() {
            ($property_type as u8).write($cursor);
            $context.$field.write($cursor);
        }
    };
    (@write, $cursor:expr, $context:expr, $property_type:path, $field:ident, $field_type:tt, $default:expr) => {
        if $context.$field != $default {
            ($property_type as u8).write($cursor);
            $context.$field.write($cursor);
        }
    };
}

macro_rules! nbytes_properties {
    ($context: expr, {
        $($property_type:path => $field:ident: $field_type:tt = $default:expr),* $(,)?
    }) => {
        {
            let mut accumulator: usize = 0;
            $(
                nbytes_properties!(@nbytes, $context, accumulator, $field, $field_type, $default);
            )*
            accumulator
        }
    };

    (@nbytes, $context:expr, $accumulator:expr, $field:ident, (Py<PyList<$inner:ty>>), $default:expr) => {
        Python::attach(|py| {
            if !$context.$field.bind(py).is_empty() {
                for item in $context.$field.bind(py).iter() {
                    let value: $inner = item.extract().unwrap();
                    $accumulator += 0u8.nbytes() + value.nbytes();
                }
            }
        });
    };
    (@nbytes, $context:expr, $accumulator:expr, $field:ident, (Option<$inner:ty>), $default:expr) => {
        if $context.$field.is_some() {
            $accumulator += 0u8.nbytes() + $context.$field.nbytes()
        }
    };
    (@nbytes, $context:expr, $accumulator:expr, $field:ident, $field_type:tt, $default:expr) => {
        if $context.$field != $default {
            $accumulator += 0u8.nbytes() + $context.$field.nbytes()
        }
    };
}

#[pyclass(frozen, eq, get_all, from_py_object, module = "mqtt5")]
pub struct Will {
    pub topic: Py<PyString>,
    pub payload: Option<Py<PyBytes>>,
    pub qos: QoS,
    pub retain: bool,
    pub payload_format_indicator: u8,
    pub message_expiry_interval: Option<u32>,
    pub content_type: Option<Py<PyString>>,
    pub response_topic: Option<Py<PyString>>,
    pub correlation_data: Option<Py<PyBytes>>,
    pub will_delay_interval: u32,
    pub user_properties: Py<PyList>,
}

#[pymethods]
impl Will {
    #[new]
    #[pyo3(signature = (
        topic,
        *,
        payload=None,
        qos=QoS::AtMostOnce,
        retain=false,
        payload_format_indicator=0,
        message_expiry_interval=None,
        content_type=None,
        response_topic=None,
        correlation_data=None,
        will_delay_interval=0,
        user_properties=None,
    ))]
    pub fn new(
        topic: Py<PyString>,
        payload: Option<Py<PyBytes>>,
        qos: QoS,
        retain: bool,
        payload_format_indicator: u8,
        message_expiry_interval: Option<u32>,
        content_type: Option<Py<PyString>>,
        response_topic: Option<Py<PyString>>,
        correlation_data: Option<Py<PyBytes>>,
        will_delay_interval: u32,
        user_properties: Option<Py<PyList>>,
    ) -> Self {
        Self {
            topic,
            payload,
            qos,
            retain,
            payload_format_indicator,
            message_expiry_interval,
            content_type,
            response_topic,
            correlation_data,
            will_delay_interval,
            user_properties: user_properties
                .unwrap_or_else(|| Python::attach(|py| PyList::empty(py).unbind())),
        }
    }
}

impl Clone for Will {
    fn clone(&self) -> Self {
        Python::attach(|py| Self {
            topic: self.topic.clone_ref(py),
            payload: self.payload.as_ref().map(|x| x.clone_ref(py)),
            qos: self.qos,
            retain: self.retain,
            payload_format_indicator: self.payload_format_indicator,
            message_expiry_interval: self.message_expiry_interval,
            content_type: self.content_type.as_ref().map(|x| x.clone_ref(py)),
            response_topic: self.response_topic.as_ref().map(|x| x.clone_ref(py)),
            correlation_data: self.correlation_data.as_ref().map(|x| x.clone_ref(py)),
            will_delay_interval: self.will_delay_interval,
            user_properties: self.user_properties.clone_ref(py),
        })
    }
}

impl PartialEq for Will {
    fn eq(&self, other: &Self) -> bool {
        self.topic.py_eq(&other.topic)
            && self.payload.py_eq(&other.payload)
            && self.qos == other.qos
            && self.retain == other.retain
            && self.payload_format_indicator == other.payload_format_indicator
            && self.message_expiry_interval == other.message_expiry_interval
            && self.content_type.py_eq(&other.content_type)
            && self.response_topic.py_eq(&other.response_topic)
            && self.correlation_data.py_eq(&other.correlation_data)
            && self.will_delay_interval == other.will_delay_interval
            && self.user_properties.py_eq(&other.user_properties)
    }
}

#[pyclass(frozen, eq, get_all, module = "mqtt5")]
pub struct Subscription {
    pub pattern: Py<PyString>,
    pub max_qos: QoS,
    pub no_local: bool,
    pub retain_as_published: bool,
    pub retain_handling: RetainHandling,
}

#[pymethods]
impl Subscription {
    #[new]
    #[pyo3(signature = (
        pattern,
        *,
        max_qos=QoS::ExactlyOnce,
        no_local=false,
        retain_as_published=true,
        retain_handling=RetainHandling::SendAlways,
    ))]
    pub fn new(
        pattern: Py<PyString>,
        max_qos: QoS,
        no_local: bool,
        retain_as_published: bool,
        retain_handling: RetainHandling,
    ) -> Self {
        Self {
            pattern,
            no_local,
            max_qos,
            retain_as_published,
            retain_handling,
        }
    }
}

impl PartialEq for Subscription {
    fn eq(&self, other: &Self) -> bool {
        self.pattern.py_eq(&other.pattern)
            && self.max_qos == other.max_qos
            && self.no_local == other.no_local
            && self.retain_as_published == other.retain_as_published
            && self.retain_handling == other.retain_handling
    }
}

#[pyclass(frozen, eq, get_all, module = "mqtt5")]
pub struct ConnectPacket {
    pub client_id: Py<PyString>,
    pub username: Option<Py<PyString>>,
    pub password: Option<Py<PyString>>,
    pub clean_start: bool,
    pub will: Option<Will>,
    pub keep_alive: u16,
    pub session_expiry_interval: u32,
    pub authentication_method: Option<Py<PyString>>,
    pub authentication_data: Option<Py<PyBytes>>,
    pub request_problem_info: bool,
    pub request_response_info: bool,
    pub receive_max: u16,
    pub topic_alias_max: u16,
    pub max_packet_size: Option<u32>,
    pub user_properties: Py<PyList>,
}

#[pymethods]
impl ConnectPacket {
    #[new]
    #[pyo3(signature = (
        client_id,
        *,
        username=None,
        password=None,
        clean_start=false,
        will=None,
        keep_alive=0,
        session_expiry_interval=0,
        authentication_method=None,
        authentication_data=None,
        request_problem_info=true,
        request_response_info=false,
        receive_max=65535,
        topic_alias_max=0,
        max_packet_size=None,
        user_properties=None,
    ))]
    pub fn new(
        client_id: Py<PyString>,
        username: Option<Py<PyString>>,
        password: Option<Py<PyString>>,
        clean_start: bool,
        will: Option<Will>,
        keep_alive: u16,
        session_expiry_interval: u32,
        authentication_method: Option<Py<PyString>>,
        authentication_data: Option<Py<PyBytes>>,
        request_problem_info: bool,
        request_response_info: bool,
        receive_max: u16,
        topic_alias_max: u16,
        max_packet_size: Option<u32>,
        user_properties: Option<Py<PyList>>,
    ) -> PyResult<Self> {
        Ok(Self {
            client_id,
            username,
            password,
            clean_start,
            will,
            keep_alive,
            session_expiry_interval,
            authentication_method,
            authentication_data,
            request_problem_info,
            request_response_info,
            receive_max,
            topic_alias_max,
            max_packet_size,
            user_properties: user_properties
                .unwrap_or_else(|| Python::attach(|py| PyList::empty(py).unbind())),
        })
    }

    pub fn write(&self, py: Python) -> PyResult<Py<PyBytes>> {
        let properties_nbytes = nbytes_properties!(self, {
            PropertyType::SessionExpiryInterval => session_expiry_interval: u32 = 0,
            PropertyType::AuthenticationMethod => authentication_method: (Option<Py<PyString>>) = None,
            PropertyType::AuthenticationData => authentication_data: (Option<Py<PyBytes>>) = None,
            PropertyType::RequestProblemInfo => request_problem_info: bool = true,
            PropertyType::RequestResponseInfo => request_response_info: bool = false,
            PropertyType::ReceiveMax => receive_max: u16 = 65535,
            PropertyType::TopicAliasMax => topic_alias_max: u16 = 0,
            PropertyType::MaxPacketSize => max_packet_size: (Option<u32>) = None,
            PropertyType::UserProperty => user_properties: (Py<PyList<UserProperty>>) = PyList::empty(py),
        });
        let properties_remaining_length = VariableByteInteger::new(properties_nbytes as u32);
        let will_properties_nbytes = self
            .will
            .as_ref()
            .map_or(0, |will| nbytes_properties!(will, {
                PropertyType::PayloadFormatIndicator => payload_format_indicator: u8 = 0,
                PropertyType::MessageExpiryInterval => message_expiry_interval: (Option<u32>) = None,
                PropertyType::ContentType => content_type: (Option<Py<PyString>>) = None,
                PropertyType::ResponseTopic => response_topic: (Option<Py<PyString>>) = None,
                PropertyType::CorrelationData => correlation_data: (Option<Py<PyBytes>>) = None,
                PropertyType::WillDelayInterval => will_delay_interval: u32 = 0,
                PropertyType::UserProperty => user_properties: (Py<PyList<UserProperty>>) = PyList::empty(py),
            }));
        let will_properties_remaining_length =
            VariableByteInteger::new(will_properties_nbytes as u32);
        let nbytes = PROTOCOL_NAME.nbytes()
            + PROTOCOL_VERSION.nbytes()
            + 0u8.nbytes()
            + self.keep_alive.nbytes()
            + properties_remaining_length.nbytes()
            + properties_nbytes
            + self.client_id.nbytes()
            + self.will.as_ref().map_or(0, |will| {
                will_properties_remaining_length.nbytes()
                    + will_properties_nbytes
                    + will.topic.nbytes()
                    + will.payload.as_ref().map_or(0u16.nbytes(), |x| x.nbytes())
            })
            + self.username.nbytes()
            + self.password.nbytes();
        let remaining_length = VariableByteInteger::new(nbytes as u32);
        PyBytes::new_with(py, 1 + remaining_length.nbytes() + nbytes, |buffer| {
            let mut cursor = WriteCursor::new(buffer, 0);

            // [3.1.1] Fixed header
            let first_byte = (PacketType::Connect as u8) << 4;
            first_byte.write(&mut cursor);
            remaining_length.write(&mut cursor);

            // [3.1.2] Variable header
            PROTOCOL_NAME.write(&mut cursor);
            PROTOCOL_VERSION.write(&mut cursor);
            let mut packet_flags = (self.clean_start as u8) << 1;
            if let Some(ref will) = self.will {
                packet_flags |= 0x04;
                packet_flags |= (will.qos as u8) << 3;
                packet_flags |= (will.retain as u8) << 5;
            }
            if self.password.is_some() {
                packet_flags |= 0x40;
            }
            if self.username.is_some() {
                packet_flags |= 0x80;
            }
            packet_flags.write(&mut cursor);
            self.keep_alive.write(&mut cursor);
            properties_remaining_length.write(&mut cursor);
            write_properties!(&mut cursor, self, {
                PropertyType::SessionExpiryInterval => session_expiry_interval: u32 = 0,
                PropertyType::AuthenticationMethod => authentication_method: (Option<Py<PyString>>) = None,
                PropertyType::AuthenticationData => authentication_data: (Option<Py<PyBytes>>) = None,
                PropertyType::RequestProblemInfo => request_problem_info: bool = true,
                PropertyType::RequestResponseInfo => request_response_info: bool = false,
                PropertyType::ReceiveMax => receive_max: u16 = 65535,
                PropertyType::TopicAliasMax => topic_alias_max: u16 = 0,
                PropertyType::MaxPacketSize => max_packet_size: (Option<u32>) = None,
                PropertyType::UserProperty => user_properties: (Py<PyList<UserProperty>>) = PyList::empty(py),
            });

            // [3.1.3] Payload
            self.client_id.write(&mut cursor);
            if let Some(ref will) = self.will {
                will_properties_remaining_length.write(&mut cursor);
                write_properties!(&mut cursor, will, {
                    PropertyType::PayloadFormatIndicator => payload_format_indicator: u8 = 0,
                    PropertyType::MessageExpiryInterval => message_expiry_interval: (Option<u32>) = None,
                    PropertyType::ContentType => content_type: (Option<Py<PyString>>) = None,
                    PropertyType::ResponseTopic => response_topic: (Option<Py<PyString>>) = None,
                    PropertyType::CorrelationData => correlation_data: (Option<Py<PyBytes>>) = None,
                    PropertyType::WillDelayInterval => will_delay_interval: u32 = 0,
                    PropertyType::UserProperty => user_properties: (Py<PyList<UserProperty>>) = PyList::empty(py),
                });
                will.topic.write(&mut cursor);
                if let Some(ref payload) = will.payload {
                    payload.write(&mut cursor);
                } else {
                    0u16.write(&mut cursor);
                }
            }
            self.username.write(&mut cursor);
            self.password.write(&mut cursor);

            Ok(())
        })
        .map(|bytes| bytes.unbind())
    }
}

impl ConnectPacket {
    pub fn read(py: Python, cursor: &mut ReadCursor, flags: u8) -> PyResult<Py<Self>> {
        if flags != 0x00 {
            return Err(PyValueError::new_err("Malformed bytes"));
        }

        // [3.1.2] Variable header
        if String::read(cursor)? != PROTOCOL_NAME {
            return Err(PyValueError::new_err("Malformed bytes"));
        }
        if u8::read(cursor)? != PROTOCOL_VERSION {
            return Err(PyValueError::new_err("Malformed bytes"));
        }
        let packet_flags = u8::read(cursor)?;
        let clean_start = (packet_flags & 0x02) != 0;
        let keep_alive = u16::read(cursor)?;
        read_properties!("ConnectPacket", cursor, {
            PropertyType::SessionExpiryInterval => session_expiry_interval: u32 = 0,
            PropertyType::AuthenticationMethod => authentication_method: (Option<Py<PyString>>) = None,
            PropertyType::AuthenticationData => authentication_data: (Option<Py<PyBytes>>) = None,
            PropertyType::RequestProblemInfo => request_problem_info: bool = true,
            PropertyType::RequestResponseInfo => request_response_info: bool = false,
            PropertyType::ReceiveMax => receive_max: u16 = 65535,
            PropertyType::TopicAliasMax => topic_alias_max: u16 = 0,
            PropertyType::MaxPacketSize => max_packet_size: (Option<u32>) = None,
            PropertyType::UserProperty => user_properties: (Py<PyList<UserProperty>>) = PyList::empty(py),
        });

        // [3.1.3] Payload
        let client_id = Py::<PyString>::read(cursor)?;
        let will = if (packet_flags & 0x04) != 0 {
            read_properties!("Will", cursor, {
                PropertyType::PayloadFormatIndicator => payload_format_indicator: u8 = 0,
                PropertyType::MessageExpiryInterval => message_expiry_interval: (Option<u32>) = None,
                PropertyType::ContentType => content_type: (Option<Py<PyString>>) = None,
                PropertyType::ResponseTopic => response_topic: (Option<Py<PyString>>) = None,
                PropertyType::CorrelationData => correlation_data: (Option<Py<PyBytes>>) = None,
                PropertyType::WillDelayInterval => will_delay_interval: u32 = 0,
                PropertyType::UserProperty => user_properties: (Py<PyList<UserProperty>>) = PyList::empty(py),
            });
            let topic = Py::<PyString>::read(cursor)?;
            let payload = Py::<PyBytes>::read(cursor)?;
            Some(Will {
                topic,
                payload: Some(payload),
                qos: QoS::new((packet_flags >> 3) & 0x03)?,
                retain: (packet_flags & 0x20) != 0,
                payload_format_indicator,
                message_expiry_interval,
                content_type,
                response_topic,
                correlation_data,
                will_delay_interval,
                user_properties: user_properties.unbind(),
            })
        } else {
            None
        };
        let username = if (packet_flags & 0x80) != 0 {
            Some(Py::<PyString>::read(cursor)?)
        } else {
            None
        };
        let password = if (packet_flags & 0x40) != 0 {
            Some(Py::<PyString>::read(cursor)?)
        } else {
            None
        };

        // Return the Python object
        let packet = Self {
            client_id,
            username,
            password,
            clean_start,
            will,
            keep_alive,
            session_expiry_interval,
            authentication_method,
            authentication_data,
            request_problem_info,
            request_response_info,
            receive_max,
            topic_alias_max,
            max_packet_size,
            user_properties: user_properties.unbind(),
        };
        Py::new(py, packet)
    }
}

impl PartialEq for ConnectPacket {
    fn eq(&self, other: &Self) -> bool {
        self.client_id.py_eq(&other.client_id)
            && self.username.py_eq(&other.username)
            && self.password.py_eq(&other.password)
            && self.clean_start == other.clean_start
            && self.will == other.will
            && self.keep_alive == other.keep_alive
            && self.session_expiry_interval == other.session_expiry_interval
            && self
                .authentication_method
                .py_eq(&other.authentication_method)
            && self.authentication_data.py_eq(&other.authentication_data)
            && self.request_problem_info == other.request_problem_info
            && self.request_response_info == other.request_response_info
            && self.receive_max == other.receive_max
            && self.topic_alias_max == other.topic_alias_max
            && self.max_packet_size == other.max_packet_size
            && self.user_properties.py_eq(&other.user_properties)
    }
}

#[pyclass(eq, get_all, module = "mqtt5")]
pub struct ConnAckPacket {
    pub session_present: bool,
    pub reason_code: ConnAckReasonCode,
    pub session_expiry_interval: Option<u32>,
    pub assigned_client_id: Option<Py<PyString>>,
    pub server_keep_alive: Option<u16>,
    pub authentication_method: Option<Py<PyString>>,
    pub authentication_data: Option<Py<PyBytes>>,
    pub response_info: Option<Py<PyString>>,
    pub server_reference: Option<Py<PyString>>,
    pub reason_str: Option<Py<PyString>>,
    pub receive_max: u16,
    pub topic_alias_max: u16,
    pub max_qos: QoS,
    pub retain_available: bool,
    pub max_packet_size: Option<u32>,
    pub wildcard_subscription_available: bool,
    pub subscription_id_available: bool,
    pub shared_subscription_available: bool,
    pub user_properties: Py<PyList>,
}

#[pymethods]
impl ConnAckPacket {
    #[new]
    #[pyo3(signature = (
        *,
        session_present=false,
        reason_code=ConnAckReasonCode::Success,
        session_expiry_interval=None,
        assigned_client_id=None,
        server_keep_alive=None,
        authentication_method=None,
        authentication_data=None,
        response_info=None,
        server_reference=None,
        reason_str=None,
        receive_max=65535,
        topic_alias_max=0,
        max_qos=QoS::ExactlyOnce,
        retain_available=true,
        max_packet_size=None,
        wildcard_subscription_available=true,
        subscription_id_available=true,
        shared_subscription_available=true,
        user_properties=None,
    ))]
    pub fn new(
        session_present: bool,
        reason_code: ConnAckReasonCode,
        session_expiry_interval: Option<u32>,
        assigned_client_id: Option<Py<PyString>>,
        server_keep_alive: Option<u16>,
        authentication_method: Option<Py<PyString>>,
        authentication_data: Option<Py<PyBytes>>,
        response_info: Option<Py<PyString>>,
        server_reference: Option<Py<PyString>>,
        reason_str: Option<Py<PyString>>,
        receive_max: u16,
        topic_alias_max: u16,
        max_qos: QoS,
        retain_available: bool,
        max_packet_size: Option<u32>,
        wildcard_subscription_available: bool,
        subscription_id_available: bool,
        shared_subscription_available: bool,
        user_properties: Option<Py<PyList>>,
    ) -> PyResult<Self> {
        Ok(Self {
            session_present,
            reason_code,
            session_expiry_interval,
            assigned_client_id,
            server_keep_alive,
            authentication_method,
            authentication_data,
            response_info,
            server_reference,
            reason_str,
            receive_max,
            topic_alias_max,
            max_qos,
            retain_available,
            max_packet_size,
            wildcard_subscription_available,
            subscription_id_available,
            shared_subscription_available,
            user_properties: user_properties
                .unwrap_or_else(|| Python::attach(|py| PyList::empty(py).unbind())),
        })
    }

    pub fn write(&self, py: Python) -> PyResult<Py<PyBytes>> {
        let properties_nbytes = nbytes_properties!(self, {
            PropertyType::SessionExpiryInterval => session_expiry_interval: (Option<u32>) = None,
            PropertyType::AssignedClientId => assigned_client_id: (Option<Py<PyString>>) = None,
            PropertyType::ServerKeepAlive => server_keep_alive: (Option<u16>) = None,
            PropertyType::AuthenticationMethod => authentication_method: (Option<Py<PyString>>) = None,
            PropertyType::AuthenticationData => authentication_data: (Option<Py<PyBytes>>) = None,
            PropertyType::ResponseInfo => response_info: (Option<Py<PyString>>) = None,
            PropertyType::ServerReference => server_reference: (Option<Py<PyString>>) = None,
            PropertyType::ReasonStr => reason_str: (Option<Py<PyString>>) = None,
            PropertyType::ReceiveMax => receive_max: u16 = 65535,
            PropertyType::TopicAliasMax => topic_alias_max: u16 = 0,
            PropertyType::MaxQoS => max_qos: QoS = (QoS::ExactlyOnce),
            PropertyType::RetainAvailable => retain_available: bool = true,
            PropertyType::MaxPacketSize => max_packet_size: (Option<u32>) = None,
            PropertyType::WildcardSubscriptionAvailable => wildcard_subscription_available: bool = true,
            PropertyType::SubscriptionIdAvailable => subscription_id_available: bool = true,
            PropertyType::SharedSubscriptionAvailable => shared_subscription_available: bool = true,
            PropertyType::UserProperty => user_properties: (Py<PyList<UserProperty>>) = PyList::empty(py),
        });
        let properties_remaining_length = VariableByteInteger::new(properties_nbytes as u32);
        let nbytes = 0u8.nbytes()
            + self.reason_code.nbytes()
            + properties_remaining_length.nbytes()
            + properties_nbytes;
        let remaining_length = VariableByteInteger::new(nbytes as u32);
        PyBytes::new_with(py, 1 + remaining_length.nbytes() + nbytes, |buffer| {
            let mut cursor = WriteCursor::new(buffer, 0);

            // [3.2.1] Fixed header
            let first_byte = (PacketType::ConnAck as u8) << 4;
            first_byte.write(&mut cursor);
            remaining_length.write(&mut cursor);

            // [3.2.2] Variable header
            let packet_flags = self.session_present as u8;
            packet_flags.write(&mut cursor);
            self.reason_code.write(&mut cursor);
            properties_remaining_length.write(&mut cursor);
            write_properties!(&mut cursor, self, {
                PropertyType::SessionExpiryInterval => session_expiry_interval: (Option<u32>) = None,
                PropertyType::AssignedClientId => assigned_client_id: (Option<Py<PyString>>) = None,
                PropertyType::ServerKeepAlive => server_keep_alive: (Option<u16>) = None,
                PropertyType::AuthenticationMethod => authentication_method: (Option<Py<PyString>>) = None,
                PropertyType::AuthenticationData => authentication_data: (Option<Py<PyBytes>>) = None,
                PropertyType::ResponseInfo => response_info: (Option<Py<PyString>>) = None,
                PropertyType::ServerReference => server_reference: (Option<Py<PyString>>) = None,
                PropertyType::ReasonStr => reason_str: (Option<Py<PyString>>) = None,
                PropertyType::ReceiveMax => receive_max: u16 = 65535,
                PropertyType::TopicAliasMax => topic_alias_max: u16 = 0,
                PropertyType::MaxQoS => max_qos: QoS = (QoS::ExactlyOnce),
                PropertyType::RetainAvailable => retain_available: bool = true,
                PropertyType::MaxPacketSize => max_packet_size: (Option<u32>) = None,
                PropertyType::WildcardSubscriptionAvailable => wildcard_subscription_available: bool = true,
                PropertyType::SubscriptionIdAvailable => subscription_id_available: bool = true,
                PropertyType::SharedSubscriptionAvailable => shared_subscription_available: bool = true,
                PropertyType::UserProperty => user_properties: (Py<PyList<UserProperty>>) = PyList::empty(py),
            });

            Ok(())
        })
        .map(|bytes| bytes.unbind())
    }
}

impl ConnAckPacket {
    pub fn read(py: Python, cursor: &mut ReadCursor, flags: u8) -> PyResult<Py<Self>> {
        if flags != 0x00 {
            return Err(PyValueError::new_err("Malformed bytes"));
        }

        // [3.2.2] Variable header
        let packet_flags = u8::read(cursor)?;
        if (packet_flags & 0xfe) != 0 {
            return Err(PyValueError::new_err("Malformed bytes"));
        }
        let session_present = (packet_flags & 0x01) != 0;
        let reason_code = ConnAckReasonCode::read(cursor)?;
        read_properties!("ConnAckPacket", cursor, {
            PropertyType::SessionExpiryInterval => session_expiry_interval: (Option<u32>) = None,
            PropertyType::AssignedClientId => assigned_client_id: (Option<Py<PyString>>) = None,
            PropertyType::ServerKeepAlive => server_keep_alive: (Option<u16>) = None,
            PropertyType::AuthenticationMethod => authentication_method: (Option<Py<PyString>>) = None,
            PropertyType::AuthenticationData => authentication_data: (Option<Py<PyBytes>>) = None,
            PropertyType::ResponseInfo => response_info: (Option<Py<PyString>>) = None,
            PropertyType::ServerReference => server_reference: (Option<Py<PyString>>) = None,
            PropertyType::ReasonStr => reason_str: (Option<Py<PyString>>) = None,
            PropertyType::ReceiveMax => receive_max: u16 = 65535,
            PropertyType::TopicAliasMax => topic_alias_max: u16 = 0,
            PropertyType::MaxQoS => max_qos: QoS = QoS::ExactlyOnce,
            PropertyType::RetainAvailable => retain_available: bool = true,
            PropertyType::MaxPacketSize => max_packet_size: (Option<u32>) = None,
            PropertyType::WildcardSubscriptionAvailable => wildcard_subscription_available: bool = true,
            PropertyType::SubscriptionIdAvailable => subscription_id_available: bool = true,
            PropertyType::SharedSubscriptionAvailable => shared_subscription_available: bool = true,
            PropertyType::UserProperty => user_properties: (Py<PyList<UserProperty>>) = PyList::empty(py),
        });

        // Return the Python object
        let packet = Self {
            session_present,
            reason_code,
            session_expiry_interval,
            assigned_client_id,
            server_keep_alive,
            authentication_method,
            authentication_data,
            response_info,
            server_reference,
            reason_str,
            receive_max,
            topic_alias_max,
            max_qos,
            retain_available,
            max_packet_size,
            wildcard_subscription_available,
            subscription_id_available,
            shared_subscription_available,
            user_properties: user_properties.unbind(),
        };
        Py::new(py, packet)
    }
}

impl PartialEq for ConnAckPacket {
    fn eq(&self, other: &Self) -> bool {
        self.session_present == other.session_present
            && self.reason_code == other.reason_code
            && self.session_expiry_interval == other.session_expiry_interval
            && self.assigned_client_id.py_eq(&other.assigned_client_id)
            && self.server_keep_alive == other.server_keep_alive
            && self
                .authentication_method
                .py_eq(&other.authentication_method)
            && self.authentication_data.py_eq(&other.authentication_data)
            && self.response_info.py_eq(&other.response_info)
            && self.server_reference.py_eq(&other.server_reference)
            && self.reason_str.py_eq(&other.reason_str)
            && self.topic_alias_max == other.topic_alias_max
            && self.max_qos == other.max_qos
            && self.retain_available == other.retain_available
            && self.max_packet_size == other.max_packet_size
            && self.wildcard_subscription_available == other.wildcard_subscription_available
            && self.subscription_id_available == other.subscription_id_available
            && self.shared_subscription_available == other.shared_subscription_available
            && self.user_properties.py_eq(&other.user_properties)
    }
}

#[pyclass(frozen, eq, get_all, module = "mqtt5")]
pub struct PublishPacket {
    pub topic: Py<PyString>,
    pub payload: Option<Py<PyBytes>>,
    pub qos: QoS,
    pub retain: bool,
    pub packet_id: Option<u16>,
    pub duplicate: bool,
    pub payload_format_indicator: u8,
    pub message_expiry_interval: Option<u32>,
    pub content_type: Option<Py<PyString>>,
    pub response_topic: Option<Py<PyString>>,
    pub correlation_data: Option<Py<PyBytes>>,
    pub subscription_ids: Py<PyList>,
    pub topic_alias: Option<u16>,
    pub user_properties: Py<PyList>,
}

#[pymethods]
impl PublishPacket {
    #[new]
    #[pyo3(signature = (
        topic,
        *,
        payload=None,
        qos=QoS::AtMostOnce,
        retain=false,
        packet_id=None,
        duplicate=false,
        payload_format_indicator=0,
        message_expiry_interval=None,
        content_type=None,
        response_topic=None,
        correlation_data=None,
        subscription_ids=None,
        topic_alias=None,
        user_properties=None,
    ))]
    pub fn new(
        topic: Py<PyString>,
        payload: Option<Py<PyBytes>>,
        qos: QoS,
        retain: bool,
        packet_id: Option<u16>,
        duplicate: bool,
        payload_format_indicator: u8,
        message_expiry_interval: Option<u32>,
        content_type: Option<Py<PyString>>,
        response_topic: Option<Py<PyString>>,
        correlation_data: Option<Py<PyBytes>>,
        subscription_ids: Option<Py<PyList>>,
        topic_alias: Option<u16>,
        user_properties: Option<Py<PyList>>,
    ) -> PyResult<Self> {
        if packet_id.is_some() && qos == QoS::AtMostOnce {
            return Err(PyValueError::new_err("Packet ID must not be set for QoS=0"));
        }
        if packet_id.is_none() && (qos == QoS::AtLeastOnce || qos == QoS::ExactlyOnce) {
            return Err(PyValueError::new_err(
                "Packet ID must be set for QoS=1 and QoS=2",
            ));
        }
        Ok(Self {
            topic,
            payload,
            qos,
            retain,
            packet_id,
            duplicate,
            payload_format_indicator,
            message_expiry_interval,
            content_type,
            response_topic,
            correlation_data,
            subscription_ids: subscription_ids
                .unwrap_or_else(|| Python::attach(|py| PyList::empty(py).unbind())),
            topic_alias,
            user_properties: user_properties
                .unwrap_or_else(|| Python::attach(|py| PyList::empty(py).unbind())),
        })
    }

    pub fn write(&self, py: Python) -> PyResult<Py<PyBytes>> {
        let payload = self.payload.as_ref().map(|x| x.bind(py).as_bytes());
        let properties_nbytes = nbytes_properties!(self, {
            PropertyType::PayloadFormatIndicator => payload_format_indicator: u8 = 0,
            PropertyType::MessageExpiryInterval => message_expiry_interval: (Option<u32>) = None,
            PropertyType::ContentType => content_type: (Option<Py<PyString>>) = None,
            PropertyType::ResponseTopic => response_topic: (Option<Py<PyString>>) = None,
            PropertyType::CorrelationData => correlation_data: (Option<Py<PyBytes>>) = None,
            PropertyType::SubscriptionId => subscription_ids: (Py<PyList<VariableByteInteger>>) = PyList::empty(py),
            PropertyType::TopicAlias => topic_alias: (Option<u16>) = None,
            PropertyType::UserProperty => user_properties: (Py<PyList<UserProperty>>) = PyList::empty(py),
        });
        let properties_remaining_length = VariableByteInteger::new(properties_nbytes as u32);
        let nbytes = self.topic.nbytes()
            + self.packet_id.nbytes()
            + properties_remaining_length.nbytes()
            + properties_nbytes
            + payload.map_or(0, |payload| payload.len());
        let remaining_length = VariableByteInteger::new(nbytes as u32);
        PyBytes::new_with(py, 1 + remaining_length.nbytes() + nbytes, |buffer| {
            let mut cursor = WriteCursor::new(buffer, 0);

            // [3.3.1] Fixed header
            let first_byte = (PacketType::Publish as u8) << 4
                | (self.duplicate as u8) << 3
                | (self.qos as u8) << 1
                | self.retain as u8;
            first_byte.write(&mut cursor);
            remaining_length.write(&mut cursor);

            // [3.3.2] Variable header
            self.topic.write(&mut cursor);
            self.packet_id.write(&mut cursor);
            properties_remaining_length.write(&mut cursor);
            write_properties!(&mut cursor, self, {
                PropertyType::PayloadFormatIndicator => payload_format_indicator: u8 = 0,
                PropertyType::MessageExpiryInterval => message_expiry_interval: (Option<u32>) = None,
                PropertyType::ContentType => content_type: (Option<Py<PyString>>) = None,
                PropertyType::ResponseTopic => response_topic: (Option<Py<PyString>>) = None,
                PropertyType::CorrelationData => correlation_data: (Option<Py<PyBytes>>) = None,
                PropertyType::SubscriptionId => subscription_ids: (Py<PyList<VariableByteInteger>>) = PyList::empty(py),
                PropertyType::TopicAlias => topic_alias: (Option<u16>) = None,
                PropertyType::UserProperty => user_properties: (Py<PyList<UserProperty>>) = PyList::empty(py),
            });

            // [3.3.3] Payload
            if let Some(payload) = payload {
                let payload_remaining_length = payload.len();
                cursor.buffer[cursor.index..cursor.index + payload_remaining_length]
                    .copy_from_slice(payload);
                cursor.index += payload_remaining_length;
            }

            Ok(())
        })
        .map(|bytes| bytes.unbind())
    }
}

impl PublishPacket {
    pub fn read(py: Python, cursor: &mut ReadCursor, flags: u8) -> PyResult<Py<Self>> {
        let retain = (flags & 0x01) != 0;
        let qos = QoS::new((flags >> 1) & 0x03)?;
        let duplicate = (flags & 0x08) != 0;

        // [3.3.2] Variable header
        let topic = Py::<PyString>::read(cursor)?;
        let packet_id = if qos == QoS::AtLeastOnce || qos == QoS::ExactlyOnce {
            Some(u16::read(cursor)?)
        } else {
            None
        };
        read_properties!("PublishPacket", cursor, {
            PropertyType::PayloadFormatIndicator => payload_format_indicator: u8 = 0,
            PropertyType::MessageExpiryInterval => message_expiry_interval: (Option<u32>) = None,
            PropertyType::ContentType => content_type: (Option<Py<PyString>>) = None,
            PropertyType::ResponseTopic => response_topic: (Option<Py<PyString>>) = None,
            PropertyType::CorrelationData => correlation_data: (Option<Py<PyBytes>>) = None,
            PropertyType::SubscriptionId => subscription_ids: (Py<PyList<VariableByteInteger>>) = PyList::empty(py),
            PropertyType::TopicAlias => topic_alias: (Option<u16>) = None,
            PropertyType::UserProperty => user_properties: (Py<PyList<UserProperty>>) = PyList::empty(py),
        });

        // [3.3.3] Payload
        let payload = PyBytes::new(py, &cursor.buffer[cursor.index..]);
        cursor.index = cursor.buffer.len();

        // Return the Python object
        let packet = Self {
            topic,
            payload: Some(payload.unbind()),
            qos,
            retain,
            packet_id,
            duplicate,
            payload_format_indicator,
            message_expiry_interval,
            content_type,
            response_topic,
            correlation_data,
            subscription_ids: subscription_ids.unbind(),
            topic_alias,
            user_properties: user_properties.unbind(),
        };
        Py::new(py, packet)
    }
}

impl PartialEq for PublishPacket {
    fn eq(&self, other: &Self) -> bool {
        self.topic.py_eq(&other.topic)
            && self.payload.py_eq(&other.payload)
            && self.qos == other.qos
            && self.retain == other.retain
            && self.packet_id == other.packet_id
            && self.duplicate == other.duplicate
            && self.payload_format_indicator == other.payload_format_indicator
            && self.message_expiry_interval == other.message_expiry_interval
            && self.content_type.py_eq(&other.content_type)
            && self.response_topic.py_eq(&other.response_topic)
            && self.correlation_data.py_eq(&other.correlation_data)
            && self.subscription_ids.py_eq(&other.subscription_ids)
            && self.topic_alias == other.topic_alias
            && self.user_properties.py_eq(&other.user_properties)
    }
}

#[pyclass(frozen, eq, get_all, module = "mqtt5")]
pub struct PubAckPacket {
    pub packet_id: u16,
    pub reason_code: PubAckReasonCode,
    pub reason_str: Option<Py<PyString>>,
    pub user_properties: Py<PyList>,
}

#[pymethods]
impl PubAckPacket {
    #[new]
    #[pyo3(signature = (
        packet_id,
        *,
        reason_code=PubAckReasonCode::Success,
        reason_str=None,
        user_properties=None,
    ))]
    pub fn new(
        packet_id: u16,
        reason_code: PubAckReasonCode,
        reason_str: Option<Py<PyString>>,
        user_properties: Option<Py<PyList>>,
    ) -> PyResult<Self> {
        Ok(Self {
            packet_id,
            reason_code,
            reason_str,
            user_properties: user_properties
                .unwrap_or_else(|| Python::attach(|py| PyList::empty(py).unbind())),
        })
    }

    pub fn write(&self, py: Python) -> PyResult<Py<PyBytes>> {
        let properties_nbytes = nbytes_properties!(self, {
            PropertyType::ReasonStr => reason_str: (Option<Py<PyString>>) = None,
            PropertyType::UserProperty => user_properties: (Py<PyList<UserProperty>>) = PyList::empty(py),
        });
        let properties_remaining_length = VariableByteInteger::new(properties_nbytes as u32);
        let nbytes = self.packet_id.nbytes()
            + self.reason_code.nbytes()
            + if properties_nbytes > 0 {
                properties_remaining_length.nbytes() + properties_nbytes
            } else {
                0
            };
        let remaining_length = VariableByteInteger::new(nbytes as u32);
        PyBytes::new_with(py, 1 + remaining_length.nbytes() + nbytes, |buffer| {
            let mut cursor = WriteCursor::new(buffer, 0);

            // [3.4.1] Fixed header
            let first_byte = (PacketType::PubAck as u8) << 4;
            first_byte.write(&mut cursor);
            remaining_length.write(&mut cursor);

            // [3.4.2] Variable header
            self.packet_id.write(&mut cursor);
            self.reason_code.write(&mut cursor);
            if properties_nbytes > 0 {
                properties_remaining_length.write(&mut cursor);
                write_properties!(&mut cursor, self, {
                    PropertyType::ReasonStr => reason_str: (Option<Py<PyString>>) = None,
                    PropertyType::UserProperty => user_properties: (Py<PyList<UserProperty>>) = PyList::empty(py),
                });
            }

            Ok(())
        })
        .map(|bytes| bytes.unbind())
    }
}

impl PubAckPacket {
    pub fn read(py: Python, cursor: &mut ReadCursor, flags: u8) -> PyResult<Py<Self>> {
        if flags != 0x00 {
            return Err(PyValueError::new_err("Malformed bytes"));
        }

        // [3.4.2] Variable header
        let packet_id = u16::read(cursor)?;
        let reason_code = if cursor.index < cursor.buffer.len() {
            PubAckReasonCode::read(cursor)?
        } else {
            PubAckReasonCode::Success
        };
        read_properties!("PubAckPacket", cursor, {
            PropertyType::ReasonStr => reason_str: (Option<Py<PyString>>) = None,
            PropertyType::UserProperty => user_properties: (Py<PyList<UserProperty>>) = PyList::empty(py),
        });

        // Return the Python object
        let packet = Self {
            packet_id,
            reason_code,
            reason_str,
            user_properties: user_properties.unbind(),
        };
        Py::new(py, packet)
    }
}

impl PartialEq for PubAckPacket {
    fn eq(&self, other: &Self) -> bool {
        self.packet_id == other.packet_id
            && self.reason_code == other.reason_code
            && self.reason_str.py_eq(&other.reason_str)
            && self.user_properties.py_eq(&other.user_properties)
    }
}

#[pyclass(frozen, eq, get_all, module = "mqtt5")]
pub struct PubRecPacket {
    pub packet_id: u16,
    pub reason_code: PubRecReasonCode,
    pub reason_str: Option<Py<PyString>>,
    pub user_properties: Py<PyList>,
}

#[pymethods]
impl PubRecPacket {
    #[new]
    #[pyo3(signature = (
        packet_id,
        *,
        reason_code=PubRecReasonCode::Success,
        reason_str=None,
        user_properties=None,
    ))]
    pub fn new(
        packet_id: u16,
        reason_code: PubRecReasonCode,
        reason_str: Option<Py<PyString>>,
        user_properties: Option<Py<PyList>>,
    ) -> PyResult<Self> {
        Ok(Self {
            packet_id,
            reason_code,
            reason_str,
            user_properties: user_properties
                .unwrap_or_else(|| Python::attach(|py| PyList::empty(py).unbind())),
        })
    }

    pub fn write(&self, py: Python) -> PyResult<Py<PyBytes>> {
        let properties_nbytes = nbytes_properties!(self, {
            PropertyType::ReasonStr => reason_str: (Option<Py<PyString>>) = None,
            PropertyType::UserProperty => user_properties: (Py<PyList<UserProperty>>) = PyList::empty(py),
        });
        let properties_remaining_length = VariableByteInteger::new(properties_nbytes as u32);
        let nbytes = self.packet_id.nbytes()
            + self.reason_code.nbytes()
            + if properties_nbytes > 0 {
                properties_remaining_length.nbytes() + properties_nbytes
            } else {
                0
            };
        let remaining_length = VariableByteInteger::new(nbytes as u32);
        PyBytes::new_with(py, 1 + remaining_length.nbytes() + nbytes, |buffer| {
            let mut cursor = WriteCursor::new(buffer, 0);

            // [3.5.1] Fixed header
            let first_byte = (PacketType::PubRec as u8) << 4;
            first_byte.write(&mut cursor);
            remaining_length.write(&mut cursor);

            // [3.5.2] Variable header
            self.packet_id.write(&mut cursor);
            self.reason_code.write(&mut cursor);
            if properties_nbytes > 0 {
                properties_remaining_length.write(&mut cursor);
                write_properties!(&mut cursor, self, {
                    PropertyType::ReasonStr => reason_str: (Option<Py<PyString>>) = None,
                    PropertyType::UserProperty => user_properties: (Py<PyList<UserProperty>>) = PyList::empty(py),
                });
            }

            Ok(())
        })
        .map(|bytes| bytes.unbind())
    }
}

impl PubRecPacket {
    pub fn read(py: Python, cursor: &mut ReadCursor, flags: u8) -> PyResult<Py<Self>> {
        if flags != 0x00 {
            return Err(PyValueError::new_err("Malformed bytes"));
        }

        // [3.5.2] Variable header
        let packet_id = u16::read(cursor)?;
        let reason_code = if cursor.index < cursor.buffer.len() {
            PubRecReasonCode::read(cursor)?
        } else {
            PubRecReasonCode::Success
        };
        read_properties!("PubRecPacket", cursor, {
            PropertyType::ReasonStr => reason_str: (Option<Py<PyString>>) = None,
            PropertyType::UserProperty => user_properties: (Py<PyList<UserProperty>>) = PyList::empty(py),
        });

        // Return the Python object
        let packet = Self {
            packet_id,
            reason_code,
            reason_str,
            user_properties: user_properties.unbind(),
        };
        Py::new(py, packet)
    }
}

impl PartialEq for PubRecPacket {
    fn eq(&self, other: &Self) -> bool {
        self.packet_id == other.packet_id
            && self.reason_code == other.reason_code
            && self.reason_str.py_eq(&other.reason_str)
            && self.user_properties.py_eq(&other.user_properties)
    }
}

#[pyclass(frozen, eq, get_all, module = "mqtt5")]
pub struct PubRelPacket {
    pub packet_id: u16,
    pub reason_code: PubRelReasonCode,
    pub reason_str: Option<Py<PyString>>,
    pub user_properties: Py<PyList>,
}

#[pymethods]
impl PubRelPacket {
    #[new]
    #[pyo3(signature = (
        packet_id,
        *,
        reason_code=PubRelReasonCode::Success,
        reason_str=None,
        user_properties=None,
    ))]
    pub fn new(
        packet_id: u16,
        reason_code: PubRelReasonCode,
        reason_str: Option<Py<PyString>>,
        user_properties: Option<Py<PyList>>,
    ) -> PyResult<Self> {
        Ok(Self {
            packet_id,
            reason_code,
            reason_str,
            user_properties: user_properties
                .unwrap_or_else(|| Python::attach(|py| PyList::empty(py).unbind())),
        })
    }

    pub fn write(&self, py: Python) -> PyResult<Py<PyBytes>> {
        let properties_nbytes = nbytes_properties!(self, {
            PropertyType::ReasonStr => reason_str: (Option<Py<PyString>>) = None,
            PropertyType::UserProperty => user_properties: (Py<PyList<UserProperty>>) = PyList::empty(py),
        });
        let properties_remaining_length = VariableByteInteger::new(properties_nbytes as u32);
        let nbytes = self.packet_id.nbytes()
            + self.reason_code.nbytes()
            + if properties_nbytes > 0 {
                properties_remaining_length.nbytes() + properties_nbytes
            } else {
                0
            };
        let remaining_length = VariableByteInteger::new(nbytes as u32);
        PyBytes::new_with(py, 1 + remaining_length.nbytes() + nbytes, |buffer| {
            let mut cursor = WriteCursor::new(buffer, 0);

            // [3.6.1] Fixed header
            let first_byte = (PacketType::PubRel as u8) << 4 | 0x02;
            first_byte.write(&mut cursor);
            remaining_length.write(&mut cursor);

            // [3.6.2] Variable header
            self.packet_id.write(&mut cursor);
            self.reason_code.write(&mut cursor);
            if properties_nbytes > 0 {
                properties_remaining_length.write(&mut cursor);
                write_properties!(&mut cursor, self, {
                    PropertyType::ReasonStr => reason_str: (Option<Py<PyString>>) = None,
                    PropertyType::UserProperty => user_properties: (Py<PyList<UserProperty>>) = PyList::empty(py),
                });
            }

            Ok(())
        })
        .map(|bytes| bytes.unbind())
    }
}

impl PubRelPacket {
    pub fn read(py: Python, cursor: &mut ReadCursor, flags: u8) -> PyResult<Py<Self>> {
        if flags != 0x02 {
            return Err(PyValueError::new_err("Malformed bytes"));
        }

        // [3.6.2] Variable header
        let packet_id = u16::read(cursor)?;
        let reason_code = if cursor.index < cursor.buffer.len() {
            PubRelReasonCode::read(cursor)?
        } else {
            PubRelReasonCode::Success
        };
        read_properties!("PubRelPacket", cursor, {
            PropertyType::ReasonStr => reason_str: (Option<Py<PyString>>) = None,
            PropertyType::UserProperty => user_properties: (Py<PyList<UserProperty>>) = PyList::empty(py),
        });

        // Return the Python object
        let packet = Self {
            packet_id,
            reason_code,
            reason_str,
            user_properties: user_properties.unbind(),
        };
        Py::new(py, packet)
    }
}

impl PartialEq for PubRelPacket {
    fn eq(&self, other: &Self) -> bool {
        self.packet_id == other.packet_id
            && self.reason_code == other.reason_code
            && self.reason_str.py_eq(&other.reason_str)
            && self.user_properties.py_eq(&other.user_properties)
    }
}

#[pyclass(frozen, eq, get_all, module = "mqtt5")]
pub struct PubCompPacket {
    pub packet_id: u16,
    pub reason_code: PubCompReasonCode,
    pub reason_str: Option<Py<PyString>>,
    pub user_properties: Py<PyList>,
}

#[pymethods]
impl PubCompPacket {
    #[new]
    #[pyo3(signature = (
        packet_id,
        *,
        reason_code=PubCompReasonCode::Success,
        reason_str=None,
        user_properties=None,
    ))]
    pub fn new(
        packet_id: u16,
        reason_code: PubCompReasonCode,
        reason_str: Option<Py<PyString>>,
        user_properties: Option<Py<PyList>>,
    ) -> PyResult<Self> {
        Ok(Self {
            packet_id,
            reason_code,
            reason_str,
            user_properties: user_properties
                .unwrap_or_else(|| Python::attach(|py| PyList::empty(py).unbind())),
        })
    }

    pub fn write(&self, py: Python) -> PyResult<Py<PyBytes>> {
        let properties_nbytes = nbytes_properties!(self, {
            PropertyType::ReasonStr => reason_str: (Option<Py<PyString>>) = None,
            PropertyType::UserProperty => user_properties: (Py<PyList<UserProperty>>) = PyList::empty(py),
        });
        let properties_remaining_length = VariableByteInteger::new(properties_nbytes as u32);
        let nbytes = self.packet_id.nbytes()
            + self.reason_code.nbytes()
            + if properties_nbytes > 0 {
                properties_remaining_length.nbytes() + properties_nbytes
            } else {
                0
            };
        let remaining_length = VariableByteInteger::new(nbytes as u32);
        PyBytes::new_with(py, 1 + remaining_length.nbytes() + nbytes, |buffer| {
            let mut cursor = WriteCursor::new(buffer, 0);

            // [3.7.1] Fixed header
            let first_byte = (PacketType::PubComp as u8) << 4;
            first_byte.write(&mut cursor);
            remaining_length.write(&mut cursor);

            // [3.7.2] Variable header
            self.packet_id.write(&mut cursor);
            self.reason_code.write(&mut cursor);
            if properties_nbytes > 0 {
                properties_remaining_length.write(&mut cursor);
                write_properties!(&mut cursor, self, {
                    PropertyType::ReasonStr => reason_str: (Option<Py<PyString>>) = None,
                    PropertyType::UserProperty => user_properties: (Py<PyList<UserProperty>>) = PyList::empty(py),
                });
            }

            Ok(())
        })
        .map(|bytes| bytes.unbind())
    }
}

impl PubCompPacket {
    pub fn read(py: Python, cursor: &mut ReadCursor, flags: u8) -> PyResult<Py<Self>> {
        if flags != 0x00 {
            return Err(PyValueError::new_err("Malformed bytes"));
        }

        // [3.7.2] Variable header
        let packet_id = u16::read(cursor)?;
        let reason_code = if cursor.index < cursor.buffer.len() {
            PubCompReasonCode::read(cursor)?
        } else {
            PubCompReasonCode::Success
        };
        read_properties!("PubCompPacket", cursor, {
            PropertyType::ReasonStr => reason_str: (Option<Py<PyString>>) = None,
            PropertyType::UserProperty => user_properties: (Py<PyList<UserProperty>>) = PyList::empty(py),
        });

        // Return the Python object
        let packet = Self {
            packet_id,
            reason_code,
            reason_str,
            user_properties: user_properties.unbind(),
        };
        Py::new(py, packet)
    }
}

impl PartialEq for PubCompPacket {
    fn eq(&self, other: &Self) -> bool {
        self.packet_id == other.packet_id
            && self.reason_code == other.reason_code
            && self.reason_str.py_eq(&other.reason_str)
            && self.user_properties.py_eq(&other.user_properties)
    }
}

#[pyclass(frozen, eq, get_all, module = "mqtt5")]
pub struct SubscribePacket {
    pub packet_id: u16,
    pub subscriptions: Py<PyList>,
    pub subscription_id: Option<VariableByteInteger>,
    pub user_properties: Py<PyList>,
}

#[pymethods]
impl SubscribePacket {
    #[new]
    #[pyo3(signature = (
        packet_id,
        subscriptions,
        *,
        subscription_id=None,
        user_properties=None,
    ))]
    pub fn new(
        packet_id: u16,
        subscriptions: Py<PyList>,
        subscription_id: Option<VariableByteInteger>,
        user_properties: Option<Py<PyList>>,
    ) -> PyResult<Self> {
        Ok(Self {
            packet_id,
            subscriptions,
            subscription_id,
            user_properties: user_properties
                .unwrap_or_else(|| Python::attach(|py| PyList::empty(py).unbind())),
        })
    }

    pub fn write(&self, py: Python) -> PyResult<Py<PyBytes>> {
        let properties_nbytes = nbytes_properties!(self, {
            PropertyType::SubscriptionId => subscription_id: (Option<VariableByteInteger>) = None,
            PropertyType::UserProperty => user_properties: (Py<PyList<UserProperty>>) = PyList::empty(py),
        });
        let properties_remaining_length = VariableByteInteger::new(properties_nbytes as u32);
        let subscriptions = self.subscriptions.bind(py);
        let nbytes = self.packet_id.nbytes()
            + properties_remaining_length.nbytes()
            + properties_nbytes
            + subscriptions
                .iter()
                .try_fold(0, |acc, item| -> PyResult<usize> {
                    Ok(acc + item.extract::<PyRef<Subscription>>()?.pattern.nbytes() + 1)
                })?;
        let remaining_length = VariableByteInteger::new(nbytes as u32);
        PyBytes::new_with(py, 1 + remaining_length.nbytes() + nbytes, |buffer| {
            let mut cursor = WriteCursor::new(buffer, 0);

            // [3.8.1] Fixed header
            let first_byte = (PacketType::Subscribe as u8) << 4 | 0x02;
            first_byte.write(&mut cursor);
            remaining_length.write(&mut cursor);

            // [3.8.2] Variable header
            self.packet_id.write(&mut cursor);
            properties_remaining_length.write(&mut cursor);
            write_properties!(&mut cursor, self, {
                PropertyType::SubscriptionId => subscription_id: (Option<VariableByteInteger>) = None,
                PropertyType::UserProperty => user_properties: (Py<PyList<UserProperty>>) = PyList::empty(py),
            });

            // [3.8.3] Payload
            for item in subscriptions.iter() {
                let subscription: PyRef<Subscription> = item.extract()?;
                subscription.pattern.write(&mut cursor);
                let options = subscription.max_qos as u8
                    | (subscription.no_local as u8) << 2
                    | (subscription.retain_as_published as u8) << 3
                    | (subscription.retain_handling as u8) << 4;
                options.write(&mut cursor);
            }

            Ok(())
        })
        .map(|bytes| bytes.unbind())
    }
}

impl SubscribePacket {
    pub fn read(py: Python, cursor: &mut ReadCursor, flags: u8) -> PyResult<Py<Self>> {
        if flags != 0x02 {
            return Err(PyValueError::new_err("Malformed bytes"));
        }

        // [3.8.2] Variable header
        let packet_id = u16::read(cursor)?;
        read_properties!("SubscribePacket", cursor, {
            PropertyType::SubscriptionId => subscription_id: (Option<VariableByteInteger>) = None,
            PropertyType::UserProperty => user_properties: (Py<PyList<UserProperty>>) = PyList::empty(py),
        });

        // [3.8.3] Payload
        let subscriptions = PyList::empty(py);
        while cursor.index < cursor.buffer.len() {
            let pattern = Py::<PyString>::read(cursor)?;
            let options = u8::read(cursor)?;
            let subscription = Subscription {
                pattern,
                max_qos: QoS::new(options & 0x03)?,
                no_local: (options >> 2) & 0x01 != 0,
                retain_as_published: (options >> 3) & 0x01 != 0,
                retain_handling: RetainHandling::new((options >> 4) & 0x03)?,
            };
            subscriptions.append(subscription)?;
        }

        // Return the Python object
        let packet = Self {
            packet_id,
            subscriptions: subscriptions.unbind(),
            subscription_id,
            user_properties: user_properties.unbind(),
        };
        Py::new(py, packet)
    }
}

impl PartialEq for SubscribePacket {
    fn eq(&self, other: &Self) -> bool {
        self.packet_id == other.packet_id
            && self.subscription_id == other.subscription_id
            && self.subscriptions.py_eq(&other.subscriptions)
            && self.user_properties.py_eq(&other.user_properties)
    }
}

#[pyclass(frozen, eq, get_all, module = "mqtt5")]
pub struct SubAckPacket {
    pub packet_id: u16,
    pub reason_codes: Py<PyList>,
    pub reason_str: Option<Py<PyString>>,
    pub user_properties: Py<PyList>,
}

#[pymethods]
impl SubAckPacket {
    #[new]
    #[pyo3(signature = (
        packet_id,
        reason_codes,
        *,
        reason_str=None,
        user_properties=None,
    ))]
    pub fn new(
        packet_id: u16,
        reason_codes: Py<PyList>,
        reason_str: Option<Py<PyString>>,
        user_properties: Option<Py<PyList>>,
    ) -> PyResult<Self> {
        Ok(Self {
            packet_id,
            reason_codes,
            reason_str,
            user_properties: user_properties
                .unwrap_or_else(|| Python::attach(|py| PyList::empty(py).unbind())),
        })
    }

    pub fn write(&self, py: Python) -> PyResult<Py<PyBytes>> {
        let properties_nbytes = nbytes_properties!(self, {
            PropertyType::ReasonStr => reason_str: (Option<Py<PyString>>) = None,
            PropertyType::UserProperty => user_properties: (Py<PyList<UserProperty>>) = PyList::empty(py),
        });
        let properties_remaining_length = VariableByteInteger::new(properties_nbytes as u32);
        let reason_codes = self.reason_codes.bind(py);
        let nbytes = self.packet_id.nbytes()
            + properties_remaining_length.nbytes()
            + properties_nbytes
            + reason_codes
                .try_iter()?
                .try_fold(0, |acc, item| -> PyResult<usize> {
                    Ok(acc + item?.extract::<PyRef<SubAckReasonCode>>()?.nbytes())
                })?;
        let remaining_length = VariableByteInteger::new(nbytes as u32);
        PyBytes::new_with(py, 1 + remaining_length.nbytes() + nbytes, |buffer| {
            let mut cursor = WriteCursor::new(buffer, 0);

            // [3.9.1] Fixed header
            let first_byte = (PacketType::SubAck as u8) << 4;
            first_byte.write(&mut cursor);
            remaining_length.write(&mut cursor);

            // [3.9.2] Variable header
            self.packet_id.write(&mut cursor);
            properties_remaining_length.write(&mut cursor);
            write_properties!(&mut cursor, self, {
                PropertyType::ReasonStr => reason_str: (Option<Py<PyString>>) = None,
                PropertyType::UserProperty => user_properties: (Py<PyList<UserProperty>>) = PyList::empty(py),
            });

            // [3.9.3] Payload
            for item in reason_codes.try_iter()? {
                let reason_code: PyRef<SubAckReasonCode> = item?.extract()?;
                reason_code.write(&mut cursor);
            }

            Ok(())
        })
        .map(|bytes| bytes.unbind())
    }
}

impl SubAckPacket {
    pub fn read(py: Python, cursor: &mut ReadCursor, flags: u8) -> PyResult<Py<Self>> {
        if flags != 0x00 {
            return Err(PyValueError::new_err("Malformed bytes"));
        }

        // [3.9.2] Variable header
        let packet_id = u16::read(cursor)?;
        read_properties!("SubAckPacket", cursor, {
            PropertyType::ReasonStr => reason_str: (Option<Py<PyString>>) = None,
            PropertyType::UserProperty => user_properties: (Py<PyList<UserProperty>>) = PyList::empty(py),
        });

        // [3.9.3] Payload
        let reason_codes = PyList::empty(py);
        while cursor.index < cursor.buffer.len() {
            let reason_code = SubAckReasonCode::read(cursor)?;
            reason_codes.append(reason_code)?;
        }

        // Return the Python object
        let packet = Self {
            packet_id,
            reason_codes: reason_codes.unbind(),
            reason_str,
            user_properties: user_properties.unbind(),
        };
        Py::new(py, packet)
    }
}

impl PartialEq for SubAckPacket {
    fn eq(&self, other: &Self) -> bool {
        self.packet_id == other.packet_id
            && self.reason_str.py_eq(&other.reason_str)
            && self.reason_codes.py_eq(&other.reason_codes)
            && self.user_properties.py_eq(&other.user_properties)
    }
}

#[pyclass(frozen, eq, get_all, module = "mqtt5")]
pub struct UnsubscribePacket {
    pub packet_id: u16,
    pub patterns: Py<PyList>,
    pub user_properties: Py<PyList>,
}

#[pymethods]
impl UnsubscribePacket {
    #[new]
    #[pyo3(signature = (
        packet_id,
        patterns,
        *,
        user_properties=None,
    ))]
    pub fn new(
        packet_id: u16,
        patterns: Py<PyList>,
        user_properties: Option<Py<PyList>>,
    ) -> PyResult<Self> {
        Ok(Self {
            packet_id,
            patterns,
            user_properties: user_properties
                .unwrap_or_else(|| Python::attach(|py| PyList::empty(py).unbind())),
        })
    }

    pub fn write(&self, py: Python) -> PyResult<Py<PyBytes>> {
        let properties_nbytes = nbytes_properties!(self, {
            PropertyType::UserProperty => user_properties: (Py<PyList<UserProperty>>) = PyList::empty(py),
        });
        let properties_remaining_length = VariableByteInteger::new(properties_nbytes as u32);
        let patterns = self.patterns.bind(py);
        let nbytes = self.packet_id.nbytes()
            + properties_remaining_length.nbytes()
            + properties_nbytes
            + patterns
                .iter()
                .try_fold(0, |acc, item| -> PyResult<usize> {
                    Ok(acc + item.extract::<Py<PyString>>()?.nbytes())
                })?;
        let remaining_length = VariableByteInteger::new(nbytes as u32);
        PyBytes::new_with(py, 1 + remaining_length.nbytes() + nbytes, |buffer| {
            let mut cursor = WriteCursor::new(buffer, 0);

            // [3.9.1] Fixed header
            let first_byte = (PacketType::Unsubscribe as u8) << 4 | 0x02;
            first_byte.write(&mut cursor);
            remaining_length.write(&mut cursor);

            // [3.9.2] Variable header
            self.packet_id.write(&mut cursor);
            properties_remaining_length.write(&mut cursor);
            write_properties!(&mut cursor, self, {
                PropertyType::UserProperty => user_properties: (Py<PyList<UserProperty>>) = PyList::empty(py),
            });

            // [3.9.3] Payload
            for item in patterns.iter() {
                let pattern: Py<PyString> = item.extract()?;
                pattern.write(&mut cursor);
            }

            Ok(())
        })
        .map(|bytes| bytes.unbind())
    }
}

impl UnsubscribePacket {
    pub fn read(py: Python, cursor: &mut ReadCursor, flags: u8) -> PyResult<Py<Self>> {
        if flags != 0x02 {
            return Err(PyValueError::new_err("Malformed bytes"));
        }

        // [3.10.2] Variable header
        let packet_id = u16::read(cursor)?;
        read_properties!("UnsubscribePacket", cursor, {
            PropertyType::UserProperty => user_properties: (Py<PyList<UserProperty>>) = PyList::empty(py),
        });

        // [3.9.3] Payload
        let patterns = PyList::empty(py);
        while cursor.index < cursor.buffer.len() {
            let pattern = Py::<PyString>::read(cursor)?;
            patterns.append(pattern)?;
        }

        // Return the Python object
        let packet = Self {
            packet_id,
            patterns: patterns.unbind(),
            user_properties: user_properties.unbind(),
        };
        Py::new(py, packet)
    }
}

impl PartialEq for UnsubscribePacket {
    fn eq(&self, other: &Self) -> bool {
        self.packet_id == other.packet_id
            && self.patterns.py_eq(&other.patterns)
            && self.user_properties.py_eq(&other.user_properties)
    }
}

#[pyclass(frozen, eq, get_all, module = "mqtt5")]
pub struct UnsubAckPacket {
    pub packet_id: u16,
    pub reason_codes: Py<PyList>,
    pub reason_str: Option<Py<PyString>>,
    pub user_properties: Py<PyList>,
}

#[pymethods]
impl UnsubAckPacket {
    #[new]
    #[pyo3(signature = (
        packet_id,
        reason_codes,
        *,
        reason_str=None,
        user_properties=None,
    ))]
    pub fn new(
        packet_id: u16,
        reason_codes: Py<PyList>,
        reason_str: Option<Py<PyString>>,
        user_properties: Option<Py<PyList>>,
    ) -> PyResult<Self> {
        Ok(Self {
            packet_id,
            reason_codes,
            reason_str,
            user_properties: user_properties
                .unwrap_or_else(|| Python::attach(|py| PyList::empty(py).unbind())),
        })
    }

    pub fn write(&self, py: Python) -> PyResult<Py<PyBytes>> {
        let properties_nbytes = nbytes_properties!(self, {
            PropertyType::ReasonStr => reason_str: (Option<Py<PyString>>) = None,
            PropertyType::UserProperty => user_properties: (Py<PyList<UserProperty>>) = PyList::empty(py),
        });
        let properties_remaining_length = VariableByteInteger::new(properties_nbytes as u32);
        let reason_codes = self.reason_codes.bind(py);
        let nbytes = self.packet_id.nbytes()
            + properties_remaining_length.nbytes()
            + properties_nbytes
            + reason_codes
                .try_iter()?
                .try_fold(0, |acc, item| -> PyResult<usize> {
                    Ok(acc + item?.extract::<PyRef<UnsubAckReasonCode>>()?.nbytes())
                })?;
        let remaining_length = VariableByteInteger::new(nbytes as u32);
        PyBytes::new_with(py, 1 + remaining_length.nbytes() + nbytes, |buffer| {
            let mut cursor = WriteCursor::new(buffer, 0);

            // [3.11.1] Fixed header
            let first_byte = (PacketType::UnsubAck as u8) << 4;
            first_byte.write(&mut cursor);
            remaining_length.write(&mut cursor);

            // [3.11.2] Variable header
            self.packet_id.write(&mut cursor);
            properties_remaining_length.write(&mut cursor);
            write_properties!(&mut cursor, self, {
                PropertyType::ReasonStr => reason_str: (Option<Py<PyString>>) = None,
                PropertyType::UserProperty => user_properties: (Py<PyList<UserProperty>>) = PyList::empty(py),
            });

            // [3.11.3] Payload
            for item in reason_codes.try_iter()? {
                let reason_code: PyRef<UnsubAckReasonCode> = item?.extract()?;
                reason_code.write(&mut cursor);
            }

            Ok(())
        })
        .map(|bytes| bytes.unbind())
    }
}

impl UnsubAckPacket {
    pub fn read(py: Python, cursor: &mut ReadCursor, flags: u8) -> PyResult<Py<Self>> {
        if flags != 0x00 {
            return Err(PyValueError::new_err("Malformed bytes"));
        }

        // [3.11.2] Variable header
        let packet_id = u16::read(cursor)?;
        read_properties!("UnsubAckPacket", cursor, {
            PropertyType::ReasonStr => reason_str: (Option<Py<PyString>>) = None,
            PropertyType::UserProperty => user_properties: (Py<PyList<UserProperty>>) = PyList::empty(py),
        });

        // [3.11.3] Payload
        let reason_codes = PyList::empty(py);
        while cursor.index < cursor.buffer.len() {
            let reason_code = UnsubAckReasonCode::read(cursor)?;
            reason_codes.append(reason_code)?;
        }

        // Return the Python object
        let packet = Self {
            packet_id,
            reason_codes: reason_codes.unbind(),
            reason_str,
            user_properties: user_properties.unbind(),
        };
        Py::new(py, packet)
    }
}

impl PartialEq for UnsubAckPacket {
    fn eq(&self, other: &Self) -> bool {
        self.packet_id == other.packet_id
            && self.reason_str.py_eq(&other.reason_str)
            && self.reason_codes.py_eq(&other.reason_codes)
            && self.user_properties.py_eq(&other.user_properties)
    }
}

#[pyclass(frozen, eq, get_all, module = "mqtt5")]
pub struct PingReqPacket {}

#[pymethods]
impl PingReqPacket {
    #[new]
    #[pyo3(signature = ())]
    pub fn new() -> PyResult<Self> {
        Ok(Self {})
    }

    pub fn write(&self, py: Python) -> PyResult<Py<PyBytes>> {
        let remaining_length = VariableByteInteger::new(0);
        PyBytes::new_with(py, 1 + remaining_length.nbytes(), |buffer| {
            let mut cursor = WriteCursor::new(buffer, 0);

            // [3.12.1] Fixed header
            let first_byte = (PacketType::PingReq as u8) << 4;
            first_byte.write(&mut cursor);
            remaining_length.write(&mut cursor);

            Ok(())
        })
        .map(|bytes| bytes.unbind())
    }
}

impl PingReqPacket {
    pub fn read(py: Python, _cursor: &mut ReadCursor, flags: u8) -> PyResult<Py<Self>> {
        if flags != 0x00 {
            return Err(PyValueError::new_err("Malformed bytes"));
        }

        // Return the Python object
        let packet = Self {};
        Py::new(py, packet)
    }
}

impl PartialEq for PingReqPacket {
    fn eq(&self, _other: &Self) -> bool {
        true
    }
}

#[pyclass(frozen, eq, get_all, module = "mqtt5")]
pub struct PingRespPacket {}

#[pymethods]
impl PingRespPacket {
    #[new]
    #[pyo3(signature = ())]
    pub fn new() -> PyResult<Self> {
        Ok(Self {})
    }

    pub fn write(&self, py: Python) -> PyResult<Py<PyBytes>> {
        let remaining_length = VariableByteInteger::new(0);
        PyBytes::new_with(py, 1 + remaining_length.nbytes(), |buffer| {
            let mut cursor = WriteCursor::new(buffer, 0);

            // [3.13.1] Fixed header
            let first_byte = (PacketType::PingResp as u8) << 4;
            first_byte.write(&mut cursor);
            remaining_length.write(&mut cursor);

            Ok(())
        })
        .map(|bytes| bytes.unbind())
    }
}

impl PingRespPacket {
    pub fn read(py: Python, _cursor: &mut ReadCursor, flags: u8) -> PyResult<Py<Self>> {
        if flags != 0x00 {
            return Err(PyValueError::new_err("Malformed bytes"));
        }

        // Return the Python object
        let packet = Self {};
        Py::new(py, packet)
    }
}

impl PartialEq for PingRespPacket {
    fn eq(&self, _other: &Self) -> bool {
        true
    }
}

#[pyclass(frozen, eq, get_all, module = "mqtt5")]
pub struct DisconnectPacket {
    pub reason_code: DisconnectReasonCode,
    pub session_expiry_interval: Option<u32>,
    pub server_reference: Option<Py<PyString>>,
    pub reason_str: Option<Py<PyString>>,
    pub user_properties: Py<PyList>,
}

#[pymethods]
impl DisconnectPacket {
    #[new]
    #[pyo3(signature = (
        *,
        reason_code=DisconnectReasonCode::NormalDisconnection,
        session_expiry_interval=None,
        server_reference=None,
        reason_str=None,
        user_properties=None,
    ))]
    pub fn new(
        reason_code: DisconnectReasonCode,
        session_expiry_interval: Option<u32>,
        server_reference: Option<Py<PyString>>,
        reason_str: Option<Py<PyString>>,
        user_properties: Option<Py<PyList>>,
    ) -> PyResult<Self> {
        Ok(Self {
            reason_code,
            session_expiry_interval,
            server_reference,
            reason_str,
            user_properties: user_properties
                .unwrap_or_else(|| Python::attach(|py| PyList::empty(py).unbind())),
        })
    }

    pub fn write(&self, py: Python) -> PyResult<Py<PyBytes>> {
        let properties_nbytes = nbytes_properties!(self, {
            PropertyType::SessionExpiryInterval => session_expiry_interval: (Option<u32>) = None,
            PropertyType::ServerReference => server_reference: (Option<Py<PyString>>) = None,
            PropertyType::ReasonStr => reason_str: (Option<Py<PyString>>) = None,
            PropertyType::UserProperty => user_properties: (Py<PyList<UserProperty>>) = PyList::empty(py),
        });
        let properties_remaining_length = VariableByteInteger::new(properties_nbytes as u32);
        let nbytes = self.reason_code.nbytes()
            + if properties_nbytes > 0 {
                properties_remaining_length.nbytes() + properties_nbytes
            } else {
                0
            };
        let remaining_length = VariableByteInteger::new(nbytes as u32);
        PyBytes::new_with(py, 1 + remaining_length.nbytes() + nbytes, |buffer| {
            let mut cursor = WriteCursor::new(buffer, 0);

            // [3.14.1] Fixed header
            let first_byte = (PacketType::Disconnect as u8) << 4;
            first_byte.write(&mut cursor);
            remaining_length.write(&mut cursor);

            // [3.14.2] Variable header
            self.reason_code.write(&mut cursor);
            if properties_nbytes > 0 {
                properties_remaining_length.write(&mut cursor);
                write_properties!(&mut cursor, self, {
                    PropertyType::SessionExpiryInterval => session_expiry_interval: (Option<u32>) = None,
                    PropertyType::ServerReference => server_reference: (Option<Py<PyString>>) = None,
                    PropertyType::ReasonStr => reason_str: (Option<Py<PyString>>) = None,
                    PropertyType::UserProperty => user_properties: (Py<PyList<UserProperty>>) = PyList::empty(py),
                });
            }

            Ok(())
        })
        .map(|bytes| bytes.unbind())
    }
}

impl DisconnectPacket {
    pub fn read(py: Python, cursor: &mut ReadCursor, flags: u8) -> PyResult<Py<Self>> {
        if flags != 0x00 {
            return Err(PyValueError::new_err("Malformed bytes"));
        }

        // [3.14.2] Variable header
        let reason_code = if cursor.index < cursor.buffer.len() {
            DisconnectReasonCode::read(cursor)?
        } else {
            DisconnectReasonCode::NormalDisconnection
        };
        read_properties!("DisconnectPacket", cursor, {
            PropertyType::SessionExpiryInterval => session_expiry_interval: (Option<u32>) = None,
            PropertyType::ServerReference => server_reference: (Option<Py<PyString>>) = None,
            PropertyType::ReasonStr => reason_str: (Option<Py<PyString>>) = None,
            PropertyType::UserProperty => user_properties: (Py<PyList<UserProperty>>) = PyList::empty(py),
        });

        // Return the Python object
        let packet = Self {
            reason_code,
            session_expiry_interval,
            server_reference,
            reason_str,
            user_properties: user_properties.unbind(),
        };
        Py::new(py, packet)
    }
}

impl PartialEq for DisconnectPacket {
    fn eq(&self, other: &Self) -> bool {
        self.reason_code == other.reason_code
            && self.session_expiry_interval == other.session_expiry_interval
            && self.server_reference.py_eq(&other.server_reference)
            && self.reason_str.py_eq(&other.reason_str)
            && self.user_properties.py_eq(&other.user_properties)
    }
}

#[pyclass(frozen, eq, get_all, module = "mqtt5")]
pub struct AuthPacket {
    pub reason_code: AuthReasonCode,
    pub authentication_method: Option<Py<PyString>>,
    pub authentication_data: Option<Py<PyBytes>>,
    pub reason_str: Option<Py<PyString>>,
    pub user_properties: Py<PyList>,
}

#[pymethods]
impl AuthPacket {
    #[new]
    #[pyo3(signature = (
        *,
        reason_code=AuthReasonCode::Success,
        authentication_method=None,
        authentication_data=None,
        reason_str=None,
        user_properties=None,
    ))]
    pub fn new(
        reason_code: AuthReasonCode,
        authentication_method: Option<Py<PyString>>,
        authentication_data: Option<Py<PyBytes>>,
        reason_str: Option<Py<PyString>>,
        user_properties: Option<Py<PyList>>,
    ) -> PyResult<Self> {
        Ok(Self {
            reason_code,
            authentication_method,
            authentication_data,
            reason_str,
            user_properties: user_properties
                .unwrap_or_else(|| Python::attach(|py| PyList::empty(py).unbind())),
        })
    }

    pub fn write(&self, py: Python) -> PyResult<Py<PyBytes>> {
        let properties_nbytes = nbytes_properties!(self, {
            PropertyType::AuthenticationMethod => authentication_method: (Option<Py<PyString>>) = None,
            PropertyType::AuthenticationData => authentication_data: (Option<Py<PyBytes>>) = None,
            PropertyType::ReasonStr => reason_str: (Option<Py<PyString>>) = None,
            PropertyType::UserProperty => user_properties: (Py<PyList<UserProperty>>) = PyList::empty(py),
        });
        let properties_remaining_length = VariableByteInteger::new(properties_nbytes as u32);
        let nbytes =
            self.reason_code.nbytes() + properties_remaining_length.nbytes() + properties_nbytes;
        let remaining_length = VariableByteInteger::new(nbytes as u32);
        PyBytes::new_with(py, 1 + remaining_length.nbytes() + nbytes, |buffer| {
            let mut cursor = WriteCursor::new(buffer, 0);

            // [3.15.1] Fixed header
            let first_byte = (PacketType::Auth as u8) << 4;
            first_byte.write(&mut cursor);
            remaining_length.write(&mut cursor);

            // [3.15.2] Variable header
            self.reason_code.write(&mut cursor);
            properties_remaining_length.write(&mut cursor);
            write_properties!(&mut cursor, self, {
                PropertyType::AuthenticationMethod => authentication_method: (Option<Py<PyString>>) = None,
                PropertyType::AuthenticationData => authentication_data: (Option<Py<PyBytes>>) = None,
                PropertyType::ReasonStr => reason_str: (Option<Py<PyString>>) = None,
                PropertyType::UserProperty => user_properties: (Py<PyList<UserProperty>>) = PyList::empty(py),
            });

            Ok(())
        })
        .map(|bytes| bytes.unbind())
    }
}

impl AuthPacket {
    pub fn read(py: Python, cursor: &mut ReadCursor, flags: u8) -> PyResult<Py<Self>> {
        if flags != 0x00 {
            return Err(PyValueError::new_err("Malformed bytes"));
        }

        // [3.15.2] Variable header
        let reason_code = if cursor.index < cursor.buffer.len() {
            AuthReasonCode::read(cursor)?
        } else {
            AuthReasonCode::Success
        };
        read_properties!("AuthPacket", cursor, {
            PropertyType::AuthenticationMethod => authentication_method: (Option<Py<PyString>>) = None,
            PropertyType::AuthenticationData => authentication_data: (Option<Py<PyBytes>>) = None,
            PropertyType::ReasonStr => reason_str: (Option<Py<PyString>>) = None,
            PropertyType::UserProperty => user_properties: (Py<PyList<UserProperty>>) = PyList::empty(py),
        });

        // Return the Python object
        let packet = Self {
            reason_code,
            authentication_method,
            authentication_data,
            reason_str,
            user_properties: user_properties.unbind(),
        };
        Py::new(py, packet)
    }
}

impl PartialEq for AuthPacket {
    fn eq(&self, other: &Self) -> bool {
        self.reason_code == other.reason_code
            && self
                .authentication_method
                .py_eq(&other.authentication_method)
            && self.authentication_data.py_eq(&other.authentication_data)
            && self.reason_str.py_eq(&other.reason_str)
            && self.user_properties.py_eq(&other.user_properties)
    }
}
