//! HTTP [Route] configuration. [Route]s dynamically congfigure things you might
//! put directly in client code like timeouts and retries, failure detection, or
//! picking a different backend based on request data.

use std::{cmp::Ordering, collections::BTreeMap, str::FromStr};

use crate::{
    backend::BackendId,
    shared::{Duration, Fraction, Regex},
    Hostname, Name, Service,
};
use serde::{Deserialize, Serialize};

#[cfg(feature = "typeinfo")]
use junction_typeinfo::TypeInfo;

#[doc(hidden)]
pub mod tags {
    //! Well known tags for Routes.

    /// Marks a Route as generated by an automated process and NOT authored by a
    /// human being. Any Route with this tag will have lower priority than a
    /// Route authored by a human.
    ///
    /// The value of this tag should be a process or application ID that
    /// indicates where the route came from and what generated it.
    pub const GENERATED_BY: &str = "junctionlabs.io/generated-by";
}

/// A matcher for URL hostnames.
#[derive(Clone, Debug, Serialize, Deserialize, PartialEq, Eq, PartialOrd, Ord)]
#[serde(try_from = "String", into = "String")]
pub enum HostnameMatch {
    /// Matches any valid subdomain of this hostname.
    ///
    /// ```rust
    /// # use junction_api::http::HostnameMatch;
    /// # use std::str::FromStr;
    ///
    /// let matcher = HostnameMatch::from_str("*.foo.example").unwrap();
    ///
    /// assert!(matcher.matches_str("bar.foo.example"));
    ///
    /// assert!(!matcher.matches_str("foo.example"));
    /// assert!(!matcher.matches_str("barfoo.example"));
    /// ```
    Subdomain(Hostname),

    /// An exact match for a hostname.
    Exact(Hostname),
}

impl HostnameMatch {
    /// Returns true if hostname is matched by this matcher.
    pub fn matches(&self, hostname: &Hostname) -> bool {
        self.matches_str_validated(hostname)
    }

    /// Returns true if the string s is a valid hostname and matches this pattern.
    pub fn matches_str(&self, s: &str) -> bool {
        if Hostname::validate(s.as_bytes()).is_err() {
            return false;
        }
        self.matches_str_validated(s)
    }

    fn matches_str_validated(&self, s: &str) -> bool {
        match self {
            HostnameMatch::Subdomain(d) => {
                let (subdomain, domain) = s.split_at(s.len() - d.len());
                domain == &d[..] && subdomain.ends_with('.')
            }
            HostnameMatch::Exact(e) => s == e.as_ref(),
        }
    }
}

#[cfg(feature = "typeinfo")]
impl junction_typeinfo::TypeInfo for HostnameMatch {
    fn kind() -> junction_typeinfo::Kind {
        junction_typeinfo::Kind::String
    }
}

impl From<Hostname> for HostnameMatch {
    fn from(hostname: Hostname) -> Self {
        Self::Exact(hostname)
    }
}

impl std::fmt::Display for HostnameMatch {
    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
        match self {
            HostnameMatch::Subdomain(hostname) => write!(f, "*.{hostname}"),
            HostnameMatch::Exact(hostname) => f.write_str(hostname),
        }
    }
}

impl FromStr for HostnameMatch {
    type Err = crate::Error;

    fn from_str(s: &str) -> Result<Self, Self::Err> {
        Ok(match s.strip_prefix("*.") {
            Some(hostname) => Self::Subdomain(Hostname::from_str(hostname)?),
            None => Self::Exact(Hostname::from_str(s)?),
        })
    }
}

// implemented so we can use serde(try_from = "String")
impl TryFrom<String> for HostnameMatch {
    type Error = crate::Error;

    fn try_from(s: String) -> Result<Self, Self::Error> {
        Ok(match s.strip_prefix("*.") {
            // if there's a prefix match, use FromStr. copying and tossing
            // the allocated String is probably just as fine as removing the
            // first two chars and doing a memcopy.
            Some(hostname) => Self::Subdomain(Hostname::from_str(hostname)?),
            // if this is an exact match, use Hostname::try_from which
            // can move the value into the Hostname
            None => Self::Exact(Hostname::try_from(s)?),
        })
    }
}
// implemented so we can use serde(into = "String")
impl From<HostnameMatch> for String {
    fn from(value: HostnameMatch) -> Self {
        match value {
            HostnameMatch::Subdomain(_) => value.to_string(),
            HostnameMatch::Exact(inner) => inner.0.to_string(),
        }
    }
}

/// A Route is a policy that describes how a request to a specific virtual
/// host should be routed.
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
#[serde(deny_unknown_fields)]
#[cfg_attr(feature = "typeinfo", derive(TypeInfo))]
pub struct Route {
    /// A globally unique identifier for this Route.
    ///
    /// Route IDs must be valid RFC 1035 DNS label names - they must start with
    /// a lowercase ascii character, and can only contain lowercase ascii
    /// alphanumeric characters and the `-` character.
    pub id: Name,

    /// A list of arbitrary tags that can be added to a Route.
    #[serde(default)]
    // TODO: limit this a-la kube annotation keys/values.
    pub tags: BTreeMap<String, String>,

    /// The hostnames that match this Route.
    #[serde(default)]
    pub hostnames: Vec<HostnameMatch>,

    /// The ports that match this Route.
    #[serde(default)]
    pub ports: Vec<u16>,

    /// The rules that determine whether a request matches and where traffic
    /// should be routed.
    #[serde(default)]
    pub rules: Vec<RouteRule>,
}

