use heapless::{String, Vec};

pub const MAX_NETWORKS: usize = 12;
pub const WIFI_CREDENTIAL_RECORD_BYTES: usize = 112;

const WIFI_CREDENTIAL_MAGIC: &[u8; 8] = b"KINDAWF1";
const WIFI_CREDENTIAL_CHECKSUM_OFFSET: usize = 8;
const WIFI_CREDENTIAL_SECURITY_OFFSET: usize = 12;
const WIFI_CREDENTIAL_SSID_LEN_OFFSET: usize = 13;
const WIFI_CREDENTIAL_PASSWORD_LEN_OFFSET: usize = 14;
const WIFI_CREDENTIAL_SSID_OFFSET: usize = 16;
const WIFI_CREDENTIAL_PASSWORD_OFFSET: usize = WIFI_CREDENTIAL_SSID_OFFSET + 32;

pub type WifiNetworkList = Vec<WifiAccessPoint, MAX_NETWORKS>;
pub type WifiPassword = String<64>;
pub type WifiSsid = String<32>;

#[derive(Clone, Debug, Eq, PartialEq)]
pub struct WifiAccessPoint {
    pub ssid: WifiSsid,
    pub bssid: [u8; 6],
    pub channel: u8,
    pub signal_strength: i8,
    pub security: WifiSecurity,
}

impl WifiAccessPoint {
    pub fn is_locked(&self) -> bool {
        self.security.is_locked()
    }

    pub fn signal_label(&self) -> &'static str {
        match self.signal_strength {
            -50..=i8::MAX => "strong",
            -70..=-51 => "medium",
            _ => "weak",
        }
    }
}

#[derive(Clone, Copy, Debug, Eq, PartialEq)]
pub enum WifiSecurity {
    Open,
    Wep,
    Wpa,
    Wpa2Personal,
    WpaWpa2Personal,
    Wpa2Wpa3Personal,
    Wpa3Personal,
    Other,
}

impl WifiSecurity {
    pub fn is_locked(self) -> bool {
        !matches!(self, Self::Open)
    }

    pub fn label(self) -> &'static str {
        match self {
            Self::Open => "open",
            Self::Wep => "wep",
            Self::Wpa => "wpa",
            Self::Wpa2Personal => "wpa2",
            Self::WpaWpa2Personal => "wpa/wpa2",
            Self::Wpa2Wpa3Personal => "wpa2/wpa3",
            Self::Wpa3Personal => "wpa3",
            Self::Other => "locked",
        }
    }

    fn to_storage_byte(self) -> u8 {
        match self {
            Self::Open => 0,
            Self::Wep => 1,
            Self::Wpa => 2,
            Self::Wpa2Personal => 3,
            Self::WpaWpa2Personal => 4,
            Self::Wpa2Wpa3Personal => 5,
            Self::Wpa3Personal => 6,
            Self::Other => 7,
        }
    }

    fn from_storage_byte(value: u8) -> Option<Self> {
        match value {
            0 => Some(Self::Open),
            1 => Some(Self::Wep),
            2 => Some(Self::Wpa),
            3 => Some(Self::Wpa2Personal),
            4 => Some(Self::WpaWpa2Personal),
            5 => Some(Self::Wpa2Wpa3Personal),
            6 => Some(Self::Wpa3Personal),
            7 => Some(Self::Other),
            _ => None,
        }
    }
}

#[derive(Clone, Debug, Eq, PartialEq)]
pub struct WifiCredentials {
    pub ssid: WifiSsid,
    pub password: WifiPassword,
    pub security: WifiSecurity,
}

impl WifiCredentials {
    pub fn new(ssid: WifiSsid, password: WifiPassword, security: WifiSecurity) -> Self {
        Self {
            ssid,
            password,
            security,
        }
    }

    pub fn from_access_point(access_point: &WifiAccessPoint, password: WifiPassword) -> Self {
        Self::new(access_point.ssid.clone(), password, access_point.security)
    }

