//! NATS wire contracts
//!
//! Catalogs generate subjects in this order:
//!
//! ```text
//! namespace.surface.version.scope_kind.scope_value.domain.action.arg_kind.arg_value
//! ```
//!
//! | Group | Types |
//! | --- | --- |
//! | Subjects | [`NatsSubject`], [`NatsSubjectFilter`], [`NatsToken`] |
//! | Endpoints | [`NatsUnary`], [`NatsServerStream`], [`NatsClientStream`], [`NatsBidiStream`], [`NatsEvent`] |
//! | Streaming | [`NatsStreamSubjects`] |
//! | Queue groups | [`NatsQueueGroup`] |
//! | Headers | [`NatsHeaderName`], [`NatsHeaderValue`] |
//! | JetStream | [`JetStreamStreamName`], [`JetStreamConsumerName`], [`JetStreamDurableName`], [`JetStreamWorkQueue`] |

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

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

pub mod endpoint;

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

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

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

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

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

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

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

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

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

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

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

pub type NatsSubject = NatsText<NatsSubjectKind>;
pub type NatsSubjectFilter = NatsText<NatsSubjectFilterKind>;
pub type NatsQueueGroup = NatsText<NatsQueueGroupKind>;
pub type NatsToken = NatsText<NatsTokenKind>;
pub type NatsHeaderName = NatsText<NatsHeaderNameKind>;
pub type NatsHeaderValue = NatsText<NatsHeaderValueKind>;
pub type JetStreamStreamName = NatsText<JetStreamStreamNameKind>;
pub type JetStreamConsumerName = NatsText<JetStreamConsumerNameKind>;
pub type JetStreamDurableName = NatsText<JetStreamDurableNameKind>;

impl<K: NatsTextKind> NatsText<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 NATS 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: NatsTextKind> fmt::Display for NatsText<K> {
    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
        f.write_str(self.as_str())
    }
}

impl<K: NatsTextKind> FromStr for NatsText<K> {
    type Err = TypeError;

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

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

impl<'de, K: NatsTextKind> Deserialize<'de> for NatsText<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 async_nats::ToSubject for NatsSubject {
    fn to_subject(&self) -> async_nats::Subject {
        async_nats::Subject::from(self.as_str())
    }
}

impl async_nats::ToSubject for &NatsSubject {
    fn to_subject(&self) -> async_nats::Subject {
        async_nats::Subject::from(self.as_str())
    }
}

impl async_nats::ToSubject for NatsSubjectFilter {
    fn to_subject(&self) -> async_nats::Subject {
        async_nats::Subject::from(self.as_str())
    }
}

impl async_nats::ToSubject for &NatsSubjectFilter {
    fn to_subject(&self) -> async_nats::Subject {
        async_nats::Subject::from(self.as_str())
    }
}

impl async_nats::header::IntoHeaderName for NatsHeaderName {
    fn into_header_name(self) -> async_nats::HeaderName {
        self.as_str()
            .parse()
            .expect("validated NATS header name must parse")
    }
}

impl async_nats::header::IntoHeaderName for &NatsHeaderName {
    fn into_header_name(self) -> async_nats::HeaderName {
        self.as_str()
            .parse()
            .expect("validated NATS header name must parse")
    }
}

impl async_nats::header::IntoHeaderValue for NatsHeaderValue {
    fn into_header_value(self) -> async_nats::HeaderValue {
        self.as_str()
            .parse()
            .expect("validated NATS header value must parse")
    }
}

impl async_nats::header::IntoHeaderValue for &NatsHeaderValue {
    fn into_header_value(self) -> async_nats::HeaderValue {
        self.as_str()
            .parse()
            .expect("validated NATS header value must parse")
    }
}

impl NatsTextKind for NatsSubjectKind {
    const NAME: &'static str = "NatsSubject";

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

impl NatsTextKind for NatsSubjectFilterKind {
    const NAME: &'static str = "NatsSubjectFilter";

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

impl NatsTextKind for NatsQueueGroupKind {
    const NAME: &'static str = "NatsQueueGroup";

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

impl NatsTextKind for NatsTokenKind {
    const NAME: &'static str = "NatsToken";

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

impl NatsTextKind for NatsHeaderNameKind {
    const NAME: &'static str = "NatsHeaderName";

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

impl NatsTextKind for NatsHeaderValueKind {
    const NAME: &'static str = "NatsHeaderValue";

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

impl NatsTextKind for JetStreamStreamNameKind {
    const NAME: &'static str = "JetStreamStreamName";

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

impl NatsTextKind for JetStreamConsumerNameKind {
    const NAME: &'static str = "JetStreamConsumerName";

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

impl NatsTextKind for JetStreamDurableNameKind {
    const NAME: &'static str = "JetStreamDurableName";

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

#[derive(Clone, Debug, PartialEq, Eq)]
pub struct JetStreamWorkQueue {
    stream: JetStreamStreamName,
    durable: JetStreamDurableName,
    filter: NatsSubjectFilter,
}

impl JetStreamWorkQueue {
    pub fn new(
        stream: JetStreamStreamName,
        durable: JetStreamDurableName,
        filter: NatsSubjectFilter,
    ) -> Self {
        Self {
            stream,
            durable,
            filter,
        }
    }

