//! Locale-independent Wi-Fi inventory via the Win32 Native Wi-Fi API
//! (`wlanapi.dll`).
//!
//! Replaces the previous `netsh wlan show …` text-parsing approach.
//! `netsh` partially localizes its output — labels like
//! `Authentication` / `Authentifizierung` / `Autenticación` / `Autentificare`
//! shift with the user's display language, and our regexes silently
//! dropped fields on non-English Windows. The fleet runs four
//! languages (en, de, es, ro), so we move to the structured API:
//!
//!   * SSIDs come back as raw `UCHAR[32]` bytes — no localization
//!     possible.
//!   * Authentication comes back as a `DOT11_AUTH_ALGORITHM` enum
//!     (a small integer); we map to our own English labels.
//!   * Cipher / signal quality are likewise plain enums / integers.
//!
//! Returned shape (consumed by `inventory::collect_inventory` and
//! merged into the sysinfo upload):
//!
//! ```text
//! wifi_current: { ssid, bssid, signal_pct, rssi_dbm, rx_kbps, tx_kbps,
//!                 auth, cipher } | None
//! wifi_nearby:  [ { ssid, signal_pct, auth, cipher }, … up to 30 ]
//! ```
//!
//! All allocations from the API (handle, interface list, network list,
//! query-interface buffer) are freed in this module — no caller cleanup.
//! Every failure path returns best-effort partial data; we never panic.
//!
//! We deliberately do **not** trigger a fresh scan via `WlanScan`. The
//! OS scans periodically on its own, and the cached BSS list is usually
//! < 60 s old. Triggering a scan would either delay startup by ~5 s
//! waiting for `wlan_notification_acm_scan_complete`, or return stale
//! data anyway if we don't wait. The next inventory cycle (≤ 120 s)
//! picks up any change.

#![cfg(target_os = "windows")]

use serde_json::{json, Value};
use std::collections::HashMap;
use std::ptr;
use std::slice;

// IMPORTANT: use winapi's `HANDLE` / `PVOID` (which under the hood wrap
// `winapi::ctypes::c_void`) rather than `std::ffi::c_void`. Rust treats
// the two void types as distinct even though they're the same in C —
// mixing them produces opaque "expected `winapi::ctypes::c_void`, found
// `std::ffi::c_void`" errors. macOS hosts can't catch this because
// winapi is gated to Windows targets, so Cargo never typechecks this
// file off Windows.
use winapi::shared::minwindef::DWORD;
use winapi::shared::ntdef::{HANDLE, PVOID};
use winapi::shared::winerror::ERROR_SUCCESS;
use winapi::shared::wlantypes::DOT11_SSID;
use winapi::um::wlanapi::{
    wlan_intf_opcode_current_connection, wlan_interface_state_connected, WlanCloseHandle,
    WlanEnumInterfaces, WlanFreeMemory, WlanGetAvailableNetworkList, WlanOpenHandle,
    WlanQueryInterface, WLAN_AVAILABLE_NETWORK_LIST, WLAN_CONNECTION_ATTRIBUTES,
    WLAN_INTERFACE_INFO_LIST,
};

/// Vista+ client version. Win10/Win11 happily negotiate down via
/// `negotiated_version`. We never need v1 (XP-era).
const CLIENT_VERSION_VISTA: DWORD = 2;

/// Public entry point. Returns `(current_connection, nearby_list)`.
/// On hosts without a Wi-Fi adapter, without the WLAN AutoConfig
/// service, or on any FFI failure → `(None, vec![])`.
pub fn collect() -> (Option<Value>, Vec<Value>) {
    // SAFETY: every raw-pointer dereference and array read below is
    // bounds-checked against the `dwNumberOfItems` field of the parent
    // list, and the API contract is that those fields match the
    // allocated array length. All allocations are paired with
    // `WlanFreeMemory` before this function returns.
    unsafe { collect_inner() }
}

