//! HTTP wire contracts
//!
//! Catalogs generate concrete endpoints and route specs from one versioned
//! prefix. REST catalogs accept `Unary` and `ServerStream` RPC modes.
//!
//! | Group | Types |
//! | --- | --- |
//! | Routing | [`HttpMethod`], [`HttpEndpoint`], [`HttpRouteSpec`] |
//! | Paths | [`HttpPath`], [`HttpRoutePattern`] |
//! | Query | [`HttpQueryKey`], [`HttpQueryValue`], [`HttpQueryString`], [`HttpQueryBuilder`] |
//! | Headers | [`HttpHeaderName`], [`HttpHeaderValue`] |
//! | Response | [`HttpStatus`], [`MediaType`] |

use std::fmt;
use std::marker::PhantomData;
use std::str::FromStr;

use efdn_types::TypeError;
use serde::de;
use serde::{Deserialize, Deserializer, Serialize, Serializer};

mod validate;

use validate::{
    encode, validate_header_name, validate_header_value, validate_media_type, validate_path,
    validate_query_key, validate_query_string, validate_query_value, validate_route_pattern,
};

#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash, PartialOrd, Ord)]
pub enum HttpMethod {
    Get,
    Post,
    Put,
    Patch,
    Delete,
    Head,
    Options,
}

impl HttpMethod {
    pub const GET: Self = Self::Get;
    pub const POST: Self = Self::Post;
    pub const PUT: Self = Self::Put;
    pub const PATCH: Self = Self::Patch;
    pub const DELETE: Self = Self::Delete;
    pub const HEAD: Self = Self::Head;
    pub const OPTIONS: Self = Self::Options;

    pub const fn as_str(self) -> &'static str {
        match self {
            Self::Get => "GET",
            Self::Post => "POST",
            Self::Put => "PUT",
            Self::Patch => "PATCH",
            Self::Delete => "DELETE",
            Self::Head => "HEAD",
            Self::Options => "OPTIONS",
        }
    }

    pub const fn is_safe(self) -> bool {
        matches!(self, Self::Get | Self::Head | Self::Options)
    }

    pub const fn is_idempotent(self) -> bool {
        matches!(
            self,
            Self::Get | Self::Put | Self::Delete | Self::Head | Self::Options
        )
    }
}

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

impl FromStr for HttpMethod {
    type Err = TypeError;

    fn from_str(value: &str) -> Result<Self, Self::Err> {
        match value {
            "GET" => Ok(Self::Get),
            "POST" => Ok(Self::Post),
            "PUT" => Ok(Self::Put),
            "PATCH" => Ok(Self::Patch),
            "DELETE" => Ok(Self::Delete),
            "HEAD" => Ok(Self::Head),
            "OPTIONS" => Ok(Self::Options),
            _ => Err(TypeError::new(
                "HttpMethod",
                format!("unsupported HTTP method `{value}`"),
                "provide GET, POST, PUT, PATCH, DELETE, HEAD, or OPTIONS",
            )),
        }
    }
}

impl Serialize for HttpMethod {
    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 HttpMethod {
    fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
    where
        D: Deserializer<'de>,
    {
        let value = String::deserialize(deserializer)?;
        value.parse().map_err(de::Error::custom)
    }
}

#[doc(hidden)]
pub trait HttpTextKind {
    const NAME: &'static str;
    fn validate(value: &str) -> Result<(), TypeError>;
}

#[doc(hidden)]
#[derive(Clone, Debug, PartialEq, Eq, Hash, PartialOrd, Ord)]
pub struct HttpText<K: HttpTextKind> {
    value: String,
    marker: PhantomData<K>,
}

#[doc(hidden)]
#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash, PartialOrd, Ord)]
pub struct HttpPathKind;

#[doc(hidden)]
#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash, PartialOrd, Ord)]
pub struct HttpRoutePatternKind;

#[doc(hidden)]
#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash, PartialOrd, Ord)]
pub struct HttpQueryKeyKind;

#[doc(hidden)]
#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash, PartialOrd, Ord)]
pub struct HttpQueryValueKind;

#[doc(hidden)]
#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash, PartialOrd, Ord)]
pub struct HttpQueryStringKind;

#[doc(hidden)]
#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash, PartialOrd, Ord)]
pub struct HttpHeaderNameKind;

#[doc(hidden)]
#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash, PartialOrd, Ord)]
pub struct HttpHeaderValueKind;

#[doc(hidden)]
#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash, PartialOrd, Ord)]
pub struct MediaTypeKind;

pub type HttpPath = HttpText<HttpPathKind>;
pub type HttpRoutePattern = HttpText<HttpRoutePatternKind>;
pub type HttpQueryKey = HttpText<HttpQueryKeyKind>;
pub type HttpQueryValue = HttpText<HttpQueryValueKind>;
pub type HttpQueryString = HttpText<HttpQueryStringKind>;
pub type HttpHeaderName = HttpText<HttpHeaderNameKind>;
pub type HttpHeaderValue = HttpText<HttpHeaderValueKind>;
pub type MediaType = HttpText<MediaTypeKind>;

impl<K: HttpTextKind> HttpText<K> {
    pub fn new(value: impl Into<String>) -> Result<Self, TypeError> {
        let value = value.into();
        K::validate(&value)?;
        Ok(Self {
            value,
            marker: PhantomData,
        })
    }

