use std::fmt;
use std::str::FromStr;

use serde::de;
use serde::{Deserialize, Deserializer, Serialize, Serializer};
use url::Url as RawUrl;

use crate::TypeError;
use crate::checked::{CheckedString, CheckedStringRule};

macro_rules! impl_url_type {
    ($type:ty) => {
        impl $type {
            pub fn as_str(&self) -> &str {
                self.0.as_str()
            }

            pub fn to_url(&self) -> Result<RawUrl, TypeError> {
                parse_url(self.as_str(), stringify!($type))
            }

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

        impl FromStr for $type {
            type Err = TypeError;

            fn from_str(value: &str) -> Result<Self, Self::Err> {
                Self::new(value)
            }
        }

        impl fmt::Display for $type {
            fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
                f.write_str(self.as_str())
            }
        }

        impl Serialize for $type {
            fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
            where
                S: Serializer,
            {
                serializer.serialize_str(self.as_str())
            }
        }

        impl<'de> Deserialize<'de> for $type {
            fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
            where
                D: Deserializer<'de>,
            {
                let value = String::deserialize(deserializer)?;
                Self::new(value).map_err(de::Error::custom)
            }
        }
    };
}

#[derive(Clone, Debug, PartialEq, Eq, PartialOrd, Ord, Hash)]
#[cfg_attr(
    feature = "wire",
    derive(rkyv::Archive, rkyv::Serialize, rkyv::Deserialize)
)]
#[cfg_attr(feature = "sqlx", derive(sqlx::Type), sqlx(transparent, no_pg_array))]
pub struct Url(CheckedString<UrlRule>);

impl Url {
    pub fn new(value: impl AsRef<str>) -> Result<Self, TypeError> {
        CheckedString::new(value.as_ref().to_owned()).map(Self)
    }
}

impl From<RawUrl> for Url {
    fn from(value: RawUrl) -> Self {
        Self(CheckedString::from_valid(value.to_string()))
    }
}

impl_url_type!(Url);

#[derive(Clone, Debug, PartialEq, Eq, PartialOrd, Ord, Hash)]
#[cfg_attr(
    feature = "wire",
    derive(rkyv::Archive, rkyv::Serialize, rkyv::Deserialize)
)]
#[cfg_attr(feature = "sqlx", derive(sqlx::Type), sqlx(transparent, no_pg_array))]
pub struct HttpUrl(CheckedString<HttpUrlRule>);

impl HttpUrl {
    pub fn new(value: impl AsRef<str>) -> Result<Self, TypeError> {
        CheckedString::new(value.as_ref().to_owned()).map(Self)
    }
}

impl_url_type!(HttpUrl);

#[derive(Clone, Debug, PartialEq, Eq, PartialOrd, Ord, Hash)]
#[cfg_attr(
    feature = "wire",
    derive(rkyv::Archive, rkyv::Serialize, rkyv::Deserialize)
)]
#[cfg_attr(feature = "sqlx", derive(sqlx::Type), sqlx(transparent, no_pg_array))]
pub struct HttpsUrl(CheckedString<HttpsUrlRule>);

impl HttpsUrl {
    pub fn new(value: impl AsRef<str>) -> Result<Self, TypeError> {
        CheckedString::new(value.as_ref().to_owned()).map(Self)
    }
}

impl_url_type!(HttpsUrl);

#[derive(Clone, Debug, PartialEq, Eq, PartialOrd, Ord, Hash)]
#[cfg_attr(
    feature = "wire",
    derive(rkyv::Archive, rkyv::Serialize, rkyv::Deserialize)
)]
#[cfg_attr(feature = "sqlx", derive(sqlx::Type), sqlx(transparent, no_pg_array))]
pub struct Origin(CheckedString<OriginRule>);

impl Origin {
    pub fn new(value: impl AsRef<str>) -> Result<Self, TypeError> {
        CheckedString::new(value.as_ref().to_owned()).map(Self)
    }
}

impl_url_type!(Origin);

#[derive(Clone, Copy, Debug, PartialEq, Eq, PartialOrd, Ord, Hash)]
struct UrlRule;

impl CheckedStringRule for UrlRule {
    fn check(value: String) -> Result<String, TypeError> {
        parse_url(&value, "Url").map(|url| url.to_string())
    }
}

#[derive(Clone, Copy, Debug, PartialEq, Eq, PartialOrd, Ord, Hash)]
struct HttpUrlRule;

impl CheckedStringRule for HttpUrlRule {
    fn check(value: String) -> Result<String, TypeError> {
        let url = parse_url(&value, "HttpUrl")?;
        require_scheme(&url, "http", "HttpUrl")?;
        require_host(&url, "HttpUrl")?;
        Ok(url.to_string())
    }
}

#[derive(Clone, Copy, Debug, PartialEq, Eq, PartialOrd, Ord, Hash)]
struct HttpsUrlRule;

impl CheckedStringRule for HttpsUrlRule {
    fn check(value: String) -> Result<String, TypeError> {
        let url = parse_url(&value, "HttpsUrl")?;
        require_scheme(&url, "https", "HttpsUrl")?;
        require_host(&url, "HttpsUrl")?;
        Ok(url.to_string())
    }
}

#[derive(Clone, Copy, Debug, PartialEq, Eq, PartialOrd, Ord, Hash)]
struct OriginRule;

impl CheckedStringRule for OriginRule {
    fn check(value: String) -> Result<String, TypeError> {
        let mut url = parse_url(&value, "Origin")?;
        validate_origin(&url)?;
        url.set_path("");
        Ok(url.to_string())
    }
}

fn parse_url(value: &str, type_name: &'static str) -> Result<RawUrl, TypeError> {
    RawUrl::parse(value).map_err(|error| {
        TypeError::new(
            type_name,
            format!("invalid URL: {error}"),
            "provide an absolute URL like https://api.example.com",
        )
    })
}

fn require_scheme(url: &RawUrl, scheme: &str, type_name: &'static str) -> Result<(), TypeError> {
    if url.scheme() == scheme {
        return Ok(());
    }
    Err(TypeError::new(
        type_name,
        format!("expected {scheme} URL, got {}", url.scheme()),
        format!("provide a URL starting with {scheme}://"),
    ))
}

fn require_host(url: &RawUrl, type_name: &'static str) -> Result<(), TypeError> {
    if url.host_str().is_some() {
        return Ok(());
    }
    Err(TypeError::new(
        type_name,
        "URL must include a host",
        "provide a URL like https://api.example.com",
    ))
}

fn validate_origin(url: &RawUrl) -> Result<(), TypeError> {
    if !matches!(url.scheme(), "http" | "https") {
        return Err(TypeError::new(
            "Origin",
            format!("expected http or https origin, got {}", url.scheme()),
            "provide an origin like https://app.example.com",
        ));
    }
    require_host(url, "Origin")?;
    if url.query().is_none() && url.fragment().is_none() && matches!(url.path(), "" | "/") {
        return Ok(());
    }
    Err(TypeError::new(
        "Origin",
        "origin must not include a path, query, or fragment",
        "provide only scheme, host, and optional port",
    ))
}