unsafe fn collect_inner() -> (Option<Value>, Vec<Value>) {
    let mut handle: HANDLE = ptr::null_mut();
    let mut neg_ver: DWORD = 0;
    if WlanOpenHandle(
        CLIENT_VERSION_VISTA,
        ptr::null_mut(),
        &mut neg_ver,
        &mut handle,
    ) != ERROR_SUCCESS as DWORD
    {
        return (None, Vec::new());
    }

    let mut current: Option<Value> = None;
    // Dedupe nearby by SSID, keeping the strongest-quality entry per
    // network. WlanGetAvailableNetworkList already collapses BSSIDs to
    // one row per (SSID, BSS-type), but if multiple Wi-Fi NICs are
    // present we'd see the same SSID twice; this keeps the louder copy.
    let mut nearby_by_ssid: HashMap<String, Value> = HashMap::new();

    let mut iface_list: *mut WLAN_INTERFACE_INFO_LIST = ptr::null_mut();
    if WlanEnumInterfaces(handle, ptr::null_mut(), &mut iface_list) == ERROR_SUCCESS as DWORD
        && !iface_list.is_null()
    {
        let count = (*iface_list).dwNumberOfItems as usize;
        if count > 0 {
            let ifaces = slice::from_raw_parts((*iface_list).InterfaceInfo.as_ptr(), count);
            for iface in ifaces {
                read_iface(handle, iface, &mut current, &mut nearby_by_ssid);
            }
        }
        WlanFreeMemory(iface_list as *mut _);
    }

    WlanCloseHandle(handle, ptr::null_mut());

    let mut nearby: Vec<Value> = nearby_by_ssid.into_values().collect();
    nearby.sort_by(|a, b| {
        let bs = b.get("signal_pct").and_then(|v| v.as_u64()).unwrap_or(0);
        let as_ = a.get("signal_pct").and_then(|v| v.as_u64()).unwrap_or(0);
        bs.cmp(&as_)
    });
    nearby.truncate(30);

    (current, nearby)
}

unsafe fn read_iface(
    handle: HANDLE,
    iface: &winapi::um::wlanapi::WLAN_INTERFACE_INFO,
    current: &mut Option<Value>,
    nearby_by_ssid: &mut HashMap<String, Value>,
) {
    let guid = iface.InterfaceGuid;

    // Current connection. Querying when the interface isn't connected
    // returns ERROR_NOT_FOUND — no harm, but skipping the call is
    // tidier and avoids a noisy ETW event on locked-down endpoints.
    if iface.isState == wlan_interface_state_connected && current.is_none() {
        let mut data_size: DWORD = 0;
        let mut data: PVOID = ptr::null_mut();
        let mut value_type: u32 = 0;
        if WlanQueryInterface(
            handle,
            &guid,
            wlan_intf_opcode_current_connection,
            ptr::null_mut(),
            &mut data_size,
            &mut data,
            &mut value_type,
        ) == ERROR_SUCCESS as DWORD
            && !data.is_null()
            && data_size as usize >= std::mem::size_of::<WLAN_CONNECTION_ATTRIBUTES>()
        {
            let attrs = &*(data as *const WLAN_CONNECTION_ATTRIBUTES);
            let assoc = &attrs.wlanAssociationAttributes;
            let sec = &attrs.wlanSecurityAttributes;
            let ssid = read_ssid(&assoc.dot11Ssid);
            let bssid = format_mac(&assoc.dot11Bssid);
            *current = Some(json!({
                "ssid": ssid,
                "bssid": bssid,
                "signal_pct": assoc.wlanSignalQuality,
                "rssi_dbm": rssi_from_quality(assoc.wlanSignalQuality),
                "rx_kbps": assoc.ulRxRate,
                "tx_kbps": assoc.ulTxRate,
                "auth": auth_label(sec.dot11AuthAlgorithm as u32),
                "cipher": cipher_label(sec.dot11CipherAlgorithm as u32),
            }));
            WlanFreeMemory(data);
        }
    }

    // Available networks (cached scan).
    let mut net_list: *mut WLAN_AVAILABLE_NETWORK_LIST = ptr::null_mut();
    if WlanGetAvailableNetworkList(handle, &guid, 0, ptr::null_mut(), &mut net_list)
        == ERROR_SUCCESS as DWORD
        && !net_list.is_null()
    {
        let n = (*net_list).dwNumberOfItems as usize;
        if n > 0 {
            let nets = slice::from_raw_parts((*net_list).Network.as_ptr(), n);
            for net in nets {
                let ssid = read_ssid(&net.dot11Ssid);
                if ssid.is_empty() {
                    // Hidden networks broadcast empty SSIDs in beacons —
                    // skip rather than emit `{ ssid: "" }` rows that
                    // collapse together in the dedupe map.
                    continue;
                }
                let entry_signal = net.wlanSignalQuality;
                let prev_signal = nearby_by_ssid
                    .get(&ssid)
                    .and_then(|v| v.get("signal_pct"))
                    .and_then(|v| v.as_u64())
                    .unwrap_or(0);
                if (entry_signal as u64) >= prev_signal {
                    nearby_by_ssid.insert(
                        ssid.clone(),
                        json!({
                            "ssid": ssid,
                            "signal_pct": entry_signal,
                            "auth": auth_label(net.dot11DefaultAuthAlgorithm as u32),
                            "cipher": cipher_label(net.dot11DefaultCipherAlgorithm as u32),
                        }),
                    );
                }
            }
        }
        WlanFreeMemory(net_list as *mut _);
    }
}

