//! Native, byte-order-explicit UUID compatibility type.

#![allow(clippy::missing_errors_doc)] // Result shapes are fixed by the public compatibility map.
#![allow(clippy::needless_pass_by_value)] // Owned arguments mirror mapped C# value parameters.

use crate::Error;
use crate::compat::{Guid, Object};
use md5::{Digest, Md5};
use std::cmp::Ordering;
use std::fmt;

/// A 128-bit UUID stored in RFC/network byte order.
///
/// `System.Guid` uses a mixed-endian byte-array representation for its first
/// three fields. Conversions to and from [`Guid`] explicitly perform that
/// permutation; all protocol-facing byte APIs remain unambiguously big-endian.
#[derive(Clone, Copy, Debug, Default, Eq, Hash, Ord, PartialEq, PartialOrd)]
pub struct UUID(uuid::Uuid);

impl UUID {
    #[must_use]
    pub const fn zero() -> Self {
        Self(uuid::Uuid::nil())
    }

    pub fn new_with_bytes_int32(source: Vec<u8>, pos: i32) -> Result<Self, Error> {
        Self::from_slice_at(&source, pos)
    }

    pub fn new_with_guid(guid: Guid) -> Result<Self, Error> {
        Ok(Self(uuid::Uuid::from_bytes(guid_raw_to_network(guid.0))))
    }

    pub fn new_with_string(val: String) -> Result<Self, Error> {
        if val.is_empty() {
            return Ok(Self::zero());
        }
        parse_guid_text(&val).map(Self).ok_or(Error::Argument)
    }

    pub fn new_with_u_int64(val: u64) -> Result<Self, Error> {
        let mut bytes = [0_u8; 16];
        bytes[8..].copy_from_slice(&val.to_le_bytes());
        Ok(Self(uuid::Uuid::from_bytes(bytes)))
    }

    pub fn crc(&mut self) -> Result<u32, Error> {
        let bytes = self.0.as_bytes();
        Ok((0..4)
            .map(|index| {
                let offset = index * 4;
                u32::from_be_bytes([
                    bytes[offset],
                    bytes[offset + 1],
                    bytes[offset + 2],
                    bytes[offset + 3],
                ])
            })
            .fold(0_u32, u32::wrapping_add))
    }

    pub fn combine(first: Self, second: Self) -> Result<Self, Error> {
        let mut digest = Md5::new();
        digest.update(first.0.as_bytes());
        digest.update(second.0.as_bytes());
        let bytes: [u8; 16] = digest.finalize().into();
        Ok(Self(uuid::Uuid::from_bytes(bytes)))
    }

    pub fn compare_to(&self, id: Self) -> Result<i32, Error> {
        Ok(match self.cmp(&id) {
            Ordering::Less => -1,
            Ordering::Equal => 0,
            Ordering::Greater => 1,
        })
    }

    pub fn deconstruct(&mut self, guid: &mut Guid) -> Result<(), Error> {
        *guid = self.guid();
        Ok(())
    }

    #[must_use]
    pub fn equals_with_uuid(&self, other: Self) -> bool {
        *self == other
    }

    #[must_use]
    pub fn equals_with_object(&self, obj: Object) -> bool {
        matches!(obj, Object::UUID(value) if *self == value)
    }

    pub fn from_bytes(source: Vec<u8>, pos: i32) -> Result<Self, Error> {
        Self::from_slice_at(&source, pos)
    }

    pub fn get_bytes(&self) -> Result<Vec<u8>, Error> {
        Ok(self.0.as_bytes().to_vec())
    }

    /// Returns the hash produced by `System.Guid.GetHashCode` for the wrapped
    /// Guid value. This is deliberately distinct from Rust's randomized
    /// [`Hash`](std::hash::Hash) use in collections.
    #[must_use]
    pub fn get_hash_code(&self) -> i32 {
        let raw = network_to_guid_raw(*self.0.as_bytes());
        let a = i32::from_le_bytes([raw[0], raw[1], raw[2], raw[3]]);
        let b = i32::from(i16::from_le_bytes([raw[4], raw[5]]));
        let c = u16::from_le_bytes([raw[6], raw[7]]);
        a ^ ((b << 16) | i32::from(c)) ^ ((i32::from(raw[10])) << 24 | i32::from(raw[15]))
    }