impl Route {
    /// Create a trivial route that passes all traffic for a target directly to
    /// the given Service. The request port will be used to identify a
    /// specific backend at request time.
    pub fn passthrough_route(id: Name, service: Service) -> Route {
        Route {
            id,
            hostnames: vec![service.hostname().into()],
            ports: vec![],
            tags: Default::default(),
            rules: vec![RouteRule {
                matches: vec![RouteMatch {
                    path: Some(PathMatch::empty_prefix()),
                    ..Default::default()
                }],
                backends: vec![BackendRef {
                    service,
                    port: None,
                    weight: 1,
                }],
                ..Default::default()
            }],
        }
    }
}

/// A RouteRule contains a set of matches that define which requests it applies
/// to, processing rules, and the final destination(s) for matching traffic.
///
/// See the Junction docs for a high level description of how Routes and
/// RouteRules behave.
///
/// # Ordering Rules
///
/// Route rules may be ordered by comparing their maximum
/// [matches][Self::matches], breaking ties by comparing their next-highest
/// match. This provides a total ordering on rules. Note that having a sorted
/// list of rules does not mean that the list of all matches across all rules is
/// totally sorted.
///
/// This ordering is provided for convenience - clients match rules in the order
/// they're listed in a Route.
#[derive(Clone, Debug, Default, Serialize, Deserialize, PartialEq, Eq)]
#[serde(deny_unknown_fields)]
#[cfg_attr(feature = "typeinfo", derive(TypeInfo))]
pub struct RouteRule {
    /// A human-readable name for this rule.
    ///
    /// This name is completely optional, and will only be used in diagnostics
    /// to make it easier to debug. Diagnostics that don't have a name will be
    /// referred to by their index in a Route's list of rules.
    #[serde(default, skip_serializing_if = "Option::is_none")]
    pub name: Option<Name>,

    /// A list of match rules applied to an outgoing request.  Each match is
    /// independent; this rule will be matched if **any** of the listed matches
    /// is satisfied.
    ///
    /// If no matches are specified, this Rule matches any outgoing request.
    #[serde(default, skip_serializing_if = "Vec::is_empty")]
    pub matches: Vec<RouteMatch>,

    /// Define the filters that are applied to requests that match this rule.
    ///
    /// The effects of ordering of multiple behaviors are currently unspecified.
    ///
    /// Specifying the same filter multiple times is not supported unless
    /// explicitly indicated in the filter.
    ///
    /// All filters are compatible with each other except for the URLRewrite and
    /// RequestRedirect filters, which may not be combined.
    #[serde(default, skip_serializing_if = "Vec::is_empty")]
    #[doc(hidden)]
    pub filters: Vec<RouteFilter>,

    // The timeouts set on any request that matches route.
    #[serde(default, skip_serializing_if = "Option::is_none")]
    pub timeouts: Option<RouteTimeouts>,

    /// How to retry requests. If not specified, requests are not retried.
    #[serde(default, skip_serializing_if = "Option::is_none")]
    pub retry: Option<RouteRetry>,

    /// Where the traffic should route if this rule matches.
    ///
    /// If no backends are specified, this route becomes a black hole for
    /// traffic and all matching requests return an error.
    #[serde(default)]
    pub backends: Vec<BackendRef>,
}

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

impl Ord for RouteRule {
    fn cmp(&self, other: &Self) -> Ordering {
        let mut self_matches: Vec<_> = self.matches.iter().collect();
        self_matches.sort();
        let mut self_matches = self_matches.iter().rev();

        let mut other_matches: Vec<_> = other.matches.iter().collect();
        other_matches.sort();
        let mut other_matches = other_matches.iter().rev();

        loop {
            match (self_matches.next(), other_matches.next()) {
                (None, None) => return Ordering::Equal,
                (None, Some(_)) => return Ordering::Less,
                (Some(_), None) => return Ordering::Greater,
                (Some(a), Some(b)) => match a.cmp(b) {
                    Ordering::Equal => {}
                    ord => return ord,
                },
            }
        }
    }
}

/// Defines timeouts that can be configured for a HTTP Route.
#[derive(Serialize, Deserialize, Clone, Debug, PartialEq, Eq)]
#[serde(deny_unknown_fields)]
#[cfg_attr(feature = "typeinfo", derive(TypeInfo))]
pub struct RouteTimeouts {
    /// Specifies the maximum duration for a HTTP request. This timeout is
    /// intended to cover as close to the whole request-response transaction as
    /// possible.
    ///
    /// An entire client HTTP transaction may result in more than one call to
    /// destination backends, for example, if automatic retries are configured.
    #[serde(default, skip_serializing_if = "Option::is_none")]
    pub request: Option<Duration>,

    /// Specifies a timeout for an individual request to a backend. This covers
    /// the time from when the request first starts being sent to when the full
    /// response has been received from the backend.
    ///
    /// Because the overall request timeout encompasses the backend request
    /// timeout, the value of this timeout must be less than or equal to the
    /// value of the overall timeout.
    #[serde(
        default,
        skip_serializing_if = "Option::is_none",
        alias = "backendRequest"
    )]
    pub backend_request: Option<Duration>,
}