    pub const fn stream(&self) -> &JetStreamStreamName {
        &self.stream
    }

    pub const fn durable(&self) -> &JetStreamDurableName {
        &self.durable
    }

    pub const fn filter(&self) -> &NatsSubjectFilter {
        &self.filter
    }
}

fn validate_publish_subject(value: &str, type_name: &'static str) -> Result<(), TypeError> {
    validate_subject_tokens(value, type_name, false)
}

fn validate_subject_filter(value: &str, type_name: &'static str) -> Result<(), TypeError> {
    validate_subject_tokens(value, type_name, true)
}

fn validate_queue_group(value: &str, type_name: &'static str) -> Result<(), TypeError> {
    validate_subject_tokens(value, type_name, false)
}

fn validate_subject_tokens(
    value: &str,
    type_name: &'static str,
    allow_wildcards: bool,
) -> Result<(), TypeError> {
    if value.is_empty() {
        return Err(TypeError::empty(type_name));
    }
    let tokens = value.split('.').collect::<Vec<_>>();
    for (index, token) in tokens.iter().enumerate() {
        if *token == ">" && index + 1 != tokens.len() {
            return Err(TypeError::new(
                type_name,
                format!("`>` wildcard at segment {index} must be the last token"),
                "move `>` to the end of the filter",
            ));
        }
        validate_subject_token(token, type_name, allow_wildcards, index)?;
    }
    Ok(())
}

fn validate_subject_token(
    token: &str,
    type_name: &'static str,
    allow_wildcards: bool,
    index: usize,
) -> Result<(), TypeError> {
    if token.is_empty() {
        return Err(TypeError::new(
            type_name,
            format!("empty token at segment {index}"),
            "remove the extra dot or provide a token",
        ));
    }
    if matches!(token, "*" | ">") {
        return validate_wildcard(token, type_name, allow_wildcards, index);
    }
    validate_plain_token(token, type_name)
}

fn validate_wildcard(
    token: &str,
    type_name: &'static str,
    allow_wildcards: bool,
    index: usize,
) -> Result<(), TypeError> {
    if allow_wildcards && token == "*" {
        return Ok(());
    }
    if allow_wildcards && token == ">" {
        return Ok(());
    }
    Err(TypeError::new(
        type_name,
        format!("wildcard `{token}` is not allowed at segment {index}"),
        "use NatsSubjectFilter for wildcard subscriptions",
    ))
}

fn validate_plain_token(value: &str, type_name: &'static str) -> Result<(), TypeError> {
    if value.is_empty() {
        return Err(TypeError::empty(type_name));
    }
    if value
        .bytes()
        .all(|byte| byte.is_ascii_alphanumeric() || matches!(byte, b'_' | b'-'))
    {
        return Ok(());
    }
    Err(TypeError::new(
        type_name,
        "token must use ASCII letters, numbers, `_`, or `-`",
        "provide a token like shell-node or instance_1",
    ))
}

fn validate_header_name(value: &str, type_name: &'static str) -> Result<(), TypeError> {
    if value.is_empty() {
        return Err(TypeError::empty(type_name));
    }
    if value
        .bytes()
        .all(|byte| byte.is_ascii_alphanumeric() || matches!(byte, b'-' | b'_'))
    {
        return Ok(());
    }
    Err(TypeError::new(
        type_name,
        "header name contains an unsupported character",
        "provide a header name like Nats-Msg-Id",
    ))
}

fn validate_header_value(value: &str, type_name: &'static str) -> Result<(), TypeError> {
    if value.chars().any(char::is_control) {
        return Err(TypeError::new(
            type_name,
            "header value contains a control character",
            "provide a printable header value",
        ));
    }
    Ok(())
}

fn validate_jetstream_name(value: &str, type_name: &'static str) -> Result<(), TypeError> {
    if value.is_empty() {
        return Err(TypeError::empty(type_name));
    }
    if value.len() > 64 {
        return Err(TypeError::new(
            type_name,
            "JetStream name must be at most 64 bytes",
            "provide a short filesystem-safe name",
        ));
    }
    validate_plain_token(value, type_name)
}
