use ahash::AHasher;
use mac_address::MacAddressIterator;
use pyo3::{
    IntoPyObjectExt,
    exceptions::{PyOSError, PyTypeError, PyValueError},
    ffi,
    prelude::*,
    pyclass::CompareOp,
    types::{PyBytes, PyDict},
};
use std::{
    hash::{Hash, Hasher},
    ptr::null_mut,
    sync::atomic::{AtomicPtr, AtomicU64, Ordering},
    time::SystemTime,
};
use uuid::{Builder, Bytes, Context, Timestamp, Uuid, Variant, Version};

static NODE: AtomicU64 = AtomicU64::new(0);

pub const RESERVED_NCS: &str = "reserved for NCS compatibility";
pub const RFC_4122: &str = "specified in RFC 4122";
pub const RESERVED_MICROSOFT: &str = "reserved for Microsoft compatibility";
pub const RESERVED_FUTURE: &str = "reserved for future definition";

#[derive(FromPyObject)]
enum StringOrBytes {
    #[pyo3(transparent, annotation = "str")]
    String(String),
    #[pyo3(transparent, annotation = "bytes")]
    Bytes(Vec<u8>),
}

#[pyclass(subclass, module = "uuid_utils")]
#[derive(Clone, Debug)]
struct UUID {
    uuid: Uuid,
}

#[pymethods]
impl UUID {
    pub const NAMESPACE_DNS: UUID = UUID {
        uuid: Uuid::NAMESPACE_DNS,
    };
    pub const NAMESPACE_URL: UUID = UUID {
        uuid: Uuid::NAMESPACE_URL,
    };
    pub const NAMESPACE_OID: UUID = UUID {
        uuid: Uuid::NAMESPACE_OID,
    };
    pub const NAMESPACE_X500: UUID = UUID {
        uuid: Uuid::NAMESPACE_X500,
    };

    #[new]
    #[pyo3(signature = (hex=None, bytes=None, bytes_le=None, fields=None, int=None, version=None))]
    fn new(
        hex: Option<&str>,
        bytes: Option<&Bound<'_, PyBytes>>,
        bytes_le: Option<&Bound<'_, PyBytes>>,
        fields: Option<(u32, u16, u16, u8, u8, u64)>,
        int: Option<u128>,
        version: Option<u8>,
    ) -> PyResult<Self> {
        let result = match (hex, bytes, bytes_le, fields, int) {
            (Some(hex), None, None, None, None) => Self::from_hex(hex),
            (None, Some(bytes), None, None, None) => Self::from_bytes(bytes),
            (None, None, Some(bytes_le), None, None) => Self::from_bytes_le(bytes_le),
            (None, None, None, Some(fields), None) => Self::from_fields(fields),
            (None, None, None, None, Some(int)) => Self::from_int(int),
            _ => Err(PyTypeError::new_err(
                "one of the hex, bytes, bytes_le, fields, or int arguments must be given",
            )),
        };

        match version {
            Some(v) => result?.set_version(v),
            None => result,
        }
    }

    fn __int__(&self) -> u128 {
        self.uuid.as_u128()
    }

    fn __str__(&self) -> String {
        self.uuid.hyphenated().to_string()
    }

    fn __repr__(&self) -> String {
        format!("UUID('{}')", self.uuid.hyphenated())
    }

    fn __richcmp__(&self, other: UUID, op: CompareOp) -> PyResult<bool> {
        Ok(op.matches(self.uuid.cmp(&other.uuid)))
    }

    fn __hash__(&self) -> PyResult<isize> {
        let mut hasher = AHasher::default();
        self.uuid.hash(&mut hasher);
        Ok(hasher.finish() as isize)
    }

    fn set_version(&self, version: u8) -> PyResult<UUID> {
        let version = match version {
            1 => Version::Mac,
            2 => Version::Dce,
            3 => Version::Md5,
            4 => Version::Random,
            5 => Version::Sha1,
            6 => Version::SortMac,
            7 => Version::SortRand,
            8 => Version::Custom,
            _ => return Err(PyErr::new::<PyValueError, &str>("illegal version number.")),
        };

        let mut builder = Builder::from_u128(self.uuid.as_u128());
        builder.set_version(version);

        Ok(UUID {
            uuid: builder.into_uuid(),
        })
    }