    pub fn encode(&self) -> [u8; WIFI_CREDENTIAL_RECORD_BYTES] {
        let mut record = [0u8; WIFI_CREDENTIAL_RECORD_BYTES];
        record[..WIFI_CREDENTIAL_MAGIC.len()].copy_from_slice(WIFI_CREDENTIAL_MAGIC);
        record[WIFI_CREDENTIAL_SECURITY_OFFSET] = self.security.to_storage_byte();
        record[WIFI_CREDENTIAL_SSID_LEN_OFFSET] = self.ssid.len() as u8;
        record[WIFI_CREDENTIAL_PASSWORD_LEN_OFFSET] = self.password.len() as u8;
        record[WIFI_CREDENTIAL_SSID_OFFSET..][..self.ssid.len()]
            .copy_from_slice(self.ssid.as_bytes());
        record[WIFI_CREDENTIAL_PASSWORD_OFFSET..][..self.password.len()]
            .copy_from_slice(self.password.as_bytes());

        let checksum = Self::checksum(&record[WIFI_CREDENTIAL_SECURITY_OFFSET..]);
        record[WIFI_CREDENTIAL_CHECKSUM_OFFSET..][..4].copy_from_slice(&checksum.to_le_bytes());

        record
    }

    pub fn decode(record: &[u8]) -> Option<Self> {
        if record.len() < WIFI_CREDENTIAL_RECORD_BYTES {
            return None;
        }

        if &record[..WIFI_CREDENTIAL_MAGIC.len()] != WIFI_CREDENTIAL_MAGIC {
            return None;
        }

        let expected_checksum = u32::from_le_bytes(
            record[WIFI_CREDENTIAL_CHECKSUM_OFFSET..][..4]
                .try_into()
                .ok()?,
        );
        let actual_checksum = Self::checksum(&record[WIFI_CREDENTIAL_SECURITY_OFFSET..]);
        if expected_checksum != actual_checksum {
            return None;
        }

        let security = WifiSecurity::from_storage_byte(record[WIFI_CREDENTIAL_SECURITY_OFFSET])?;
        let ssid_len = usize::from(record[WIFI_CREDENTIAL_SSID_LEN_OFFSET]);
        let password_len = usize::from(record[WIFI_CREDENTIAL_PASSWORD_LEN_OFFSET]);

        if ssid_len > 32 || password_len > 64 {
            return None;
        }

        let ssid_text =
            core::str::from_utf8(&record[WIFI_CREDENTIAL_SSID_OFFSET..][..ssid_len]).ok()?;
        let password_text =
            core::str::from_utf8(&record[WIFI_CREDENTIAL_PASSWORD_OFFSET..][..password_len])
                .ok()?;

        let mut ssid = WifiSsid::new();
        ssid.push_str(ssid_text).ok()?;

        let mut password = WifiPassword::new();
        password.push_str(password_text).ok()?;

        Some(Self {
            ssid,
            password,
            security,
        })
    }

    fn checksum(bytes: &[u8]) -> u32 {
        let mut hash = 2_166_136_261u32;
        for byte in bytes {
            hash ^= u32::from(*byte);
            hash = hash.wrapping_mul(16_777_619);
        }
        hash
    }
}

#[derive(Clone, Debug, Eq, PartialEq)]
pub enum WifiEvent {
    ScanFinished(Result<WifiNetworkList, WifiFailure>),
    JoinFinished(Result<WifiSsid, WifiFailure>),
}

#[derive(Clone, Copy, Debug, Eq, PartialEq)]
pub enum WifiFailure {
    ScanFailed,
    JoinFailed,
    InvalidCredentials,
    OutOfMemory,
    Unsupported,
    NotConnected,
}

impl WifiFailure {
    pub fn message(self) -> &'static str {
        match self {
            Self::ScanFailed => "WiFi scan failed",
            Self::JoinFailed => "WiFi join failed",
            Self::InvalidCredentials => "Invalid WiFi credentials",
            Self::OutOfMemory => "WiFi ran out of memory",
            Self::Unsupported => "WiFi operation unsupported",
            Self::NotConnected => "WiFi is not connected",
        }
    }
}