/// Defines the predicate used to match requests to a given action. Multiple
/// match types are ANDed together; the match will evaluate to true only if all
/// conditions are satisfied. For example, if a match specifies a `path` match
/// and two `query_params` matches, it will match only if the request's path
/// matches and both of the `query_params` are matches.
///
/// The default RouteMatch functions like a path match on the empty prefix,
/// which matches every request.
///
/// ## Match Ordering
///
/// Route matches are [Ordered][Ord] to help break ties. While once a Route is
/// constructed, rules are matched in-order, sorting matches and rules can be
/// useful while constructing a Route in case there isn't an obvious order
/// matches should apply in. From highest-value to lowest value, routes are
/// ordered by:
///
/// - "Exact" path match.
/// - "Prefix" path match with largest number of characters.
/// - Method match.
/// - Largest number of header matches.
/// - Largest number of query param matches.
///
/// Note that this means a route that matches on a path is greater than a route
/// that only matches on headers. To get a natural sort order by precedence, you
/// may want to reverse-sort a list of matches.
#[derive(Clone, Debug, Default, Serialize, Deserialize, PartialEq, Eq)]
#[serde(deny_unknown_fields)]
#[cfg_attr(feature = "typeinfo", derive(TypeInfo))]
pub struct RouteMatch {
    /// Specifies a HTTP request path matcher.
    #[serde(default, skip_serializing_if = "Option::is_none")]
    pub path: Option<PathMatch>,

    /// Specifies HTTP request header matchers. Multiple match values are ANDed
    /// together, meaning, a request must match all the specified headers.
    #[serde(default, skip_serializing_if = "Vec::is_empty")]
    pub headers: Vec<HeaderMatch>,

    /// Specifies HTTP query parameter matchers. Multiple match values are ANDed
    /// together, meaning, a request must match all the specified query
    /// parameters.
    #[serde(default, skip_serializing_if = "Vec::is_empty", alias = "queryParams")]
    pub query_params: Vec<QueryParamMatch>,

    /// Specifies HTTP method matcher. When specified, this route will be
    /// matched only if the request has the specified method.
    #[serde(default, skip_serializing_if = "Option::is_none")]
    pub method: Option<Method>,
}

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

impl Ord for RouteMatch {
    fn cmp(&self, other: &Self) -> Ordering {
        match self.path.cmp(&other.path) {
            Ordering::Equal => (),
            cmp => return cmp,
        }

        match (&self.method, &other.method) {
            (None, Some(_)) => return Ordering::Less,
            (Some(_), None) => return Ordering::Greater,
            _ => (),
        }

        match self.headers.len().cmp(&other.headers.len()) {
            Ordering::Equal => (),
            cmp => return cmp,
        }

        self.query_params.len().cmp(&other.query_params.len())
    }
}

/// Describes how to select a HTTP route by matching the HTTP request path.  The
/// `type` of a match specifies how HTTP paths should be compared.
///
/// PathPrefix and Exact paths must be syntactically valid:
/// - Must begin with the `/` character
/// - Must not contain consecutive `/` characters (e.g. `/foo///`, `//`)
#[derive(Serialize, Deserialize, Clone, Debug, PartialEq, Eq)]
#[serde(tag = "type")]
#[cfg_attr(feature = "typeinfo", derive(TypeInfo))]
pub enum PathMatch {
    #[serde(alias = "prefix")]
    Prefix { value: String },

    #[serde(alias = "regularExpression", alias = "regular_expression")]
    RegularExpression { value: Regex },

    #[serde(untagged)]
    Exact { value: String },
}

impl PathMatch {
    /// Return a [PathMatch] that matches the empty prefix.
    ///
    /// The empty prefix matches every path, so any matcher using this will
    /// always return `true`.
    pub fn empty_prefix() -> Self {
        Self::Prefix {
            value: String::new(),
        }
    }
}

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

impl Ord for PathMatch {
    fn cmp(&self, other: &Self) -> std::cmp::Ordering {
        match (self, other) {
            // exact.cmp
            (Self::Exact { value: v1 }, Self::Exact { value: v2 }) => v1.len().cmp(&v2.len()),
            (Self::Exact { .. }, _) => Ordering::Greater,
            // prefix.cmp
            (Self::Prefix { .. }, Self::Exact { .. }) => Ordering::Less,
            (Self::Prefix { value: v1 }, Self::Prefix { value: v2 }) => v1.len().cmp(&v2.len()),
            (Self::Prefix { .. }, _) => Ordering::Greater,
            // regex.cmp
            (Self::RegularExpression { value: v1 }, Self::RegularExpression { value: v2 }) => {
                v1.as_str().len().cmp(&v2.as_str().len())
            }
            (Self::RegularExpression { .. }, _) => Ordering::Less,
        }
    }
}

/// The name of an HTTP header.
///
/// Valid values include:
///
/// * "Authorization"
/// * "Set-Cookie"
///
/// Invalid values include:
///
/// * ":method" - ":" is an invalid character. This means that HTTP/2 pseudo
///    headers are not currently supported by this type.
///
/// * "/invalid" - "/" is an invalid character
//
// FIXME: newtype and validate this. probably also make this Bytes or SmolString
pub type HeaderName = String;