    #[allow(unused_variables)]
    fn __setattr__(&self, name: &str, value: Py<PyAny>) -> PyResult<()> {
        Err(PyTypeError::new_err("UUID objects are immutable"))
    }

    fn __getnewargs__(&self) -> (String,) {
        (self.__str__(),)
    }

    pub fn __deepcopy__(&self, py: Python, _memo: &Bound<'_, PyDict>) -> Py<PyAny> {
        self.clone().into_py_any(py).unwrap()
    }

    #[getter]
    fn hex(&self) -> String {
        self.uuid.simple().to_string()
    }

    #[getter]
    fn bytes(&self) -> &[u8] {
        self.uuid.as_bytes()
    }

    #[getter]
    fn bytes_le<'py>(&self, py: Python<'py>) -> Bound<'py, PyBytes> {
        PyBytes::new(py, &self.uuid.to_bytes_le())
    }

    #[getter]
    fn int(&self) -> u128 {
        self.uuid.as_u128()
    }

    #[getter]
    fn urn(&self) -> String {
        self.uuid.urn().to_string()
    }

    #[getter]
    fn version(&self) -> usize {
        self.uuid.get_version_num()
    }

    #[getter]
    fn variant(&self) -> &str {
        match self.uuid.get_variant() {
            Variant::NCS => RESERVED_NCS,
            Variant::RFC4122 => RFC_4122,
            Variant::Microsoft => RESERVED_MICROSOFT,
            Variant::Future => RESERVED_FUTURE,
            _ => RESERVED_FUTURE,
        }
    }

    #[getter]
    fn node(&self) -> u64 {
        (self.int() & 0xffffffffffff) as u64
    }

    #[getter]
    fn time_low(&self) -> u32 {
        self.int().wrapping_shr(96) as u32
    }

    #[getter]
    fn time_mid(&self) -> u16 {
        ((self.int().wrapping_shr(80)) & 0xffff) as u16
    }

    #[getter]
    fn time_hi_version(&self) -> u16 {
        ((self.int().wrapping_shr(64)) & 0xffff) as u16
    }

    #[getter]
    fn clock_seq_hi_variant(&self) -> u8 {
        ((self.int().wrapping_shr(56)) & 0xff) as u8
    }

    #[getter]
    fn clock_seq_low(&self) -> u8 {
        ((self.int().wrapping_shr(48)) & 0xff) as u8
    }

    #[getter]
    fn clock_seq(&self) -> u16 {
        let high = self.clock_seq_hi_variant() as u16 & 0x3f;
        high << 8 | self.clock_seq_low() as u16
    }

    #[getter]
    fn time(&self) -> u64 {
        let high = self.time_hi_version() as u64 & 0x0fff;
        let mid = self.time_mid() as u64;
        high << 48 | mid << 32 | self.time_low() as u64
    }

    #[getter]
    fn timestamp(&self) -> PyResult<u64> {
        match self.uuid.get_timestamp() {
            Some(timestamp) => {
                let (secs, nanos) = timestamp.to_unix();
                Ok(secs * 1_000 + nanos as u64 / 1_000 / 1_000)
            }
            _ => Err(PyErr::new::<PyValueError, &str>(
                "UUID version should be one of (v1, v6 or v7).",
            )),
        }
    }

    #[getter]
    fn fields(&self) -> (u32, u16, u16, u8, u8, u64) {
        let int = self.uuid.as_u128();
        (
            int.wrapping_shr(96) as u32,              // time_low
            ((int.wrapping_shr(80)) & 0xffff) as u16, // time_mid
            ((int.wrapping_shr(64)) & 0xffff) as u16, // time_hi_version
            ((int.wrapping_shr(56)) & 0xff) as u8,    // clock_seq_hi_variant
            ((int.wrapping_shr(48)) & 0xff) as u8,    // clock_seq_low
            (int & 0xffffffffffff) as u64,            // node
        )
    }

    #[staticmethod]
    fn from_hex(hex: &str) -> PyResult<UUID> {
        match Uuid::parse_str(hex) {
            Ok(uuid) => Ok(UUID { uuid }),
            _ => Err(PyValueError::new_err(
                "badly formed hexadecimal UUID string",
            )),
        }
    }

    #[staticmethod]
    fn from_bytes(bytes: &Bound<'_, PyBytes>) -> PyResult<UUID> {
        let bytes: Bytes = bytes.extract()?;
        Ok(UUID {
            uuid: Uuid::from_bytes(bytes),
        })
    }

    #[staticmethod]
    fn from_bytes_le(bytes: &Bound<'_, PyBytes>) -> PyResult<UUID> {
        let bytes: Bytes = bytes.extract()?;
        Ok(UUID {
            uuid: Uuid::from_bytes_le(bytes),
        })
    }

    #[staticmethod]
    fn from_fields(fields: (u32, u16, u16, u8, u8, u64)) -> PyResult<UUID> {
        let time_low = fields.0 as u128;
        let time_mid = fields.1 as u128;
        let time_hi_version = fields.2 as u128;
        let clock_seq_hi_variant = fields.3 as u128;
        let clock_seq_low = fields.4 as u128;
        let node = fields.5 as u128;
        let clock_seq = clock_seq_hi_variant << 8 | clock_seq_low;

        let value =
            time_low << 96 | time_mid << 80 | time_hi_version << 64 | clock_seq << 48 | node;

        Ok(UUID {
            uuid: Uuid::from_u128(value),
        })
    }

    #[staticmethod]
    fn from_int(int: u128) -> PyResult<UUID> {
        Ok(UUID {
            uuid: Uuid::from_u128(int),
        })
    }

    #[getter]
    fn is_safe(&self) -> *mut ffi::PyObject {
        SAFE_UUID_UNKNOWN.load(Ordering::Relaxed)
    }
}

