use std::cmp::Ordering;
use std::fmt;
use std::hash::{Hash, Hasher};
use std::marker::PhantomData;

use crate::TypeError;

#[cfg(feature = "id")]
use uuid::{Uuid, Version};

/// Validation rule a checked string enforces at construction
pub trait CheckedStringRule {
    fn check(value: String) -> Result<String, TypeError>;
}

/// String validated on construction by rule `R`
#[cfg_attr(
    feature = "wire",
    derive(rkyv::Archive, rkyv::Serialize, rkyv::Deserialize)
)]
pub struct CheckedString<R> {
    value: String,
    #[cfg_attr(feature = "wire", rkyv(omit_bounds))]
    marker: PhantomData<fn() -> R>,
}

/// Port validated on construction to lie in 1..=65535
#[cfg(feature = "net")]
#[derive(Clone, Copy, Debug, PartialEq, Eq, PartialOrd, Ord, Hash)]
#[cfg_attr(
    feature = "wire",
    derive(rkyv::Archive, rkyv::Serialize, rkyv::Deserialize)
)]
pub struct CheckedPort(i32);

#[cfg(feature = "net")]
impl CheckedPort {
    pub fn new(value: u16) -> Result<Self, TypeError> {
        if value == 0 {
            return Err(Self::error("got 0"));
        }
        Ok(Self(i32::from(value)))
    }

    #[cfg(feature = "sqlx")]
    pub fn from_i32(value: i32) -> Result<Self, TypeError> {
        match u16::try_from(value) {
            Ok(value) if value != 0 => Self::new(value),
            _ => Err(Self::error(format!("got {value}"))),
        }
    }

    pub fn get(self) -> u16 {
        match u16::try_from(self.0) {
            Ok(value) if value != 0 => value,
            _ => panic!("Port contains an invalid value"),
        }
    }

    fn error(reason: impl Into<String>) -> TypeError {
        TypeError::new(
            "Port",
            format!("port must be in 1..=65535, {}", reason.into()),
            "provide a valid TCP or UDP port",
        )
    }
}

/// UUID version a checked UUID enforces at construction
#[cfg(feature = "id")]
pub trait UuidVersionRule {
    fn type_name() -> &'static str;
    fn version() -> Version;
    fn version_name() -> &'static str;
}

/// UUID validated on construction against version rule `R`
#[cfg(feature = "id")]
#[cfg_attr(
    feature = "wire",
    derive(rkyv::Archive, rkyv::Serialize, rkyv::Deserialize)
)]
pub struct CheckedUuid<R> {
    value: Uuid,
    #[cfg_attr(feature = "wire", rkyv(omit_bounds))]
    marker: PhantomData<fn() -> R>,
}

#[cfg(feature = "id")]
impl<R> CheckedUuid<R> {
    pub fn as_uuid(&self) -> &Uuid {
        &self.value
    }

    pub fn into_uuid(self) -> Uuid {
        self.value
    }

    pub fn from_valid(value: Uuid) -> Self {
        Self {
            value,
            marker: PhantomData,
        }
    }
}

#[cfg(feature = "id")]
impl<R: UuidVersionRule> CheckedUuid<R> {
    pub fn new(value: Uuid) -> Result<Self, TypeError> {
        if value.get_version() == Some(R::version()) {
            return Ok(Self::from_valid(value));
        }
        Err(TypeError::new(
            R::type_name(),
            format!(
                "expected UUID {}, got UUID v{}",
                R::version_name(),
                value.get_version_num()
            ),
            format!("provide a UUID {}", R::version_name()),
        ))
    }
}

#[cfg(feature = "id")]
impl<R> Clone for CheckedUuid<R> {
    fn clone(&self) -> Self {
        *self
    }
}

#[cfg(feature = "id")]
impl<R> Copy for CheckedUuid<R> {}

#[cfg(feature = "id")]
impl<R> fmt::Debug for CheckedUuid<R> {
    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
        self.value.fmt(f)
    }
}

#[cfg(feature = "id")]
impl<R> fmt::Display for CheckedUuid<R> {
    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
        self.value.fmt(f)
    }
}