    pub fn from_static(value: &'static str) -> Self {
        match Self::new(value) {
            Ok(value) => value,
            Err(error) => panic!("{error}"),
        }
    }

    pub fn from_generated(value: String, context: &'static str) -> Self {
        match Self::new(value) {
            Ok(value) => value,
            Err(error) => panic!("generated HTTP value is invalid\ncontext: {context}\n{error}"),
        }
    }

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

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

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

impl<K: HttpTextKind> FromStr for HttpText<K> {
    type Err = TypeError;

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

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

impl<'de, K: HttpTextKind> Deserialize<'de> for HttpText<K> {
    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)
    }
}

impl HttpTextKind for HttpPathKind {
    const NAME: &'static str = "HttpPath";

    fn validate(value: &str) -> Result<(), TypeError> {
        validate_path(value, Self::NAME)
    }
}

impl HttpTextKind for HttpRoutePatternKind {
    const NAME: &'static str = "HttpRoutePattern";

    fn validate(value: &str) -> Result<(), TypeError> {
        validate_route_pattern(value, Self::NAME)
    }
}

impl HttpTextKind for HttpQueryKeyKind {
    const NAME: &'static str = "HttpQueryKey";

    fn validate(value: &str) -> Result<(), TypeError> {
        validate_query_key(value, Self::NAME)
    }
}

impl HttpTextKind for HttpQueryValueKind {
    const NAME: &'static str = "HttpQueryValue";

    fn validate(value: &str) -> Result<(), TypeError> {
        validate_query_value(value, Self::NAME)
    }
}

impl HttpTextKind for HttpQueryStringKind {
    const NAME: &'static str = "HttpQueryString";

    fn validate(value: &str) -> Result<(), TypeError> {
        validate_query_string(value, Self::NAME)
    }
}

impl HttpTextKind for HttpHeaderNameKind {
    const NAME: &'static str = "HttpHeaderName";

    fn validate(value: &str) -> Result<(), TypeError> {
        validate_header_name(value, Self::NAME)
    }
}

impl HttpTextKind for HttpHeaderValueKind {
    const NAME: &'static str = "HttpHeaderValue";

    fn validate(value: &str) -> Result<(), TypeError> {
        validate_header_value(value, Self::NAME)
    }
}

impl HttpTextKind for MediaTypeKind {
    const NAME: &'static str = "MediaType";

    fn validate(value: &str) -> Result<(), TypeError> {
        validate_media_type(value, Self::NAME)
    }
}

#[derive(Clone, Debug, PartialEq, Eq)]
pub struct HttpEndpoint<M> {
    method: HttpMethod,
    path: HttpPath,
    marker: PhantomData<M>,
}

impl<M> HttpEndpoint<M> {
    pub fn new(method: HttpMethod, path: HttpPath) -> Self {
        Self {
            method,
            path,
            marker: PhantomData,
        }
    }

    pub const fn method(&self) -> HttpMethod {
        self.method
    }

    pub const fn path(&self) -> &HttpPath {
        &self.path
    }
}

#[derive(Clone, Debug, PartialEq, Eq)]
pub struct HttpRouteSpec<M> {
    method: HttpMethod,
    pattern: HttpRoutePattern,
    marker: PhantomData<M>,
}

impl<M> HttpRouteSpec<M> {
    pub fn new(method: HttpMethod, pattern: HttpRoutePattern) -> Self {
        Self {
            method,
            pattern,
            marker: PhantomData,
        }
    }

    pub const fn method(&self) -> HttpMethod {
        self.method
    }

    pub const fn pattern(&self) -> &HttpRoutePattern {
        &self.pattern
    }
}

#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash, PartialOrd, Ord, Serialize, Deserialize)]
pub struct HttpStatus(u16);

impl HttpStatus {
    pub fn new(value: u16) -> Result<Self, TypeError> {
        if (100..=999).contains(&value) {
            return Ok(Self(value));
        }
        Err(TypeError::new(
            "HttpStatus",
            format!("status code must be in 100..=999, got {value}"),
            "provide a valid HTTP status code",
        ))
    }

    pub const fn get(self) -> u16 {
        self.0
    }
}

#[derive(Clone, Debug, Default, PartialEq, Eq)]
pub struct HttpQueryBuilder(Vec<(HttpQueryKey, HttpQueryValue)>);

impl HttpQueryBuilder {
    pub fn new() -> Self {
        Self(Vec::new())
    }

    pub fn push(&mut self, key: HttpQueryKey, value: HttpQueryValue) {
        self.0.push((key, value));
    }

    pub fn finish(self) -> Result<HttpQueryString, TypeError> {
        let pairs = self
            .0
            .into_iter()
            .map(|(key, value)| format!("{}={}", encode(key.as_str()), encode(value.as_str())))
            .collect::<Vec<_>>();
        HttpQueryString::new(pairs.join("&"))
    }
}