#[pyfunction]
#[pyo3(signature = (node=None, clock_seq=None))]
fn uuid1(node: Option<u64>, clock_seq: Option<u64>) -> PyResult<UUID> {
    let node = match node {
        Some(node) => node.to_be_bytes(),
        None => _getnode().to_be_bytes(),
    };
    let node: &[u8; 6] = node[2..8].try_into().unwrap();
    let uuid = match clock_seq {
        Some(clock_seq) => {
            let dur = SystemTime::now()
                .duration_since(SystemTime::UNIX_EPOCH)
                .unwrap();
            let timestamp =
                Timestamp::from_unix_time(dur.as_secs(), dur.subsec_nanos(), clock_seq as u128, 14);
            Uuid::new_v1(timestamp, node)
        }
        None => Uuid::now_v1(node),
    };
    Ok(UUID { uuid })
}

#[pyfunction]
fn uuid3(namespace: UUID, name: StringOrBytes) -> PyResult<UUID> {
    match name {
        StringOrBytes::String(name) => Ok(UUID {
            uuid: Uuid::new_v3(&namespace.uuid, name.as_bytes()),
        }),
        StringOrBytes::Bytes(name) => Ok(UUID {
            uuid: Uuid::new_v3(&namespace.uuid, &name),
        }),
    }
}

#[pyfunction]
fn uuid4() -> PyResult<UUID> {
    Ok(UUID {
        uuid: Uuid::new_v4(),
    })
}

#[pyfunction]
fn uuid5(namespace: &UUID, name: StringOrBytes) -> PyResult<UUID> {
    match name {
        StringOrBytes::String(name) => Ok(UUID {
            uuid: Uuid::new_v5(&namespace.uuid, name.as_bytes()),
        }),
        StringOrBytes::Bytes(name) => Ok(UUID {
            uuid: Uuid::new_v5(&namespace.uuid, &name),
        }),
    }
}

#[pyfunction]
#[pyo3(signature = (node=None, timestamp=None, nanos=None))]
fn uuid6(node: Option<u64>, timestamp: Option<u64>, nanos: Option<u32>) -> PyResult<UUID> {
    let node = match node {
        Some(node) => node.to_be_bytes(),
        None => _getnode().to_be_bytes(),
    };
    let node: &[u8; 6] = node[2..8].try_into().unwrap();

    let uuid = match timestamp {
        Some(timestamp) => {
            let timestamp =
                Timestamp::from_unix(&Context::new_random(), timestamp, nanos.unwrap_or(0));
            return Ok(UUID {
                uuid: Uuid::new_v6(timestamp, node),
            });
        }
        None => Uuid::now_v6(node),
    };
    Ok(UUID { uuid })
}

