//! Wake-on-LAN.
//!
//! v0.5.0: from the Hosts tab, an operator can wake a bound machine.
//! WoL is a "magic packet" — six `0xFF` bytes followed by the target
//! MAC repeated sixteen times (102 bytes total) — broadcast on the
//! local segment. The NIC's WoL logic matches the repeated MAC and
//! powers the board on.
//!
//! ## Why this is trivial and safe in our container
//!
//! - It's a single UDP datagram to a broadcast address. No privileged
//!   *local* port is needed (we bind an ephemeral source port); the
//!   destination port is conventionally 9 (discard) or 7 (echo), and
//!   nothing actually listens there — the magic is in the payload, not
//!   the port. So WoL works without any extra capability.
//! - We send to the limited broadcast `255.255.255.255` (stays on the
//!   local link) and, when the caller knows the server's own subnet
//!   broadcast, to that too — directed broadcast reaches the right VLAN
//!   even when the host bridges multiple segments.
//!
//! ## Limits
//!
//! WoL only crosses L2. If the target is on a different subnet than the
//! OpenPXE host, the intervening router must be configured to forward
//! directed broadcasts (most aren't, by design). For the common case —
//! OpenPXE and its PXE clients on the same VLAN — the limited broadcast
//! is enough.

use crate::{Error, Result};
use std::net::{Ipv4Addr, SocketAddrV4, UdpSocket};

/// Conventional WoL destination port. 9 (discard) is the de-facto
/// default; the port is immaterial since the match is on the payload.
const WOL_PORT: u16 = 9;

/// Parse a MAC string in any common form (`aa:bb:cc:dd:ee:ff`,
/// `aa-bb-...`, `aabb.ccdd.eeff`, or bare hex) into six octets.
///
/// Returns `Error::Invalid` if it doesn't resolve to exactly six bytes.
pub fn parse_mac(mac: &str) -> Result<[u8; 6]> {
    // Strip every non-hex-digit, then expect exactly 12 hex chars.
    let hex: String = mac.chars().filter(char::is_ascii_hexdigit).collect();
    if hex.len() != 12 {
        return Err(Error::Invalid(format!(
            "invalid MAC '{mac}': expected 6 octets (12 hex digits), got {}",
            hex.len()
        )));
    }
    let mut out = [0u8; 6];
    for (i, byte) in out.iter_mut().enumerate() {
        // Each octet is two hex chars; unwrap is safe — we validated
        // the length and that every char is a hex digit above.
        *byte = u8::from_str_radix(&hex[i * 2..i * 2 + 2], 16)
            .map_err(|e| Error::Invalid(format!("invalid MAC '{mac}': {e}")))?;
    }
    Ok(out)
}

/// Build the 102-byte magic packet for `mac`.
#[must_use]
pub fn magic_packet(mac: [u8; 6]) -> [u8; 102] {
    let mut pkt = [0u8; 102];
    // 6 bytes of 0xFF.
    for b in &mut pkt[..6] {
        *b = 0xFF;
    }
    // MAC repeated 16 times.
    for rep in 0..16 {
        let start = 6 + rep * 6;
        pkt[start..start + 6].copy_from_slice(&mac);
    }
    pkt
}

/// Send a Wake-on-LAN magic packet for `mac` to every address in
/// `broadcasts` (e.g. `255.255.255.255` plus the server's subnet
/// broadcast). Returns the number of broadcast addresses the packet was
/// successfully sent to; errors only if the MAC is malformed or the
/// socket can't be opened at all.
pub fn wake(mac: &str, broadcasts: &[Ipv4Addr]) -> Result<usize> {
    let parsed = parse_mac(mac)?;
    let packet = magic_packet(parsed);

    // Always include the limited broadcast even if the caller didn't —
    // it's the one that works with zero network configuration.
    let mut targets: Vec<Ipv4Addr> = vec![Ipv4Addr::BROADCAST];
    for b in broadcasts {
        if !targets.contains(b) {
            targets.push(*b);
        }
    }

    let sent = send_magic(&packet, &targets, WOL_PORT)?;
    tracing::info!(
        target: "openpxe::wol",
        mac = %mac, broadcasts = sent,
        "Wake-on-LAN magic packet sent"
    );
    Ok(sent)
}