#[cfg(feature = "id")]
impl<R> PartialEq for CheckedUuid<R> {
    fn eq(&self, other: &Self) -> bool {
        self.value == other.value
    }
}

#[cfg(feature = "id")]
impl<R> Eq for CheckedUuid<R> {}

#[cfg(feature = "id")]
impl<R> PartialOrd for CheckedUuid<R> {
    fn partial_cmp(&self, other: &Self) -> Option<Ordering> {
        Some(self.cmp(other))
    }
}

#[cfg(feature = "id")]
impl<R> Ord for CheckedUuid<R> {
    fn cmp(&self, other: &Self) -> Ordering {
        self.value.cmp(&other.value)
    }
}

#[cfg(feature = "id")]
impl<R> Hash for CheckedUuid<R> {
    fn hash<H: Hasher>(&self, state: &mut H) {
        self.value.hash(state);
    }
}

#[cfg(all(feature = "id", feature = "sqlx"))]
impl<R, DB> sqlx::Type<DB> for CheckedUuid<R>
where
    R: UuidVersionRule,
    DB: sqlx::Database,
    Uuid: sqlx::Type<DB>,
{
    fn type_info() -> DB::TypeInfo {
        <Uuid as sqlx::Type<DB>>::type_info()
    }

    fn compatible(ty: &DB::TypeInfo) -> bool {
        <Uuid as sqlx::Type<DB>>::compatible(ty)
    }
}

#[cfg(all(feature = "id", feature = "sqlx"))]
impl<'q, R, DB> sqlx::Encode<'q, DB> for CheckedUuid<R>
where
    R: UuidVersionRule,
    DB: sqlx::Database,
    Uuid: sqlx::Encode<'q, DB>,
{
    fn encode_by_ref(
        &self,
        buf: &mut <DB as sqlx::Database>::ArgumentBuffer<'q>,
    ) -> Result<sqlx::encode::IsNull, sqlx::error::BoxDynError> {
        <Uuid as sqlx::Encode<'q, DB>>::encode_by_ref(&self.value, buf)
    }

    fn produces(&self) -> Option<DB::TypeInfo> {
        <Uuid as sqlx::Encode<'q, DB>>::produces(&self.value)
    }

    fn size_hint(&self) -> usize {
        <Uuid as sqlx::Encode<'q, DB>>::size_hint(&self.value)
    }
}

#[cfg(all(feature = "id", feature = "sqlx"))]
impl<'r, R, DB> sqlx::Decode<'r, DB> for CheckedUuid<R>
where
    R: UuidVersionRule,
    DB: sqlx::Database,
    Uuid: sqlx::Decode<'r, DB>,
{
    fn decode(
        value: <DB as sqlx::Database>::ValueRef<'r>,
    ) -> Result<Self, sqlx::error::BoxDynError> {
        let value = <Uuid as sqlx::Decode<'r, DB>>::decode(value)?;
        Ok(Self::new(value)?)
    }
}

impl<R> CheckedString<R> {
    pub fn from_valid(value: String) -> Self {
        Self {
            value,
            marker: PhantomData,
        }
    }

    pub fn as_str(&self) -> &str {
        &self.value
    }

    pub fn into_string(self) -> String {
        self.value
    }
}

impl<R: CheckedStringRule> CheckedString<R> {
    pub fn new(value: impl Into<String>) -> Result<Self, TypeError> {
        R::check(value.into()).map(Self::from_valid)
    }
}

impl<R> Clone for CheckedString<R> {
    fn clone(&self) -> Self {
        Self::from_valid(self.value.clone())
    }
}

impl<R> fmt::Debug for CheckedString<R> {
    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
        self.value.fmt(f)
    }
}

impl<R> PartialEq for CheckedString<R> {
    fn eq(&self, other: &Self) -> bool {
        self.value == other.value
    }
}

impl<R> Eq for CheckedString<R> {}

impl<R> PartialOrd for CheckedString<R> {
    fn partial_cmp(&self, other: &Self) -> Option<Ordering> {
        Some(self.cmp(other))
    }
}