/// Describes how to select a HTTP route by matching HTTP request headers.
///
/// `name` is the name of the HTTP Header to be matched. Name matching is case
/// insensitive. (See <https://tools.ietf.org/html/rfc7230#section-3.2>).
///
/// If multiple entries specify equivalent header names, only the first entry
/// with an equivalent name WILL be considered for a match. Subsequent entries
/// with an equivalent header name WILL be ignored. Due to the
/// case-insensitivity of header names, "foo" and "Foo" are considered
/// equivalent.
//
// FIXME: actually do this only-the-first-entry matching thing
#[derive(Serialize, Deserialize, Clone, Debug, PartialEq, Eq)]
#[cfg_attr(feature = "typeinfo", derive(TypeInfo))]
#[serde(tag = "type", deny_unknown_fields)]
pub enum HeaderMatch {
    #[serde(
        alias = "regex",
        alias = "regular_expression",
        alias = "regularExpression"
    )]
    RegularExpression { name: String, value: Regex },

    #[serde(untagged)]
    Exact { name: String, value: String },
}

impl HeaderMatch {
    pub fn name(&self) -> &str {
        match self {
            HeaderMatch::RegularExpression { name, .. } => name,
            HeaderMatch::Exact { name, .. } => name,
        }
    }

    pub fn is_match(&self, header_value: &str) -> bool {
        match self {
            HeaderMatch::RegularExpression { value, .. } => value.is_match(header_value),
            HeaderMatch::Exact { value, .. } => value == header_value,
        }
    }
}

/// Describes how to select a HTTP route by matching HTTP query parameters.
#[derive(Serialize, Deserialize, Clone, Debug, PartialEq, Eq)]
#[serde(deny_unknown_fields, tag = "type")]
#[cfg_attr(feature = "typeinfo", derive(TypeInfo))]
pub enum QueryParamMatch {
    #[serde(
        alias = "regex",
        alias = "regular_expression",
        alias = "regularExpression"
    )]
    RegularExpression { name: String, value: Regex },

    #[serde(untagged)]
    Exact { name: String, value: String },
}

impl QueryParamMatch {
    pub fn name(&self) -> &str {
        match self {
            QueryParamMatch::RegularExpression { name, .. } => name,
            QueryParamMatch::Exact { name, .. } => name,
        }
    }

    pub fn is_match(&self, param_value: &str) -> bool {
        match self {
            QueryParamMatch::RegularExpression { value, .. } => value.is_match(param_value),
            QueryParamMatch::Exact { value, .. } => value == param_value,
        }
    }
}

/// Describes how to select a HTTP route by matching the HTTP method as defined by [RFC
/// 7231](https://datatracker.ietf.org/doc/html/rfc7231#section-4) and [RFC
/// 5789](https://datatracker.ietf.org/doc/html/rfc5789#section-2). The value is expected in upper
/// case.
//
// FIXME: replace with http::Method
pub type Method = String;

/// Defines processing steps that must be completed during the request or
/// response lifecycle.
//
// TODO: This feels very gateway-ey and redundant to type out in config. Should we switch to
// untagged here? Something else?
#[derive(Serialize, Deserialize, Clone, Debug, PartialEq, Eq)]
#[serde(tag = "type", deny_unknown_fields)]
#[cfg_attr(feature = "typeinfo", derive(TypeInfo))]
pub enum RouteFilter {
    /// Defines a schema for a filter that modifies request headers.
    RequestHeaderModifier {
        /// A Header filter.
        #[serde(alias = "requestHeaderModifier")]
        request_header_modifier: HeaderFilter,
    },

    ///  Defines a schema for a filter that modifies response headers.
    ResponseHeaderModifier {
        /// A Header filter.
        #[serde(alias = "responseHeaderModifier")]
        response_header_modifier: HeaderFilter,
    },

    /// Defines a schema for a filter that mirrors requests. Requests are sent to the specified
    /// destination, but responses from that destination are ignored.
    ///
    /// This filter can be used multiple times within the same rule. Note that not all
    /// implementations will be able to support mirroring to multiple backends.
    RequestMirror {
        #[serde(alias = "requestMirror")]
        request_mirror: RequestMirrorFilter,
    },

    /// Defines a schema for a filter that responds to the request with an HTTP redirection.
    RequestRedirect {
        #[serde(alias = "requestRedirect")]
        /// A redirect filter.
        request_redirect: RequestRedirectFilter,
    },

    /// Defines a schema for a filter that modifies a request during forwarding.
    URLRewrite {
        /// A URL rewrite filter.
        #[serde(alias = "urlRewrite")]
        url_rewrite: UrlRewriteFilter,
    },
}

/// Defines configuration for the RequestHeaderModifier filter.
#[derive(Serialize, Deserialize, Clone, Debug, PartialEq, Eq)]
#[serde(deny_unknown_fields)]
#[cfg_attr(feature = "typeinfo", derive(TypeInfo))]
pub struct HeaderFilter {
    /// Overwrites the request with the given header (name, value) before the action. Note that the
    /// header names are case-insensitive (see
    /// <https://datatracker.ietf.org/doc/html/rfc2616#section-4.2>).
    ///
    /// Input: GET /foo HTTP/1.1 my-header: foo
    ///
    /// Config: set:
    ///   - name: "my-header" value: "bar"
    ///
    /// Output: GET /foo HTTP/1.1 my-header: bar
    #[serde(default, skip_serializing_if = "Vec::is_empty")]
    pub set: Vec<HeaderValue>,

    /// Add adds the given header(s) (name, value) to the request before the action. It appends to
    /// any existing values associated with the header name.
    ///
    /// Input: GET /foo HTTP/1.1 my-header: foo
    ///
    /// Config: add:
    ///   - name: "my-header" value: "bar"
    ///
    /// Output: GET /foo HTTP/1.1 my-header: foo my-header: bar
    #[serde(default, skip_serializing_if = "Vec::is_empty")]
    pub add: Vec<HeaderValue>,