    pub fn get_u_long(&self) -> Result<u64, Error> {
        let bytes = self.0.as_bytes();
        Ok(u64::from_le_bytes([
            bytes[8], bytes[9], bytes[10], bytes[11], bytes[12], bytes[13], bytes[14], bytes[15],
        ]))
    }

    pub fn parse(val: String) -> Result<Self, Error> {
        Self::new_with_string(val)
    }

    pub fn random() -> Result<Self, Error> {
        Ok(Self(uuid::Uuid::new_v4()))
    }

    pub fn secure_random() -> Result<Self, Error> {
        // Match the pinned implementation exactly: random bytes are first
        // interpreted in Guid.ToByteArray layout, then its raw indices 6 and 8
        // receive the version/variant masks.
        let mut raw = [0_u8; 16];
        getrandom::fill(&mut raw).map_err(|_| Error::InvalidOperation)?;
        raw[6] = (raw[6] & 0x0f) | 0x40;
        raw[8] = (raw[8] & 0x3f) | 0x80;
        Ok(Self(uuid::Uuid::from_bytes(guid_raw_to_network(raw))))
    }

    pub fn to_bytes(&self, dest: &mut [u8], pos: i32) -> Result<(), Error> {
        let start = usize::try_from(pos).map_err(|_| Error::IndexOutOfRange)?;
        let end = start.checked_add(16).ok_or(Error::IndexOutOfRange)?;
        let target = dest.get_mut(start..end).ok_or(Error::IndexOutOfRange)?;
        target.copy_from_slice(self.0.as_bytes());
        Ok(())
    }

    #[must_use]
    #[allow(clippy::inherent_to_string_shadow_display)] // Required mapped C# member.
    pub fn to_string(&self) -> String {
        self.0.hyphenated().to_string()
    }

    pub fn try_parse(val: String, result: &mut Self) -> bool {
        if val.is_empty() {
            *result = Self::zero();
            return false;
        }
        if let Some(value) = parse_guid_text(&val) {
            *result = Self(value);
            true
        } else {
            *result = Self::zero();
            false
        }
    }

    #[must_use]
    #[allow(clippy::should_implement_trait)] // Required mapped operator name.
    pub fn eq(left: Self, right: Self) -> bool {
        left == right
    }

    #[must_use]
    #[allow(clippy::should_implement_trait)] // Required mapped operator name.
    pub fn bitxor(lhs: Self, rhs: Self) -> Self {
        let mut bytes = [0_u8; 16];
        for ((output, left), right) in bytes.iter_mut().zip(lhs.0.as_bytes()).zip(rhs.0.as_bytes())
        {
            *output = left ^ right;
        }
        Self(uuid::Uuid::from_bytes(bytes))
    }

    /// Preserves the mapped explicit conversion. Like the C# operator, invalid
    /// input cannot be represented in its return type and therefore panics.
    ///
    /// # Panics
    ///
    /// Panics when `val` is not accepted by `System.Guid` parsing rules.
    #[must_use]
    pub fn try_from(val: String) -> Self {
        Self::new_with_string(val).expect("invalid UUID string in explicit conversion")
    }

    #[must_use]
    pub fn ne(left: Self, right: Self) -> bool {
        left != right
    }

    #[must_use]
    pub fn guid(&self) -> Guid {
        Guid(network_to_guid_raw(*self.0.as_bytes()))
    }

    pub fn set_guid(&mut self, value: Guid) {
        self.0 = uuid::Uuid::from_bytes(guid_raw_to_network(value.0));
    }

    fn from_slice_at(source: &[u8], pos: i32) -> Result<Self, Error> {
        let start = usize::try_from(pos).map_err(|_| Error::IndexOutOfRange)?;
        let end = start.checked_add(16).ok_or(Error::IndexOutOfRange)?;
        let bytes: [u8; 16] = source
            .get(start..end)
            .ok_or(Error::IndexOutOfRange)?
            .try_into()
            .expect("UUID range has sixteen bytes");
        Ok(Self(uuid::Uuid::from_bytes(bytes)))
    }
}

impl fmt::Display for UUID {
    fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
        self.0.hyphenated().fmt(formatter)
    }
}

impl From<UUID> for Object {
    fn from(value: UUID) -> Self {
        Self::UUID(value)
    }
}

