use std::str::from_utf8;
use std::sync::Arc;

use pyo3::intern;
use pyo3::prelude::*;
use pyo3::sync::PyOnceLock;
use pyo3::types::{PyDict, PyType};
use uuid::Uuid;
use uuid::Variant;

use crate::build_tools::is_strict;
use crate::errors::{ErrorType, ErrorTypeDefaults, ValError, ValResult};
use crate::input::Input;
use crate::input::InputType;
use crate::input::ValidationMatch;
use crate::input::input_as_python_instance;
use crate::serializers::BytesMode;
use crate::tools::SchemaDict;

use super::config::ValBytesMode;
use super::model::create_class;
use super::model::force_setattr;
use super::{BuildValidator, CombinedValidator, DefinitionsBuilder, Exactness, ValidationState, Validator};

const UUID_INT: &str = "int";
const UUID_IS_SAFE: &str = "is_safe";

static UUID_TYPE: PyOnceLock<Py<PyType>> = PyOnceLock::new();

fn get_uuid_type(py: Python<'_>) -> PyResult<&Bound<'_, PyType>> {
    UUID_TYPE.import(py, "uuid", "UUID")
}

#[derive(Debug, Clone, Copy)]
enum Version {
    UUIDv1 = 1,
    UUIDv3 = 3,
    UUIDv4 = 4,
    UUIDv5 = 5,
    UUIDv6 = 6,
    UUIDv7 = 7,
    UUIDv8 = 8,
}

impl From<Version> for usize {
    fn from(v: Version) -> Self {
        v as usize
    }
}

impl From<u8> for Version {
    fn from(u: u8) -> Self {
        match u {
            1 => Version::UUIDv1,
            3 => Version::UUIDv3,
            4 => Version::UUIDv4,
            5 => Version::UUIDv5,
            6 => Version::UUIDv6,
            7 => Version::UUIDv7,
            8 => Version::UUIDv8,
            _ => unreachable!(),
        }
    }
}

#[derive(Debug, Clone)]
pub struct UuidValidator {
    strict: bool,
    version: Option<usize>,
}

impl BuildValidator for UuidValidator {
    const EXPECTED_TYPE: &'static str = "uuid";

    fn build(
        schema: &Bound<'_, PyDict>,
        config: Option<&Bound<'_, PyDict>>,
        _definitions: &mut DefinitionsBuilder<Arc<CombinedValidator>>,
    ) -> PyResult<Arc<CombinedValidator>> {
        let py = schema.py();
        // Note(lig): let's keep this conversion through the Version enum just for the sake of validation
        let version = schema.get_as::<u8>(intern!(py, "version"))?.map(Version::from);
        Ok(CombinedValidator::Uuid(Self {
            strict: is_strict(schema, config)?,
            version: version.map(usize::from),
        })
        .into())
    }
}

impl_py_gc_traverse!(UuidValidator {});

impl Validator for UuidValidator {
    fn validate<'py>(
        &self,
        py: Python<'py>,
        input: &(impl Input<'py> + ?Sized),
        state: &mut ValidationState<'_, 'py>,
    ) -> ValResult<Py<PyAny>> {
        let class = get_uuid_type(py)?;
        if let Some(py_input) = input_as_python_instance(input, class) {
            if let Some(expected_version) = self.version {
                let py_input_version: Option<usize> = py_input.getattr(intern!(py, "version"))?.extract()?;
                if !match py_input_version {
                    Some(py_input_version) => py_input_version == expected_version,
                    None => false,
                } {
                    return Err(ValError::new(
                        ErrorType::UuidVersion {
                            expected_version,
                            context: None,
                        },
                        input,
                    ));
                }
            }
            Ok(py_input.clone().unbind())
        } else if state.strict_or(self.strict) && state.extra().input_type == InputType::Python {
            Err(ValError::new(
                ErrorType::IsInstanceOf {
                    class: class
                        .qualname()
                        .and_then(|name| name.extract())
                        .unwrap_or_else(|_| "UUID".to_owned()),
                    context: None,
                },
                input,
            ))
        } else {
            // In python mode this is a coercion, in JSON mode we treat a UUID string as an
            // exact match.
            // TODO V3: we might want to remove the JSON special case
            if state.extra().input_type == InputType::Python {
                state.floor_exactness(Exactness::Lax);
            }
            let uuid = self.get_uuid(input)?;
            // This block checks if the UUID version matches the expected version and
            // if the UUID variant conforms to RFC 9562 (superseding RFC 4122).
            // When dealing with Python inputs, UUIDs must adhere to RFC 9562 standards.
            if let Some(expected_version) = self.version
                && (uuid.get_version_num() != expected_version || uuid.get_variant() != Variant::RFC4122)
            {
                return Err(ValError::new(
                    ErrorType::UuidVersion {
                        expected_version,
                        context: None,
                    },
                    input,
                ));
            }
            self.create_py_uuid(class, &uuid)
        }
    }

    fn get_name(&self) -> &str {
        Self::EXPECTED_TYPE
    }
}

impl UuidValidator {
    fn get_uuid<'py>(&self, input: &(impl Input<'py> + ?Sized)) -> ValResult<Uuid> {
        let uuid = match input.validate_str(true, false).ok().map(ValidationMatch::into_inner) {
            Some(either_string) => {
                let cow = either_string.as_cow()?;
                let uuid_str = cow.as_ref();
                Uuid::parse_str(uuid_str).map_err(|e| {
                    ValError::new(
                        ErrorType::UuidParsing {
                            error: e.to_string(),
                            context: None,
                        },
                        input,
                    )
                })?
            }
            None => {
                let either_bytes = input
                    .validate_bytes(true, ValBytesMode { ser: BytesMode::Utf8 })
                    .map_err(|_| ValError::new(ErrorTypeDefaults::UuidType, input))?
                    .into_inner();
                let bytes_slice = either_bytes.as_slice();

                // Try parsing as utf8, if it fails fall back to bytes
                if let Ok(utf8_str) = from_utf8(bytes_slice)
                    && let Ok(uuid) = Uuid::parse_str(utf8_str)
                {
                    uuid
                } else {
                    Uuid::from_slice(bytes_slice).map_err(|e| {
                        ValError::new(
                            ErrorType::UuidParsing {
                                error: e.to_string(),
                                context: None,
                            },
                            input,
                        )
                    })?
                }
            }
        };

        if let Some(expected_version) = self.version
            && uuid.get_version_num() != expected_version
        {
            return Err(ValError::new(
                ErrorType::UuidVersion {
                    expected_version,
                    context: None,
                },
                input,
            ));
        }
        Ok(uuid)
    }

    /// Sets the attributes in a Python type object (`py_type`) to represent a UUID class.
    /// The function creates the python class and converts the UUID to a u128 integer and
    /// sets the corresponding attributes in the dictionary object to the converted value
    /// and a 'safe' flag.
    ///
    /// This implementation does not use the Python `__init__` function to speed up the process,
    /// as the `__init__` function in the Python `uuid` module performs extensive checks.
    fn create_py_uuid(&self, py_type: &Bound<'_, PyType>, uuid: &Uuid) -> ValResult<Py<PyAny>> {
        static UUID_SAFE_UNKNOWN: PyOnceLock<Py<PyAny>> = PyOnceLock::new();
        let py = py_type.py();
        let safe_unknown = UUID_SAFE_UNKNOWN.get_or_try_init(py, || {
            py.import("uuid")?
                .getattr("SafeUUID")?
                .get_item("unknown")
                .map(Bound::unbind)
        })?;

        let uuid_instance = create_class(py_type)?;
        force_setattr(py, &uuid_instance, intern!(py, UUID_INT), uuid.as_u128())?;
        force_setattr(py, &uuid_instance, intern!(py, UUID_IS_SAFE), safe_unknown)?;
        Ok(uuid_instance.into())
    }
}