    /// Remove the given header(s) from the HTTP request before the action. The value of Remove is a
    /// list of HTTP header names. Note that the header names are case-insensitive (see
    /// <https://datatracker.ietf.org/doc/html/rfc2616#section-4.2>).
    ///
    /// Input: GET /foo HTTP/1.1 my-header1: foo my-header2: bar my-header3: baz
    ///
    /// Config: remove: ["my-header1", "my-header3"]
    ///
    /// Output: GET /foo HTTP/1.1 my-header2: bar
    #[serde(default, skip_serializing_if = "Vec::is_empty")]
    pub remove: Vec<String>,
}

#[derive(Serialize, Deserialize, Clone, Debug, PartialEq, Eq)]
#[serde(deny_unknown_fields)]
#[cfg_attr(feature = "typeinfo", derive(TypeInfo))]
pub struct HeaderValue {
    /// The name of the HTTP Header. Note header names are case insensitive. (See
    /// <https://tools.ietf.org/html/rfc7230#section-3.2>).
    pub name: HeaderName,

    /// The value of HTTP Header.
    pub value: String,
}

/// Defines configuration for path modifiers.
#[derive(Serialize, Deserialize, Clone, Debug, PartialEq, Eq)]
#[serde(tag = "type", deny_unknown_fields)]
#[cfg_attr(feature = "typeinfo", derive(TypeInfo))]
pub enum PathModifier {
    /// Specifies the value with which to replace the full path of a request during a rewrite or
    /// redirect.
    ReplaceFullPath {
        /// The value to replace the path with.
        #[serde(alias = "replaceFullPath")]
        replace_full_path: String,
    },

    /// Specifies the value with which to replace the prefix match of a request during a rewrite or
    /// redirect. For example, a request to "/foo/bar" with a prefix match of "/foo" and a
    /// ReplacePrefixMatch of "/xyz" would be modified to "/xyz/bar".
    ///
    /// Note that this matches the behavior of the PathPrefix match type. This matches full path
    /// elements. A path element refers to the list of labels in the path split by the `/`
    /// separator. When specified, a trailing `/` is ignored. For example, the paths `/abc`,
    /// `/abc/`, and `/abc/def` would all match the prefix `/abc`, but the path `/abcd` would not.
    ///
    /// ReplacePrefixMatch is only compatible with a `PathPrefix` route match::
    ///
    ///  ```plaintext,no_run
    ///  Request Path | Prefix Match | Replace Prefix | Modified Path
    ///  -------------|--------------|----------------|----------
    ///  /foo/bar     | /foo         | /xyz           | /xyz/bar
    ///  /foo/bar     | /foo         | /xyz/          | /xyz/bar
    ///  /foo/bar     | /foo/        | /xyz           | /xyz/bar
    ///  /foo/bar     | /foo/        | /xyz/          | /xyz/bar
    ///  /foo         | /foo         | /xyz           | /xyz
    ///  /foo/        | /foo         | /xyz           | /xyz/
    ///  /foo/bar     | /foo         | <empty string> | /bar
    ///  /foo/        | /foo         | <empty string> | /
    ///  /foo         | /foo         | <empty string> | /
    ///  /foo/        | /foo         | /              | /
    ///  /foo         | /foo         | /              | /
    ///  ```
    ReplacePrefixMatch {
        #[serde(alias = "replacePrefixMatch")]
        replace_prefix_match: String,
    },
}

/// Defines a filter that redirects a request. This filter MUST not be used on the same Route rule
/// as a URL Rewrite filter.
#[derive(Serialize, Deserialize, Clone, Debug, PartialEq, Eq)]
#[serde(deny_unknown_fields)]
#[cfg_attr(feature = "typeinfo", derive(TypeInfo))]
pub struct RequestRedirectFilter {
    /// The scheme to be used in the value of the `Location` header in the response. When empty, the
    /// scheme of the request is used.
    ///
    /// Scheme redirects can affect the port of the redirect, for more information, refer to the
    /// documentation for the port field of this filter.
    #[serde(default, skip_serializing_if = "Option::is_none")]
    pub scheme: Option<String>,

    /// The hostname to be used in the value of the `Location` header in the response. When empty,
    /// the hostname in the `Host` header of the request is used.
    #[serde(default, skip_serializing_if = "Option::is_none")]
    pub hostname: Option<Name>,

    /// Defines parameters used to modify the path of the incoming request. The modified path is
    /// then used to construct the `Location` header. When empty, the request path is used as-is.
    #[serde(default, skip_serializing_if = "Option::is_none")]
    pub path: Option<PathModifier>,

    /// The port to be used in the value of the `Location` header in the response.
    ///
    /// If no port is specified, the redirect port MUST be derived using the following rules:
    ///
    /// * If redirect scheme is not-empty, the redirect port MUST be the well-known port associated
    ///   with the redirect scheme. Specifically "http" to port 80 and "https" to port 443. If the
    ///   redirect scheme does not have a well-known port, the listener port of the Gateway SHOULD
    ///   be used.
    /// * If redirect scheme is empty, the redirect port MUST be the Gateway Listener port.
    ///
    /// Will not add the port number in the 'Location' header in the following cases:
    ///
    /// * A Location header that will use HTTP (whether that is determined via the Listener protocol
    ///   or the Scheme field) _and_ use port 80.
    /// * A Location header that will use HTTPS (whether that is determined via the Listener
    ///   protocol or the Scheme field) _and_ use port 443.
    #[serde(default, skip_serializing_if = "Option::is_none")]
    pub port: Option<u16>,