impl<R> Ord for CheckedString<R> {
    fn cmp(&self, other: &Self) -> Ordering {
        self.value.cmp(&other.value)
    }
}

impl<R> Hash for CheckedString<R> {
    fn hash<H: Hasher>(&self, state: &mut H) {
        self.value.hash(state);
    }
}

#[cfg(feature = "sqlx")]
impl<R, DB> sqlx::Type<DB> for CheckedString<R>
where
    R: CheckedStringRule,
    DB: sqlx::Database,
    String: sqlx::Type<DB>,
{
    fn type_info() -> DB::TypeInfo {
        <String as sqlx::Type<DB>>::type_info()
    }

    fn compatible(ty: &DB::TypeInfo) -> bool {
        <String as sqlx::Type<DB>>::compatible(ty)
    }
}

#[cfg(feature = "sqlx")]
impl<'q, R, DB> sqlx::Encode<'q, DB> for CheckedString<R>
where
    R: CheckedStringRule,
    DB: sqlx::Database,
    String: sqlx::Encode<'q, DB>,
{
    fn encode_by_ref(
        &self,
        buf: &mut <DB as sqlx::Database>::ArgumentBuffer<'q>,
    ) -> Result<sqlx::encode::IsNull, sqlx::error::BoxDynError> {
        <String as sqlx::Encode<'q, DB>>::encode_by_ref(&self.value, buf)
    }

    fn produces(&self) -> Option<DB::TypeInfo> {
        <String as sqlx::Encode<'q, DB>>::produces(&self.value)
    }

    fn size_hint(&self) -> usize {
        <String as sqlx::Encode<'q, DB>>::size_hint(&self.value)
    }
}

#[cfg(feature = "sqlx")]
impl<'r, R, DB> sqlx::Decode<'r, DB> for CheckedString<R>
where
    R: CheckedStringRule,
    DB: sqlx::Database,
    String: sqlx::Decode<'r, DB>,
{
    fn decode(
        value: <DB as sqlx::Database>::ValueRef<'r>,
    ) -> Result<Self, sqlx::error::BoxDynError> {
        let value = <String as sqlx::Decode<'r, DB>>::decode(value)?;
        Ok(Self::new(value)?)
    }
}

#[cfg(all(feature = "net", feature = "sqlx"))]
impl<DB> sqlx::Type<DB> for CheckedPort
where
    DB: sqlx::Database,
    i32: sqlx::Type<DB>,
{
    fn type_info() -> DB::TypeInfo {
        <i32 as sqlx::Type<DB>>::type_info()
    }

    fn compatible(ty: &DB::TypeInfo) -> bool {
        <i32 as sqlx::Type<DB>>::compatible(ty)
    }
}

#[cfg(all(feature = "net", feature = "sqlx"))]
impl<'q, DB> sqlx::Encode<'q, DB> for CheckedPort
where
    DB: sqlx::Database,
    i32: sqlx::Encode<'q, DB>,
{
    fn encode_by_ref(
        &self,
        buf: &mut <DB as sqlx::Database>::ArgumentBuffer<'q>,
    ) -> Result<sqlx::encode::IsNull, sqlx::error::BoxDynError> {
        <i32 as sqlx::Encode<'q, DB>>::encode_by_ref(&self.0, buf)
    }

    fn produces(&self) -> Option<DB::TypeInfo> {
        <i32 as sqlx::Encode<'q, DB>>::produces(&self.0)
    }

    fn size_hint(&self) -> usize {
        <i32 as sqlx::Encode<'q, DB>>::size_hint(&self.0)
    }
}

#[cfg(all(feature = "net", feature = "sqlx"))]
impl<'r, DB> sqlx::Decode<'r, DB> for CheckedPort
where
    DB: sqlx::Database,
    i32: sqlx::Decode<'r, DB>,
{
    fn decode(
        value: <DB as sqlx::Database>::ValueRef<'r>,
    ) -> Result<Self, sqlx::error::BoxDynError> {
        let value = <i32 as sqlx::Decode<'r, DB>>::decode(value)?;
        Ok(Self::from_i32(value)?)
    }
}
