//! Client architecture detection from DHCP options.
//!
//! Primary source is DHCP option 93 (Client System Architecture, RFC 4578/5970).
//! Some firmwares report `0x0009` ("EFI BC") instead of `0x0007` — aliased here.
//! Secondary: option 60 vendor-class with `HTTPClient` signals native UEFI HTTP
//! boot, in which case we can skip TFTP and return an http:// URL directly.

use serde::{Deserialize, Serialize};

#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
pub enum ClientArch {
    /// Legacy x86 BIOS PXE (option 93 = 0x0000).
    LegacyX86,
    /// IA32 UEFI (option 93 = 0x0006).
    Ia32Uefi,
    /// x86_64 UEFI (option 93 = 0x0007 or 0x0009).
    X64Uefi,
    /// ARM32 UEFI (option 93 = 0x000A).
    Arm32Uefi,
    /// ARM64 UEFI (option 93 = 0x000B).
    Arm64Uefi,
    /// Unknown / unsupported architecture; caller should log and skip.
    Unknown(u16),
}

impl ClientArch {
    #[must_use]
    pub fn from_option_93(value: u16) -> Self {
        match value {
            0x0000 => Self::LegacyX86,
            0x0006 => Self::Ia32Uefi,
            0x0007 | 0x0009 => Self::X64Uefi,
            0x000A => Self::Arm32Uefi,
            0x000B => Self::Arm64Uefi,
            other => Self::Unknown(other),
        }
    }

    /// Default iPXE binary filename to return via TFTP for this architecture.
    /// Uses `snponly` variants which reuse the firmware's UNDI/SNP network
    /// stack — smaller binaries and broader hardware compatibility than the
    /// all-drivers-included `ipxe.efi`.
    #[must_use]
    pub fn ipxe_bootfile(self) -> Option<&'static str> {
        Some(match self {
            Self::LegacyX86 => "undionly.kpxe",
            Self::Ia32Uefi => "snponly-i386.efi",
            Self::X64Uefi => "snponly.efi",
            // ARM32 UEFI: upstream boot.ipxe.org does not publish a prebuilt
            // snponly variant for this arch. We return None so the DHCP
            // proxy declines rather than advertising a file we can't serve.
            Self::Arm32Uefi | Self::Unknown(_) => return None,
            Self::Arm64Uefi => "snponly-arm64.efi",
        })
    }

    #[must_use]
    pub fn as_str(self) -> &'static str {
        match self {
            Self::LegacyX86 => "bios",
            Self::Ia32Uefi => "uefi-ia32",
            Self::X64Uefi => "uefi-x64",
            Self::Arm32Uefi => "uefi-arm32",
            Self::Arm64Uefi => "uefi-arm64",
            Self::Unknown(_) => "unknown",
        }
    }
}

/// Which firmware class issued the DHCP request. Used to decide the reply
/// path — PXEClient gets TFTP/iPXE chainload, HTTPClient gets an HTTP URL,
/// iPXE itself gets the boot script URL.
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum FirmwareClass {
    /// Firmware PXE ROM (option 60 = "PXEClient").
    PxeClient,
    /// UEFI HTTP boot (option 60 = "HTTPClient").
    HttpClient,
    /// iPXE (option 77 user-class = "iPXE").
    IpxeUserClass,
    /// No recognised signature — likely not a PXE client at all.
    Other,
}

impl FirmwareClass {
    /// Classify a DHCP request. `vendor_class_60` is option 60 (vendor class
    /// identifier); `user_class_77` is option 77 (user class). Check user-class
    /// first because iPXE-that-we-chainloaded will set option 60 to PXEClient
    /// *and* option 77 to iPXE, and the iPXE classification wins.
    #[must_use]
    pub fn classify(vendor_class_60: Option<&[u8]>, user_class_77: Option<&[u8]>) -> Self {
        if let Some(uc) = user_class_77 {
            if uc.windows(b"iPXE".len()).any(|w| w == b"iPXE") {
                return Self::IpxeUserClass;
            }
        }
        match vendor_class_60 {
            Some(v) if v.starts_with(b"PXEClient") => Self::PxeClient,
            Some(v) if v.starts_with(b"HTTPClient") => Self::HttpClient,
            _ => Self::Other,
        }
    }
}

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

    #[test]
    fn arch_aliases_0x0009_to_x64() {
        assert_eq!(ClientArch::from_option_93(0x0007), ClientArch::X64Uefi);
        assert_eq!(ClientArch::from_option_93(0x0009), ClientArch::X64Uefi);
    }

    #[test]
    fn legacy_and_arm() {
        assert_eq!(ClientArch::from_option_93(0x0000), ClientArch::LegacyX86);
        assert_eq!(ClientArch::from_option_93(0x000B), ClientArch::Arm64Uefi);
        assert!(matches!(
            ClientArch::from_option_93(0x1234),
            ClientArch::Unknown(0x1234)
        ));
    }

    #[test]
    fn bootfile_names_stable() {
        assert_eq!(ClientArch::LegacyX86.ipxe_bootfile(), Some("undionly.kpxe"));
        assert_eq!(ClientArch::X64Uefi.ipxe_bootfile(), Some("snponly.efi"));
        assert_eq!(
            ClientArch::Arm64Uefi.ipxe_bootfile(),
            Some("snponly-arm64.efi")
        );
        assert_eq!(ClientArch::Unknown(0xFFFF).ipxe_bootfile(), None);
    }

    #[test]
    fn firmware_class_detects_ipxe_over_pxeclient() {
        let c = FirmwareClass::classify(Some(b"PXEClient:Arch:00007"), Some(b"iPXE"));
        assert_eq!(c, FirmwareClass::IpxeUserClass);
    }

    #[test]
    fn firmware_class_http() {
        let c = FirmwareClass::classify(Some(b"HTTPClient:Arch:00016"), None);
        assert_eq!(c, FirmwareClass::HttpClient);
    }

    #[test]
    fn firmware_class_plain_pxe() {
        let c = FirmwareClass::classify(Some(b"PXEClient"), None);
        assert_eq!(c, FirmwareClass::PxeClient);
    }
}