    /// The HTTP status code to be used in response.
    #[serde(default, skip_serializing_if = "Option::is_none", alias = "statusCode")]
    pub status_code: Option<u16>,
}

/// Defines a filter that modifies a request during forwarding. At most one of these filters may be
/// used on a Route rule. This may not be used on the same Route rule as a RequestRedirect filter.
#[derive(Serialize, Deserialize, Clone, Debug, PartialEq, Eq)]
#[serde(deny_unknown_fields)]
#[cfg_attr(feature = "typeinfo", derive(TypeInfo))]
pub struct UrlRewriteFilter {
    /// The value to be used to replace the Host header value during forwarding.
    #[serde(default, skip_serializing_if = "Option::is_none")]
    pub hostname: Option<Hostname>,

    /// Defines a path rewrite.
    #[serde(default, skip_serializing_if = "Option::is_none")]
    pub path: Option<PathModifier>,
}

/// Defines configuration for the RequestMirror filter.
#[derive(Serialize, Deserialize, Clone, Debug, PartialEq, Eq)]
#[serde(deny_unknown_fields)]
#[cfg_attr(feature = "typeinfo", derive(TypeInfo))]
pub struct RequestMirrorFilter {
    /// Represents the percentage of requests that should be mirrored to BackendRef. Its minimum
    /// value is 0 (indicating 0% of requests) and its maximum value is 100 (indicating 100% of
    /// requests).
    ///
    /// Only one of Fraction or Percent may be specified. If neither field is specified, 100% of
    /// requests will be mirrored.
    pub percent: Option<i32>,

    /// Only one of Fraction or Percent may be specified. If neither field is specified, 100% of
    /// requests will be mirrored.
    pub fraction: Option<Fraction>,

    pub backend: Service,
}

/// Configure client retry policy.
#[derive(Serialize, Deserialize, Clone, Debug, PartialEq, Eq, Default)]
#[serde(deny_unknown_fields)]
#[cfg_attr(feature = "typeinfo", derive(TypeInfo))]
pub struct RouteRetry {
    /// The HTTP error codes that retries should be applied to.
    //
    // TODO: should this be http::StatusCode?
    #[serde(default, skip_serializing_if = "Vec::is_empty")]
    pub codes: Vec<u16>,

    /// The total number of attempts to make when retrying this request.
    #[serde(default, skip_serializing_if = "Option::is_none")]
    pub attempts: Option<u32>,

    /// The amount of time to back off between requests during a series of
    /// retries.
    #[serde(default, skip_serializing_if = "Option::is_none")]
    pub backoff: Option<Duration>,
}

const fn default_weight() -> u32 {
    1
}

// TODO: gateway API also allows filters here under an extended support
// condition we need to decide whether this is one where its simpler just to
// drop it.
#[derive(Serialize, Deserialize, Clone, Debug, PartialEq, Eq)]
#[cfg_attr(feature = "typeinfo", derive(TypeInfo))]
pub struct BackendRef {
    /// The Serivce to route to when traffic matches. This Service will always
    /// be combined with a `port` to uniquely identify the
    /// [Backend][crate::backend::Backend] traffic should be routed to.
    #[serde(flatten)]
    pub service: Service,

    /// The port to route traffic to, used in combination with
    /// [service][Self::service] to identify the
    /// [Backend][crate::backend::Backend] to route traffic to.
    ///
    /// If omitted, the port of the incoming request is used to route traffic.
    pub port: Option<u16>,

    /// The relative weight of this backend relative to any other backends in
    /// [the list][RouteRule::backends].
    ///
    /// If not specified, defaults to `1`.
    ///
    /// An individual backend may have a weight of `0`, but specifying every
    /// backend with `0` weight is an error.
    #[serde(default = "default_weight")]
    pub weight: u32,
}

impl BackendRef {
    #[doc(hidden)]
    pub fn into_backend_id(&self, default_port: u16) -> BackendId {
        let port = self.port.unwrap_or(default_port);

        BackendId {
            service: self.service.clone(),
            port,
        }
    }

    #[doc(hidden)]
    pub fn as_backend_id(&self) -> Option<BackendId> {
        let port = self.port?;

        Some(BackendId {
            service: self.service.clone(),
            port,
        })
    }

    #[cfg(feature = "xds")]
    pub(crate) fn name(&self) -> String {
        let mut buf = String::new();
        self.write_name(&mut buf).unwrap();
        buf
    }

    #[cfg(feature = "xds")]
    fn write_name(&self, w: &mut impl std::fmt::Write) -> std::fmt::Result {
        self.service.write_name(w)?;
        if let Some(port) = self.port {
            write!(w, ":{port}")?;
        }

        Ok(())
    }
}

impl FromStr for BackendRef {
    type Err = crate::Error;

    fn from_str(s: &str) -> Result<Self, Self::Err> {
        let (name, port) = super::parse_port(s)?;
        let backend = Service::from_str(name)?;

        Ok(Self {
            service: backend,
            port,
            weight: default_weight(),
        })
    }
}

#[cfg(test)]
mod test {
    use std::str::FromStr;

    use rand::seq::SliceRandom;
    use serde::de::DeserializeOwned;
    use serde_json::json;

    use super::*;
    use crate::{
        http::{HeaderMatch, RouteRule},
        shared::Regex,
        Service,
    };