/// Read a `DOT11_SSID` (length-prefixed UCHAR[32]) into a Rust String.
/// SSIDs are nominally any byte sequence ≤ 32 octets; UTF-8 is by far
/// the most common (also the IEEE recommendation), but Latin-1 and
/// random bytes occur. We use lossy UTF-8 decoding so weird encodings
/// don't yield panics or empty strings — the operator can still
/// recognize most networks visually.
fn read_ssid(s: &DOT11_SSID) -> String {
    let len = (s.uSSIDLength as usize).min(s.ucSSID.len());
    String::from_utf8_lossy(&s.ucSSID[..len]).into_owned()
}

fn format_mac(bytes: &[u8; 6]) -> String {
    format!(
        "{:02X}:{:02X}:{:02X}:{:02X}:{:02X}:{:02X}",
        bytes[0], bytes[1], bytes[2], bytes[3], bytes[4], bytes[5]
    )
}

/// MS docs: WLAN_SIGNAL_QUALITY 0 ↔ −100 dBm, 100 ↔ −50 dBm, linear
/// interpolation in between. `quality / 2 - 100`.
fn rssi_from_quality(q: u32) -> i32 {
    -100 + (q.min(100) as i32 / 2)
}

/// Human label for a `DOT11_AUTH_ALGORITHM` value. Matched on raw
/// integers because winapi 0.3 was tagged before WPA3 / OWE went into
/// the SDK — relying on the named constants would force a dep upgrade
/// just to recognize WPA3 networks. Numeric values are part of the
/// stable Wi-Fi protocol spec, not the SDK.
fn auth_label(a: u32) -> &'static str {
    match a {
        1 => "Open",
        2 => "Shared",
        3 => "WPA-Enterprise",
        4 => "WPA-Personal",
        5 => "WPA-None",
        6 => "WPA2-Enterprise",
        7 => "WPA2-Personal",
        8 => "WPA3-Enterprise (192-bit)",
        9 => "WPA3-Personal (SAE)",
        10 => "OWE",
        11 => "WPA3-Enterprise",
        _ => "Unknown",
    }
}

/// Human label for a `DOT11_CIPHER_ALGORITHM` value. Same numeric-match
/// rationale as `auth_label`. Values from the IEEE 802.11 / Microsoft
/// header (wlantypes.h DOT11_CIPHER_ALGORITHM enum).
fn cipher_label(c: u32) -> &'static str {
    match c {
        0x00 => "None",
        0x01 => "WEP-40",
        0x02 => "TKIP",
        0x04 => "AES (CCMP)",
        0x05 => "WEP-104",
        0x06 => "BIP-CMAC-128",
        0x08 => "GCMP",
        0x09 => "GCMP-256",
        0x0A => "CCMP-256",
        0x0B => "BIP-GMAC-128",
        0x0C => "BIP-GMAC-256",
        0x0D => "BIP-CMAC-256",
        0x100 => "WPA-Use-Group",
        0x101 => "WEP",
        _ => "Unknown",
    }
}