fn guid_raw_to_network(raw: [u8; 16]) -> [u8; 16] {
    [
        raw[3], raw[2], raw[1], raw[0], raw[5], raw[4], raw[7], raw[6], raw[8], raw[9], raw[10],
        raw[11], raw[12], raw[13], raw[14], raw[15],
    ]
}

fn network_to_guid_raw(network: [u8; 16]) -> [u8; 16] {
    [
        network[3],
        network[2],
        network[1],
        network[0],
        network[5],
        network[4],
        network[7],
        network[6],
        network[8],
        network[9],
        network[10],
        network[11],
        network[12],
        network[13],
        network[14],
        network[15],
    ]
}

fn parse_guid_text(value: &str) -> Option<uuid::Uuid> {
    let value = value.trim();
    if let Ok(parsed) = uuid::Uuid::parse_str(value) {
        return Some(parsed);
    }
    if let Some(inner) = value
        .strip_prefix('(')
        .and_then(|inner| inner.strip_suffix(')'))
        .or_else(|| {
            value
                .strip_prefix('{')
                .and_then(|inner| inner.strip_suffix('}'))
        })
        && let Ok(parsed) = uuid::Uuid::parse_str(inner)
    {
        return Some(parsed);
    }
    parse_guid_x_format(value)
}

fn parse_guid_x_format(value: &str) -> Option<uuid::Uuid> {
    let compact: String = value
        .chars()
        .filter(|character| !character.is_whitespace())
        .collect();
    let outer = compact.strip_prefix('{')?.strip_suffix('}')?;
    let (leading, trailing) = outer.split_once(",{")?;
    let trailing = trailing.strip_suffix('}')?;
    let fields: Vec<&str> = leading.split(',').chain(trailing.split(',')).collect();
    if fields.len() != 11 {
        return None;
    }
    let a = u32::from_str_radix(strip_hex_prefix(fields[0])?, 16).ok()?;
    let b = u16::from_str_radix(strip_hex_prefix(fields[1])?, 16).ok()?;
    let c = u16::from_str_radix(strip_hex_prefix(fields[2])?, 16).ok()?;
    let mut bytes = [0_u8; 16];
    bytes[..4].copy_from_slice(&a.to_be_bytes());
    bytes[4..6].copy_from_slice(&b.to_be_bytes());
    bytes[6..8].copy_from_slice(&c.to_be_bytes());
    for (output, field) in bytes[8..].iter_mut().zip(&fields[3..]) {
        *output = u8::from_str_radix(strip_hex_prefix(field)?, 16).ok()?;
    }
    Some(uuid::Uuid::from_bytes(bytes))
}