    #[test]
    fn test_hostname_match() {
        let exact_matcher = HostnameMatch::from_str("foo.bar").unwrap();
        let subdomain_matcher = HostnameMatch::from_str("*.foo.bar").unwrap();

        for invalid_hostname in [
            "",
            "*",
            ".*",
            ".",
            "!@#@!#!@",
            "foo....bar",
            ".foo.bar",
            "...foo.bar",
        ] {
            assert!(!exact_matcher.matches_str(invalid_hostname));
            assert!(!subdomain_matcher.matches_str(invalid_hostname));
        }

        for not_matching in ["blahfoo.bar", "bfoo.bar", "bar.foo"] {
            assert!(!exact_matcher.matches_str(not_matching));
            assert!(!subdomain_matcher.matches_str(not_matching));
        }

        assert!(exact_matcher.matches_str("foo.bar"));
        assert!(!subdomain_matcher.matches_str("foo.bar"));

        assert!(!exact_matcher.matches_str("blah.foo.bar"));
        assert!(subdomain_matcher.matches_str("blah.foo.bar"));
        assert!(subdomain_matcher.matches_str("b.foo.bar"));
    }

    #[test]
    fn test_hostname_match_json() {
        let json_value = json!(["foo.bar.baz", "*.foo.bar.baz",]);
        let matchers = vec![
            HostnameMatch::Exact(Hostname::from_static("foo.bar.baz")),
            HostnameMatch::Subdomain(Hostname::from_static("foo.bar.baz")),
        ];

        assert_eq!(
            serde_json::from_value::<Vec<HostnameMatch>>(json_value.clone()).unwrap(),
            matchers,
        );
        assert_eq!(serde_json::to_value(&matchers).unwrap(), json_value,);
    }

    #[test]
    fn test_header_matcher_json() {
        let test_json = json!([
            { "name":"bar", "type" : "RegularExpression", "value": ".*foo"},
            { "name":"bar", "value": "a literal"},
        ]);
        let obj: Vec<HeaderMatch> = serde_json::from_value(test_json.clone()).unwrap();

        assert_eq!(
            obj,
            vec![
                HeaderMatch::RegularExpression {
                    name: "bar".to_string(),
                    value: Regex::from_str(".*foo").unwrap(),
                },
                HeaderMatch::Exact {
                    name: "bar".to_string(),
                    value: "a literal".to_string(),
                }
            ]
        );

        let output_json = serde_json::to_value(&obj).unwrap();
        assert_eq!(test_json, output_json);
    }

    #[test]
    fn test_retry_policy_json() {
        let test_json = json!({
            "codes":[ 1, 2 ],
            "attempts": 3,
            // NOTE: serde will happily read an int here, but Duration serializes as a float
            "backoff": 60.0,
        });
        let obj: RouteRetry = serde_json::from_value(test_json.clone()).unwrap();
        let output_json = serde_json::to_value(obj).unwrap();
        assert_eq!(test_json, output_json);
    }

    #[test]
    fn test_route_rule_json() {
        let test_json = json!({
            "matches":[
                {
                    "method": "GET",
                    "path": { "value": "foo" },
                    "headers": [
                        {"name":"ian", "value": "foo"},
                        {"name": "bar", "type":"RegularExpression", "value": ".*foo"}
                    ]
                },
                {
                    "query_params": [
                        {"name":"ian", "value": "foo"},
                        {"name": "bar", "type":"RegularExpression", "value": ".*foo"}
                    ]
                }
            ],
            "filters":[{
                "type": "URLRewrite",
                "url_rewrite":{
                    "hostname":"ian.com",
                    "path": {"type":"ReplacePrefixMatch", "replace_prefix_match":"/"}
                }
            }],
            "backends":[
                {
                    "type": "kube",
                    "name": "timeout-svc",
                    "namespace": "foo",
                    "port": 80,
                    "weight": 1,
                }
            ],
            "timeouts": {
                "request": 1.0,
            }
        });
        let obj: RouteRule = serde_json::from_value(test_json.clone()).unwrap();
        let output_json = serde_json::to_value(&obj).unwrap();
        assert_eq!(test_json, output_json);
    }

    #[test]
    fn test_route_json() {
        assert_deserialize(
            json!({
                "id": "sweet-potato",
                "hostnames": ["foo.bar.svc.cluster.local"],
                "rules": [
                    {
                        "backends": [
                            {
                                "type": "kube",
                                "name": "foo",
                                "namespace": "bar",
                                "port": 80,
                            }
                        ],
                    }
                ]
            }),
            Route {
                id: Name::from_static("sweet-potato"),
                hostnames: vec![Hostname::from_static("foo.bar.svc.cluster.local").into()],
                ports: vec![],
                tags: Default::default(),
                rules: vec![RouteRule {
                    name: None,
                    matches: vec![],
                    filters: vec![],
                    timeouts: None,
                    retry: None,
                    backends: vec![BackendRef {
                        service: Service::kube("bar", "foo").unwrap(),
                        port: Some(80),
                        weight: 1,
                    }],
                }],
            },
        );
    }

    #[test]
    fn test_route_json_missing_fields() {
        assert_deserialize_err::<Route>(json!({
            "uhhhh": ["foo.bar"],
            "rules": [
                {
                    "matches": [],
                }
            ]
        }));
    }