#[pyfunction]
#[pyo3(signature = (timestamp=None, nanos=None))]
fn uuid7(timestamp: Option<u64>, nanos: Option<u32>) -> PyResult<UUID> {
    let uuid = match timestamp {
        Some(timestamp) => {
            let timestamp =
                Timestamp::from_unix(&Context::new_random(), timestamp, nanos.unwrap_or(0));
            return Ok(UUID {
                uuid: Uuid::new_v7(timestamp),
            });
        }
        None => Uuid::now_v7(),
    };
    Ok(UUID { uuid })
}

#[pyfunction]
fn uuid8(bytes: &Bound<'_, PyBytes>) -> PyResult<UUID> {
    let bytes: Bytes = bytes.extract()?;
    Ok(UUID {
        uuid: Uuid::new_v8(bytes),
    })
}

fn _getnode() -> u64 {
    let cached = NODE.load(Ordering::Relaxed);

    if cached != 0 {
        return cached;
    }

    fn _is_universal(mac: u64) -> bool {
        (mac & (1 << 41)) == 0
    }

    let mut first_local_mac: Option<u64> = None;
    if let Ok(iter) = MacAddressIterator::new() {
        for mac in iter {
            let bytes = mac.bytes();
            let node = u64::from_be_bytes([
                0, 0, bytes[0], bytes[1], bytes[2], bytes[3], bytes[4], bytes[5],
            ]);

            if node == 0 {
                continue;
            }

            if _is_universal(node) {
                NODE.store(node, Ordering::Relaxed);
                return node;
            } else if first_local_mac.is_none() {
                first_local_mac = Some(node);
            }
        }
    }

    let node = first_local_mac.unwrap_or_else(|| {
        let mut bytes = rand::random::<[u8; 6]>();
        bytes[0] |= 0x01;
        u64::from_be_bytes([
            0, 0, bytes[0], bytes[1], bytes[2], bytes[3], bytes[4], bytes[5],
        ])
    });

    NODE.store(node, Ordering::Relaxed);
    node
}

// ptr to python stdlib uuid.SafeUUID.unknown
static SAFE_UUID_UNKNOWN: AtomicPtr<ffi::PyObject> = AtomicPtr::new(null_mut());

#[pyfunction]
fn getnode() -> PyResult<u64> {
    Ok(_getnode())
}

#[pyfunction]
fn reseed() -> PyResult<()> {
    rand::rng()
        .reseed()
        .map_err(|err| PyOSError::new_err(err.to_string()))
}

#[pymodule]
fn _uuid_utils(m: &Bound<'_, PyModule>) -> PyResult<()> {
    let safe_uuid_unknown = Python::attach(|py| {
        return PyModule::import(py, "uuid")
            .unwrap()
            .getattr("SafeUUID")
            .unwrap()
            .getattr("unknown")
            .unwrap()
            .unbind();
    });

    SAFE_UUID_UNKNOWN.store(safe_uuid_unknown.into_ptr(), Ordering::Relaxed);

    m.add("__version__", env!("CARGO_PKG_VERSION"))?;
    m.add_class::<UUID>()?;
    m.add_function(wrap_pyfunction!(uuid1, m)?)?;
    m.add_function(wrap_pyfunction!(uuid3, m)?)?;
    m.add_function(wrap_pyfunction!(uuid4, m)?)?;
    m.add_function(wrap_pyfunction!(uuid5, m)?)?;
    m.add_function(wrap_pyfunction!(uuid6, m)?)?;
    m.add_function(wrap_pyfunction!(uuid7, m)?)?;
    m.add_function(wrap_pyfunction!(uuid8, m)?)?;
    m.add_function(wrap_pyfunction!(getnode, m)?)?;
    m.add_function(wrap_pyfunction!(reseed, m)?)?;
    m.add("NAMESPACE_DNS", UUID::NAMESPACE_DNS)?;
    m.add("NAMESPACE_URL", UUID::NAMESPACE_URL)?;
    m.add("NAMESPACE_OID", UUID::NAMESPACE_OID)?;
    m.add("NAMESPACE_X500", UUID::NAMESPACE_X500)?;
    m.add("RESERVED_NCS", RESERVED_NCS)?;
    m.add("RFC_4122", RFC_4122)?;
    m.add("RESERVED_MICROSOFT", RESERVED_MICROSOFT)?;
    m.add("RESERVED_FUTURE", RESERVED_FUTURE)?;
    m.add("NIL", UUID { uuid: Uuid::nil() })?;
    m.add("MAX", UUID { uuid: Uuid::max() })?;
    Ok(())
}