/// Open a broadcast-enabled UDP socket and send `packet` to every
/// `target:port`. Returns how many sends succeeded. Errors if the
/// socket can't be opened or if *no* target accepted the packet.
fn send_magic(packet: &[u8], targets: &[Ipv4Addr], port: u16) -> Result<usize> {
    // Bind an ephemeral local UDP port on all interfaces. SO_BROADCAST
    // must be enabled to send to a broadcast address.
    let sock = UdpSocket::bind(SocketAddrV4::new(Ipv4Addr::UNSPECIFIED, 0))
        .map_err(|e| Error::Invalid(format!("could not open WoL socket: {e}")))?;
    sock.set_broadcast(true)
        .map_err(|e| Error::Invalid(format!("could not enable broadcast: {e}")))?;

    let mut sent = 0usize;
    for &addr in targets {
        match sock.send_to(packet, SocketAddrV4::new(addr, port)) {
            Ok(_) => sent += 1,
            Err(e) => {
                tracing::warn!(
                    target: "openpxe::wol",
                    broadcast = %addr,
                    "WoL send failed: {e}"
                );
            }
        }
    }
    if sent == 0 {
        return Err(Error::Invalid(
            "Wake-on-LAN: no broadcast address accepted the packet".into(),
        ));
    }
    Ok(sent)
}

/// Compute the IPv4 broadcast address for `ip`/`mask`, if both parse.
/// Used so the caller can include the server's own subnet broadcast
/// alongside the limited broadcast.
#[must_use]
pub fn subnet_broadcast(ip: Ipv4Addr, mask: Ipv4Addr) -> Ipv4Addr {
    let ip = u32::from(ip);
    let mask = u32::from(mask);
    Ipv4Addr::from(ip | !mask)
}

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

    #[test]
    fn parse_mac_accepts_common_forms() {
        let want = [0xaa, 0xbb, 0xcc, 0xdd, 0xee, 0xff];
        assert_eq!(parse_mac("aa:bb:cc:dd:ee:ff").unwrap(), want);
        assert_eq!(parse_mac("AA-BB-CC-DD-EE-FF").unwrap(), want);
        assert_eq!(parse_mac("aabb.ccdd.eeff").unwrap(), want);
        assert_eq!(parse_mac("aabbccddeeff").unwrap(), want);
    }

    #[test]
    fn parse_mac_rejects_bad_length() {
        assert!(parse_mac("aa:bb:cc").is_err());
        assert!(parse_mac("").is_err());
        assert!(parse_mac("zz:bb:cc:dd:ee:ff").is_err()); // non-hex stripped → too short
    }

    #[test]
    fn magic_packet_shape() {
        let pkt = magic_packet([0x01, 0x02, 0x03, 0x04, 0x05, 0x06]);
        assert_eq!(&pkt[..6], &[0xFF; 6]);
        // First MAC repetition.
        assert_eq!(&pkt[6..12], &[0x01, 0x02, 0x03, 0x04, 0x05, 0x06]);
        // Last (16th) repetition ends the packet.
        assert_eq!(&pkt[96..102], &[0x01, 0x02, 0x03, 0x04, 0x05, 0x06]);
    }

    #[test]
    fn subnet_broadcast_computes() {
        assert_eq!(
            subnet_broadcast(
                Ipv4Addr::new(192, 168, 1, 49),
                Ipv4Addr::new(255, 255, 255, 0)
            ),
            Ipv4Addr::new(192, 168, 1, 255)
        );
        assert_eq!(
            subnet_broadcast(
                Ipv4Addr::new(10, 5, 3, 7),
                Ipv4Addr::new(255, 255, 0, 0)
            ),
            Ipv4Addr::new(10, 5, 255, 255)
        );
    }

    #[test]
    fn send_magic_delivers_intact_packet_over_loopback() {
        // Deterministic round-trip that doesn't depend on the sandbox
        // permitting a real L2 broadcast: bind a receiver on loopback
        // and confirm send_magic transmits the exact 102-byte packet.
        let rx = UdpSocket::bind(SocketAddrV4::new(Ipv4Addr::LOCALHOST, 0)).unwrap();
        let port = rx.local_addr().unwrap().port();
        rx.set_read_timeout(Some(std::time::Duration::from_secs(2))).unwrap();

        let packet = magic_packet([0x0a, 0x1b, 0x2c, 0x3d, 0x4e, 0x5f]);
        let sent = send_magic(&packet, &[Ipv4Addr::LOCALHOST], port).unwrap();
        assert_eq!(sent, 1);

        let mut buf = [0u8; 128];
        let n = rx.recv(&mut buf).unwrap();
        assert_eq!(n, 102, "magic packet should be 102 bytes");
        assert_eq!(&buf[..102], &packet[..]);
    }
}