    #[track_caller]
    fn assert_deserialize<T: DeserializeOwned + PartialEq + std::fmt::Debug>(
        json: serde_json::Value,
        expected: T,
    ) {
        let actual: T = serde_json::from_value(json).unwrap();
        assert_eq!(expected, actual);
    }

    #[track_caller]
    fn assert_deserialize_err<T: DeserializeOwned + PartialEq + std::fmt::Debug>(
        json: serde_json::Value,
    ) -> serde_json::Error {
        serde_json::from_value::<T>(json).unwrap_err()
    }

    #[test]
    fn test_path_match() {
        // test that exact cmps are only based on length
        arbtest::arbtest(|u| {
            let s1: String = u.arbitrary()?;
            let s2: String = u.arbitrary()?;

            let m1 = PathMatch::Exact { value: s1.clone() };
            let m2 = PathMatch::Exact { value: s2.clone() };

            assert_eq!(s1.len().cmp(&s2.len()), m1.cmp(&m2));

            Ok(())
        });

        // test that prefix cmps are only based on length
        arbtest::arbtest(|u| {
            let s1: String = u.arbitrary()?;
            let s2: String = u.arbitrary()?;

            let m1 = PathMatch::Prefix { value: s1.clone() };
            let m2 = PathMatch::Prefix { value: s2.clone() };

            assert_eq!(s1.len().cmp(&s2.len()), m1.cmp(&m2));

            Ok(())
        });

        // test that exact > prefix always
        arbtest::arbtest(|u| {
            let m1 = PathMatch::Exact {
                value: u.arbitrary()?,
            };
            let m2 = PathMatch::Prefix {
                value: u.arbitrary()?,
            };
            assert!(m1 > m2);

            Ok(())
        });
    }

    #[test]
    fn test_order_route_match() {
        let path_match = RouteMatch {
            path: Some(PathMatch::Exact {
                value: "/potato".to_string(),
            }),
            ..Default::default()
        };
        let method_match = RouteMatch {
            method: Some("PUT".to_string()),
            ..Default::default()
        };
        let header_match = RouteMatch {
            headers: vec![HeaderMatch::Exact {
                name: "x-user".to_string(),
                value: "a-user".to_string(),
            }],
            ..Default::default()
        };
        let query_match = RouteMatch {
            query_params: vec![QueryParamMatch::Exact {
                name: "q".to_string(),
                value: "value".to_string(),
            }],
            ..Default::default()
        };

        // order some simple combos
        assert_eq!(
            vec![&query_match, &header_match, &method_match, &path_match],
            shuffle_and_sort([&path_match, &query_match, &header_match, &method_match]),
        );

        // tie-breaking within a field
        let m1 = RouteMatch {
            path: Some(PathMatch::Exact {
                value: "fooooooooooo".to_string(),
            }),
            query_params: query_match.query_params.clone(),
            ..Default::default()
        };
        let m2 = RouteMatch {
            path: Some(PathMatch::Exact {
                value: "foo".to_string(),
            }),
            query_params: query_match.query_params.clone(),
            ..Default::default()
        };
        assert!(m1 > m2, "should tie break by comparing path_match");

        // tie-breaking with other fields
        let m1 = RouteMatch {
            path: path_match.path.clone(),
            query_params: query_match.query_params.clone(),
            ..Default::default()
        };
        let m2 = RouteMatch {
            path: path_match.path.clone(),
            ..Default::default()
        };
        assert!(m1 > m2, "should tie-break with query params");

        let m1 = RouteMatch {
            method: Some("GET".to_string()),
            query_params: query_match.query_params.clone(),
            ..Default::default()
        };
        let m2 = RouteMatch {
            method: Some("PUT".to_string()),
            ..Default::default()
        };
        assert!(m1 > m2, "should tie-break with query params");
    }

    #[test]
    fn test_order_route_rule() {
        let path_match = RouteMatch {
            path: Some(PathMatch::Exact {
                value: "/potato".to_string(),
            }),
            ..Default::default()
        };
        let header_match = RouteMatch {
            headers: vec![HeaderMatch::Exact {
                name: "x-user".to_string(),
                value: "a-user".to_string(),
            }],
            ..Default::default()
        };
        let query_match = RouteMatch {
            query_params: vec![QueryParamMatch::Exact {
                name: "q".to_string(),
                value: "value".to_string(),
            }],
            ..Default::default()
        };

        // simple single match
        let r1 = RouteRule {
            matches: vec![path_match.clone()],
            ..Default::default()
        };
        let r2 = RouteRule {
            matches: vec![header_match.clone()],
            ..Default::default()
        };
        assert!(r1 > r2);
        assert!(r2 < r1);

        // tie break with extra matches
        let r1 = RouteRule {
            matches: vec![path_match.clone()],
            ..Default::default()
        };
        let r2 = RouteRule {
            matches: vec![path_match.clone(), header_match.clone()],
            ..Default::default()
        };
        assert!(r1 < r2);
        assert!(r2 > r1);

        // empty matches sorts last
        let r1 = RouteRule {
            matches: vec![query_match.clone()],
            ..Default::default()
        };
        let r2 = RouteRule {
            matches: vec![],
            ..Default::default()
        };
        assert!(r2 < r1);
        assert!(r1 > r2);
    }

    fn shuffle_and_sort<T: Ord>(xs: impl IntoIterator<Item = T>) -> Vec<T> {
        let mut rng = rand::thread_rng();

        let mut v: Vec<_> = xs.into_iter().collect();
        v.shuffle(&mut rng);
        v.sort();
        v
    }
}