fn strip_hex_prefix(field: &str) -> Option<&str> {
    field
        .strip_prefix("0x")
        .or_else(|| field.strip_prefix("0X"))
}

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

    const NETWORK: [u8; 16] = [
        0x00, 0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, 0x08, 0x09, 0x0a, 0x0b, 0x0c, 0x0d, 0x0e,
        0x0f,
    ];
    const TEXT: &str = "00010203-0405-0607-0809-0a0b0c0d0e0f";

    #[test]
    fn network_bytes_string_and_guid_raw_bytes_are_unambiguous() {
        let id = UUID::new_with_bytes_int32(NETWORK.to_vec(), 0).unwrap();
        assert_eq!(id.to_string(), TEXT);
        assert_eq!(id.get_bytes().unwrap(), NETWORK);
        assert_eq!(
            id.guid().0,
            [
                0x03, 0x02, 0x01, 0x00, 0x05, 0x04, 0x07, 0x06, 0x08, 0x09, 0x0a, 0x0b, 0x0c, 0x0d,
                0x0e, 0x0f,
            ]
        );
        assert_eq!(UUID::new_with_guid(id.guid()).unwrap(), id);
    }

    #[test]
    fn parsing_offsets_and_malformed_inputs_match_contract() {
        assert_eq!(UUID::new_with_string(String::new()).unwrap(), UUID::zero());
        assert_eq!(
            UUID::parse(TEXT.replace('-', "")).unwrap().to_string(),
            TEXT
        );
        assert_eq!(UUID::parse(TEXT.to_uppercase()).unwrap().to_string(), TEXT);
        assert_eq!(
            UUID::parse(format!("  {TEXT}\t")).unwrap().to_string(),
            TEXT
        );
        assert_eq!(UUID::parse(format!("({TEXT})")).unwrap().to_string(), TEXT);
        assert_eq!(
            UUID::parse(format!("{{{TEXT}}}")).unwrap().to_string(),
            TEXT
        );
        assert_eq!(
            UUID::parse(
                "{0x00010203,0x0405,0x0607,{0x08,0x09,0x0a,0x0b,0x0c,0x0d,0x0e,0x0f}}".to_owned()
            )
            .unwrap()
            .to_string(),
            TEXT
        );
        assert_eq!(UUID::parse("bad".to_owned()), Err(Error::Argument));

        let mut output = UUID::parse(TEXT.to_owned()).unwrap();
        assert!(!UUID::try_parse(String::new(), &mut output));
        assert_eq!(output, UUID::zero());
        assert!(!UUID::try_parse("bad".to_owned(), &mut output));
        assert_eq!(output, UUID::zero());

        let mut with_offset = vec![0xff; 20];
        with_offset[2..18].copy_from_slice(&NETWORK);
        assert_eq!(UUID::from_bytes(with_offset, 2).unwrap().to_string(), TEXT);
        assert_eq!(
            UUID::from_bytes(vec![0; 15], 0),
            Err(Error::IndexOutOfRange)
        );
    }

    #[test]
    fn crc_ulong_xor_and_combine_match_golden_values() {
        let mut id = UUID::parse(TEXT.to_owned()).unwrap();
        assert_eq!(id.crc().unwrap(), 0x181c_2024);
        assert_eq!(id.get_u_long().unwrap(), 0x0f0e_0d0c_0b0a_0908);
        assert_eq!(UUID::bitxor(id, id), UUID::zero());

        let second = UUID::parse("ffffffff-ffff-ffff-ffff-ffffffffffff".to_owned()).unwrap();
        assert_eq!(
            UUID::combine(id, second).unwrap().to_string(),
            "1145ee5c-de6d-592f-027e-3daf0424653e"
        );
    }

    #[test]
    fn destination_write_and_u64_constructor_preserve_exact_bytes() {
        let id = UUID::parse(TEXT.to_owned()).unwrap();
        let mut destination = [0xff; 20];
        id.to_bytes(&mut destination, 2).unwrap();
        assert_eq!(&destination[2..18], &NETWORK);
        assert_eq!(destination[0], 0xff);
        assert_eq!(destination[19], 0xff);
        assert_eq!(
            id.to_bytes(&mut destination, 5),
            Err(Error::IndexOutOfRange)
        );

        let from_u64 = UUID::new_with_u_int64(0x0102_0304_0506_0708).unwrap();
        assert_eq!(
            from_u64.get_bytes().unwrap(),
            [0, 0, 0, 0, 0, 0, 0, 0, 8, 7, 6, 5, 4, 3, 2, 1]
        );
    }

    #[test]
    fn generated_values_have_expected_uniqueness_and_bit_layout() {
        let random = UUID::random().unwrap();
        assert_ne!(random, UUID::zero());
        assert_eq!(random.0.get_version_num(), 4);
        assert_eq!(random.0.as_bytes()[8] & 0xc0, 0x80);

        let secure = UUID::secure_random().unwrap();
        let raw = secure.guid().0;
        assert_eq!(raw[6] & 0xf0, 0x40);
        assert_eq!(raw[8] & 0xc0, 0x80);
    }

    #[test]
    fn comparison_hash_guid_mutation_and_mapped_operators_are_compatible() {
        let id = UUID::parse(TEXT.to_owned()).unwrap();
        let greater = UUID::parse("00010203-0405-0607-0809-0a0b0c0d0e10".to_owned()).unwrap();
        assert_eq!(id.compare_to(id).unwrap(), 0);
        assert_eq!(id.compare_to(greater).unwrap(), -1);
        assert_eq!(greater.compare_to(id).unwrap(), 1);
        assert_eq!(id.get_hash_code(), 0x0e04_040b);

        assert!(UUID::eq(id, id));
        assert!(!UUID::ne(id, id));
        assert!(id.equals_with_object(Object::from(id)));
        assert!(!id.equals_with_object(Object::from(TEXT)));
        assert_eq!(UUID::try_from(TEXT.to_owned()), id);

        let mut deconstructed = Guid([0xff; 16]);
        let mut mutable = id;
        mutable.deconstruct(&mut deconstructed).unwrap();
        assert_eq!(deconstructed, id.guid());
        mutable.set_guid(greater.guid());
        assert_eq!(mutable, greater);
    }
}
