///
/// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
///
/// Licensed under the Apache License, Version 2.0 (the "License").
/// You may not use this file except in compliance with the License.
/// A copy of the License is located at
///
///  http://aws.amazon.com/apache2.0
///
/// or in the "license" file accompanying this file. This file is distributed
/// on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either
/// express or implied. See the License for the specific language governing
/// permissions and limitations under the License.
///
use crate::http_token_utils::TokenSimilarity::{Distant, Identical, SameLetters};
use crate::MESSAGE_MAX_SIZE;
use std::fmt::Write;

/// No allocation of new tokens is happening.
/// All operations are in-place and read only.
pub type HttpToken<'a> = &'a [u8];

pub const fn http_token(v: &str) -> HttpToken {
    v.as_bytes()
}

// Include generated char tables. See `build.rs`
include!(concat!(env!("OUT_DIR"), "/char_tables.rs"));
pub const TE: HttpToken = http_token("transfer-encoding");
pub const CL: HttpToken = http_token("content-length");
///
/// https://tools.ietf.org/html/rfc7230#page-22
///
/// ```ignore
/// OWS = *( SP / HTAB )
/// header-field = field-name ":" OWS field-value OWS
/// field-content = field-vchar [ 1*( SP / HTAB ) field-vchar ]
/// field-name = token
/// field-value = *( field-content / obs-fold )
/// field-vchar = VCHAR / obs-text
/// obs-fold = CRLF 1*( SP / HTAB )
/// obs-text = %x80-FF
/// fragment = <fragment, see [RFC3986], Section 3.5>
/// header-field = field-name ":" OWS field-value OWS
/// token = 1*tchar
/// token          = 1*<any CHAR except CTLs or separators>
/// tchar = "!" / "#" / "$" / "%" / "&" / "'" / "*" / "+" / "-" / "." /
///         "^" / "_" / "`" / "|" / "~" / DIGIT / ALPHA
///
/// t-codings = "trailers" / ( transfer-coding [ t-ranking ] )
/// t-ranking = OWS ";" OWS "q=" rank
/// transfer-coding = "chunked" / "compress" / "deflate" / "gzip" / transfer-extension
/// transfer-extension = token *( OWS ";" OWS transfer-parameter )
/// transfer-parameter = token BWS "=" BWS ( token / quoted-string )
/// ```

/// URL: https://tools.ietf.org/html/rfc3986#appendix-A
/// ```ignore
/// path          = path-abempty    ; begins with "/" or is empty
///                   / path-absolute   ; begins with "/" but not "//"
///                   / path-noscheme   ; begins with a non-colon segment
///                   / path-rootless   ; begins with a segment
///                   / path-empty      ; zero characters
///
///     path-abempty  = *( "/" segment )
///     path-absolute = "/" [ segment-nz *( "/" segment ) ]
///     path-noscheme = segment-nz-nc *( "/" segment )
///     path-rootless = segment-nz *( "/" segment )
///     path-empty    = 0<pchar>
///
/// Berners-Lee, et al.         Standards Track                    [Page 22]
///
/// RFC 3986                   URI Generic Syntax               January 2005
///
///     segment       = *pchar
///     segment-nz    = 1*pchar
///     segment-nz-nc = 1*( unreserved / pct-encoded / sub-delims / "@" )
///                   ; non-zero-length segment without any colon ":"
///
///     pchar         = unreserved / pct-encoded / sub-delims / ":" / "@"
///     sub-delims    = "!" / "$" / "&" / "'" / "(" / ")"
///                 / "*" / "+" / "," / ";" / "="
///     pct-encoded = "%" HEXDIG HEXDIG
///     unreserved    = ALPHA / DIGIT / "-" / "." / "_" / "~"
/// ```

const SP: u8 = b' ';
const COLON: u8 = b':';
pub const CHUNKED: HttpToken = http_token("chunked");
const VALID_TE: &[HttpToken] = &[
    CHUNKED,
    http_token("compress"),
    http_token("deflate"),
    http_token("gzip"),
    http_token("identity"),
];

#[derive(PartialOrd, PartialEq, Debug)]
pub enum TokenSimilarity {
    /// If two token are identical, i.e. equal ignoring case.
    /// ```no_run
    /// use http_desync_guardian::http_token_utils::*;
    /// use http_desync_guardian::http_token_utils::TokenSimilarity::*;
    ///
    /// assert_eq!(determine_similarity(http_token("token"), http_token("Token")), Identical);
    /// assert_eq!(determine_similarity(http_token("same-token"), http_token("Same-Token")), Identical);
    /// ```
    Identical,
    /// If two tokens differ only by non-letter characters.
    /// ```no_run
    /// use http_desync_guardian::http_token_utils::*;
    /// use http_desync_guardian::http_token_utils::TokenSimilarity::*;
    ///
    /// assert_eq!(determine_similarity(http_token("token"), http_token("Token\t")), SameLetters);
    /// assert_eq!(determine_similarity(http_token("same-token"), http_token("Same\t-Token")), SameLetters);
    /// ```
    SameLetters,
    /// Otherwise it's `Distant`. E.g. "some-token" and "other-token"
    /// ```no_run
    /// use http_desync_guardian::http_token_utils::*;
    /// use http_desync_guardian::http_token_utils::TokenSimilarity::*;
    ///
    /// assert_eq!(determine_similarity(http_token("token"), http_token("Different-token")), Distant);
    /// assert_eq!(determine_similarity(http_token("same-token"), http_token("Other-token")), Distant);
    /// ```
    Distant,
}

pub fn is_valid_te(value: HttpToken) -> Option<HttpToken> {
    let trimmed = rfc_whitespace_trim(value);
    let mut end: usize = 0;
    while end < trimmed.len() && trimmed[end] != b';' {
        end += 1;
    }
    let te = rfc_whitespace_trim(&trimmed[..end]);
    VALID_TE
        .iter()
        .copied()
        .find(|s| s.eq_ignore_ascii_case(te))
}

#[inline(always)]
pub fn is_valid_uri_char(b: u8) -> bool {
    // disable CTL in URL
    VALID_URI_CHARS[b as usize]
}

#[inline(always)]
pub fn is_space(b: u8) -> bool {
    b == SP
}

#[inline(always)]
pub fn is_colon(b: u8) -> bool {
    b == COLON
}

#[inline(always)]
pub fn find_next_header_symbol(value: HttpToken, current: usize) -> usize {
    let mut i = current;
    while i < value.len() && value[i] <= SP {
        i += 1;
    }
    i
}

#[inline(always)]
pub fn find_next_alphanumeric(value: HttpToken, current: usize) -> usize {
    let mut i = current;
    while i < value.len() && !value[i].is_ascii_alphanumeric() {
        i += 1;
    }
    i
}

#[inline(always)]
pub fn is_rfc_tchar(b: u8) -> bool {
    TCHAR_TABLE[b as usize]
}

#[inline(always)]
pub fn is_valid_header_value_char(b: u8) -> bool {
    HEADER_VALUE_CHARS[b as usize]
}

#[inline(always)]
pub fn is_rfc_vchar(b: u8) -> bool {
    VCHAR[b as usize]
}

#[inline(always)]
pub fn is_rfc_obs_text(b: u8) -> bool {
    b >= 0x80
}

///
/// Determines if a given character is an indication
/// of a parsing error or a crafted malicious request.
/// Includes: `\r`, `\n` and `\0`.
///
#[inline(always)]
pub fn is_bad_http_character(b: u8) -> bool {
    BAD_CHARACTERS[b as usize]
}

pub fn to_quoted_ascii(raw_data: HttpToken) -> String {
    let mut str = String::with_capacity(raw_data.len().min(MESSAGE_MAX_SIZE / 2));
    str.push('"');
    for b in raw_data {
        if str.len() >= MESSAGE_MAX_SIZE / 2 - 4 {
            break;
        }
        match *b {
            b'\r' => str.push_str("\\r"),
            b'\n' => str.push_str("\\n"),
            b'\t' => str.push_str("\\t"),
            b'\\' => str.push_str("\\\\"),
            _ if is_rfc_vchar(*b) => {
                str.push(*b as char);
            }
            _ => {
                write!(str, "\\{:#04x}", *b).expect("Writing to strings is infallible");
            }
        }
    }
    str.push('"');
    str
}

/// We do not want to log header values
/// It only loads violating characters
pub fn obfuscate_value(raw_data: HttpToken) -> String {
    let mut str = String::with_capacity(MESSAGE_MAX_SIZE / 2);
    str.push('"');
    let mut prev_char = 0;
    for b in raw_data {
        if str.len() >= MESSAGE_MAX_SIZE / 2 - 4 {
            break;
        }
        match *b {
            b'\r' => str.push_str("\\r"),
            b'\n' => str.push_str("\\n"),
            b'\t' => str.push_str("\\t"),
            b'\\' => str.push_str("\\\\"),
            b' ' => str.push(' '),
            _ if !is_rfc_vchar(*b) && !is_rfc_obs_text(*b) => {
                write!(str, "\\{:#04x}", *b).expect("Writing to strings is infallible");
            }
            _ if prev_char != b'_' => {
                str.push('_');
            }
            _ => {
                // skip
            }
        }
        prev_char = str.as_bytes()[str.len() - 1];
    }
    str.push('"');
    str
}

#[inline(always)]
pub fn is_rfc_whitespace(b: u8) -> bool {
    RFC_WHITE_SPACE[b as usize]
}

pub fn parse_num(value: HttpToken) -> Result<u64, &str> {
    let trimmed = rfc_whitespace_trim(value);
    let mut result: u64 = 0;
    for c in trimmed.iter() {
        if !c.is_ascii_digit() {
            return Err("Not an integer");
        }
        let digit = (*c - b'0') as u64;

        // check for 64-bits overflow after multiplication
        // with 10 and addition with current digit
        result = result.checked_mul(10).ok_or("64-bits overflow")?;
        result = result.checked_add(digit).ok_or("64-bits overflow")?;
    }
    Ok(result)
}

#[inline]
pub fn rfc_whitespace_trim(value: HttpToken) -> HttpToken {
    if value.is_empty() {
        return value;
    }

    let mut first_non_space = 0;
    let mut last_non_space = value.len();

    while first_non_space < last_non_space && is_rfc_whitespace(value[first_non_space]) {
        first_non_space += 1;
    }

    while first_non_space < last_non_space && is_rfc_whitespace(value[last_non_space - 1]) {
        last_non_space -= 1;
    }

    &value[first_non_space..last_non_space]
}

#[inline]
fn same_char(original_char: u8, header: HttpToken, index: usize) -> (bool, usize) {
    // check if it's the same char, taking into account UTF8 characters
    // that convert to ASCII on to_uppercase transformation, such as:
    // Latin Small Letter Long S https://www.compart.com/en/unicode/U+017f
    // "\0xc5\0xbf" -> "Å¿".upper() == "S"
    // Latin Small Letter Dotless I https://www.compart.com/en/unicode/U+0131
    // "\0xc4\0xb1" -> "Ä±".upper() == "I"
    // These characters were found by scanning all UTF8 with a property
    // of upper/lower case transformation coincide with TE/CL letters.
    let header_char = header[index];
    if original_char == header_char.to_ascii_lowercase() {
        return (true, index + 1);
    } else if header.len() > index + 1 {
        const MATCHES: &[(u8, u8, u8)] = &[(b's', 0xc5, 0xbf), (b'i', 0xc4, 0xb1)];
        if MATCHES
            .iter()
            .any(|p| *p == (original_char, header_char, header[index + 1]))
        {
            return (true, index + 2);
        }
    }
    (false, index + 1)
}

///
/// Determines similarity of two tokens taking into account only alpha-numeric characters.
///
/// # Parameters
/// `important_header` - must be only in lower-case letters and '-'.
/// `header` - any token to check
///
pub fn determine_similarity(important_header: HttpToken, header: HttpToken) -> TokenSimilarity {
    debug_assert!(important_header
        .iter()
        .all(|c| *c == c.to_ascii_lowercase()));
    debug_assert!(important_header
        .iter()
        .all(|c| c.is_ascii_alphabetic() || *c == b'-'));

    if header.len() < important_header.len() {
        return Distant;
    }

    let mut header_index = find_next_alphanumeric(header, 0);
    let mut identical = header_index == 0 && important_header.len() == header.len();
    for c in important_header.iter() {
        if header_index == header.len() {
            return Distant;
        }
        let delimiter = *c == b'-';
        let (same_symbol, new_index) = same_char(*c, header, header_index);
        if !same_symbol {
            if !delimiter {
                break;
            } else {
                identical = false;
            }
        }
        header_index = if delimiter {
            find_next_alphanumeric(header, new_index)
        } else {
            find_next_header_symbol(header, new_index)
        };
    }
    let exhausted_header = header_index == header.len();
    if exhausted_header && identical {
        Identical
    } else if exhausted_header || find_next_alphanumeric(header, header_index) == header.len() {
        SameLetters
    } else {
        Distant
    }
}

#[cfg(test)]
mod tests {
    use crate::http_token_utils::*;
    use crate::MESSAGE_MAX_SIZE;
    use smallvec::alloc::str::from_utf8;

    #[test]
    fn verify_essential_headers_lowercase() {
        // for performance consideration the code assumes all constants are in lower-case
        // this test ensures it's the case
        &[TE, CL].iter().for_each(|h| {
            h.iter().for_each(|b| {
                assert_eq!(b.to_ascii_lowercase(), *b);
            });
        });
    }

    #[test]
    fn verify_te_options_lowercase() {
        // for performance consideration the code assumes all constants are in lower-case
        // this test ensures it's the case
        VALID_TE.iter().for_each(|h| {
            h.iter().for_each(|b| {
                assert_eq!((*b).to_ascii_lowercase(), *b);
            });
        });
    }

    #[test]
    fn test_same_char() {
        assert_eq!((true, 2), same_char(b's', &[0xc5, 0xbf], 0));
        assert_eq!((true, 2), same_char(b'i', &[0xc4, 0xb1], 0));
        assert_eq!((true, 1), same_char(b's', &[b's'], 0));
        assert_eq!((true, 1), same_char(b's', &[b'S'], 0));

        assert_eq!((false, 1), same_char(b's', &[b'a'], 0));
        assert_eq!((false, 1), same_char(b's', &[0xc5], 0));
    }

    #[test]
    fn test_same_char_covers_all_utf8() {
        // Verify that all UTF8 characters, that
        let important_headers = &[
            String::from_utf8(TE.to_vec()).expect("ASCII -> UTF8 is infallible"),
            String::from_utf8(CL.to_vec()).expect("ASCII -> UTF8 is infallible"),
        ];
        let mut chars_matched = 0;
        let mut chars_checked = 0;
        // iterate over all UTF8
        for d in 0x80..=0xFF {
            for c in 0x80..=0xFF {
                let (matched, checked) = check_utf8_character(important_headers, &[c, d]);
                chars_checked += checked;
                chars_matched += matched;
                for b in 0x80..=0xFF {
                    let (matched, checked) = check_utf8_character(important_headers, &[b, c, d]);
                    chars_checked += checked;
                    chars_matched += matched;
                    for a in 0xF0..=0xFF {
                        let (matched, checked) =
                            check_utf8_character(important_headers, &[a, b, c, d]);
                        chars_checked += checked;
                        chars_matched += matched;
                    }
                }
            }
        }
        assert_eq!(
            chars_checked, 3,
            "We know there are three such characters, that convert from UTF8 to ASCII"
        );
        assert_eq!(
            chars_matched, 2,
            "We know there are two of them that should match TE/CL headers"
        );
    }

    fn check_utf8_character(important_headers: &[String], utf8_char: &[u8]) -> (usize, usize) {
        let mut checked = 0;
        let mut matched = 0;

        if let Ok(s) = from_utf8(utf8_char) {
            let upper = s.to_uppercase();
            let lower = s.to_lowercase();
            let to_ascii = upper.len() == 1 || lower.len() == 1;
            if to_ascii {
                checked += 1;

                let contains_upper = upper.len() == 1
                    && important_headers
                        .iter()
                        .any(|header| header.to_ascii_uppercase().contains(upper.as_str()));
                if contains_upper {
                    assert_eq!(
                        same_char(upper.to_ascii_lowercase().as_bytes()[0], utf8_char, 0),
                        (true, utf8_char.len()),
                        "{} is not covered",
                        s
                    );
                    matched += 1;
                }

                let contains_lower = lower.len() == 1
                    && important_headers
                        .iter()
                        .any(|header| header.contains(lower.as_str()));
                if contains_lower {
                    assert_eq!(
                        same_char(lower.as_bytes()[0], utf8_char, 0),
                        (true, utf8_char.len()),
                        "{} is not covered",
                        s
                    );
                    matched += 1;
                }
            }
            if matched == 0 {
                for c in b'a'..=b'z' {
                    assert_eq!(
                        same_char(c, utf8_char, 0),
                        (false, 1),
                        "{} must not match any characters, but it did: {}",
                        s,
                        to_quoted_ascii(&[c])
                    );
                }
            }
        }
        return (matched, checked);
    }

    #[test]
    fn test_same_letters() {
        let test_cases = vec![
            ("t-e", "t-e", TokenSimilarity::Identical),
            ("t-e", "t_e", TokenSimilarity::SameLetters),
            ("t-e", "t_e ", TokenSimilarity::SameLetters),
            ("t-e", "t____e", TokenSimilarity::SameLetters),
            ("t-e", "t----e", TokenSimilarity::SameLetters),
            ("t", "tp", TokenSimilarity::Distant),
            ("t", "t0", TokenSimilarity::Distant),
            ("t", "t_", TokenSimilarity::SameLetters),
            ("t", "p", TokenSimilarity::Distant),
            ("t", "", TokenSimilarity::Distant),
            (
                "transfer-encoding",
                "transfer-encoding1",
                TokenSimilarity::Distant,
            ),
            (
                "transfer-encoding",
                // Latin Small Letter Long S https://www.compart.com/en/unicode/U+017f
                "tran\u{017f}fer-encoding",
                TokenSimilarity::SameLetters,
            ),
            (
                "transfer-encoding",
                // Latin Small Letter Dotless I https://www.compart.com/en/unicode/U+0131
                "transfer-encod\u{0131}ng",
                TokenSimilarity::SameLetters,
            ),
            (
                "transfer-encoding",
                "transfer-encoding",
                TokenSimilarity::Identical,
            ),
            (
                "transfer-encoding",
                "Transfer-Encoding",
                TokenSimilarity::Identical,
            ),
            (
                "x-my-custom-important-header",
                "X-My-Custom-Important-Header",
                TokenSimilarity::Identical,
            ),
            (
                "x-my-custom-important-header",
                "X_My_Custom_Important_Header",
                TokenSimilarity::SameLetters,
            ),
            (
                "transfer-encoding",
                "transfer-encodin",
                TokenSimilarity::Distant,
            ),
            ("transfer", "transfer-encoding", TokenSimilarity::Distant),
            (
                "transfer-encoding",
                "transfer-encoding1",
                TokenSimilarity::Distant,
            ),
            (
                "transfer-encoding",
                "TRANSFER-ENCODING",
                TokenSimilarity::Identical,
            ),
            (
                "transfer-encoding",
                "transfer-\tencoding",
                TokenSimilarity::SameLetters,
            ),
            (
                "transfer-encoding",
                "\r\ntransfer-\t\te\tn\tc o d i ng\t",
                TokenSimilarity::SameLetters,
            ),
            (
                "transfer-encoding",
                "transfer_encoding",
                TokenSimilarity::SameLetters,
            ),
            (
                "transfer-encoding",
                "transfer---encoding",
                TokenSimilarity::SameLetters,
            ),
            (
                "transfer-encoding",
                "x_transfer_encoding",
                TokenSimilarity::Distant,
            ),
            (
                "transfer-encoding",
                "x_transfer_encodings",
                TokenSimilarity::Distant,
            ),
            ("transfer-encoding", "gibberish", TokenSimilarity::Distant),
            (
                "transfer-encoding",
                "\t\x01\x02transfer-encoding\t\x0f\x0c",
                TokenSimilarity::SameLetters,
            ),
            (
                "transfer-encoding",
                "\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t",
                TokenSimilarity::Distant,
            ),
            (
                "transfer-encoding",
                "accept-encoding",
                TokenSimilarity::Distant,
            ),
        ];

        test_cases.iter().for_each(|(e, c, expected_distance)| {
            let important_header = http_token(e);
            let header = http_token(c);
            let actual_distance = determine_similarity(important_header, header);
            assert_eq!(
                actual_distance, *expected_distance,
                "similarity({}, {:?}) != {:?}",
                e, c, *expected_distance
            );
        });
    }

    #[test]
    fn test_utf8_spaces() {
        // https://en.wikipedia.org/wiki/Whitespace_character
        let test_cases = vec![
            "\u{0009}", // character tabulation
            "\u{000A}", // line feed
            "\u{000B}", // line tabulation
            "\u{000C}", // form feed
            "\u{000D}", // carriage return
            "\u{0020}", // space
            "\u{0085}", // next line
            "\u{00A0}", // no-break space
            "\u{1680}", // ogham space mark
            "\u{180E}", // mongolian vowel separator
            "\u{2000}", // en quad
            "\u{2001}", // em quad
            "\u{2002}", // en space
            "\u{2003}", // em space
            "\u{2004}", // three-per-em space
            "\u{2005}", // four-per-em space
            "\u{2006}", // six-per-em space
            "\u{2007}", // figure space
            "\u{2008}", // punctuation space
            "\u{2009}", // thin space
            "\u{200A}", // hair space
            "\u{200B}", // zero width space
            "\u{200C}", // zero width non-joiner
            "\u{200D}", // zero width joiner
            "\u{2028}", // line separator
            "\u{2029}", // paragraph separator
            "\u{202F}", // narrow no-break space
            "\u{205F}", // medium mathematical space
            "\u{2060}", // word joiner
            "\u{3000}", // ideographic space
            "\u{FEFF}", // zero width non-breaking space
        ];

        test_cases.iter().for_each(|e| {
            let important_header = http_token("transfer-encoding");
            let header_name = format!("{}Transfer-Encoding{}", e, e);
            let header = http_token(header_name.as_str());
            let actual_distance = determine_similarity(important_header, header);
            assert_eq!(
                actual_distance,
                TokenSimilarity::SameLetters,
                "similarity({}) != TokenSimilarity::SameLetters",
                to_quoted_ascii(e.as_bytes()),
            );
        });
    }

    #[test]
    fn test_utf8_delimiters() {
        // https://www.fileformat.info/info/unicode/category/Pd/list.htm
        let test_cases = vec![
            "Transfer\u{058A}Encoding",  // armenian hyphen
            "Transfer\u{05BE}Encoding",  // hebrew punctuation maqaf
            "Transfer\u{1400}Encoding",  // canadian syllabics hyphen
            "Transfer\u{1806}Encoding",  // mongolian soft hyphen
            "Transfer\u{2010}Encoding",  // hyphen
            "Transfer\u{2011}Encoding",  // non-breaking hyphen
            "Transfer\u{2012}Encoding",  // figure dash
            "Transfer\u{2013}Encoding",  // en dash
            "Transfer\u{2014}Encoding",  // em dash
            "Transfer\u{2015}Encoding",  // horizontal bar
            "Transfer\u{2E17}Encoding",  // double oblique hyphen
            "Transfer\u{2E1A}Encoding",  // hyphen with diaeresis
            "Transfer\u{2E3A}Encoding",  // two-em dash
            "Transfer\u{2E3B}Encoding",  // three-em dash
            "Transfer\u{2E40}Encoding",  // double hyphen
            "Transfer\u{301C}Encoding",  // wave dash
            "Transfer\u{3030}Encoding",  // wavy dash
            "Transfer\u{30A0}Encoding",  // katakana-hiragana double hyphen
            "Transfer\u{FE31}Encoding",  // presentation form for vertical em dash
            "Transfer\u{FE32}Encoding",  // presentation form for vertical en dash
            "Transfer\u{FE58}Encoding",  // small em dash
            "Transfer\u{FE63}Encoding",  // small hyphen-minus
            "Transfer\u{FF0D}Encoding",  // fullwidth hyphen-minus
            "Transfer\u{10EAD}Encoding", // yezidi hyphenation mark
        ];

        test_cases.iter().for_each(|e| {
            let important_header = http_token("transfer-encoding");
            let header = http_token(e);
            let actual_distance = determine_similarity(important_header, header);
            assert_eq!(
                actual_distance,
                TokenSimilarity::SameLetters,
                "similarity({}) != TokenSimilarity::SameLetters",
                to_quoted_ascii(e.as_bytes()),
            );
        });
    }

    #[test]
    fn test_arbitrary_non_utf8_bytes() {
        let test_cases = vec![
            "Transfer\u{88}Encoding",
            "Transfer\u{88}\u{88}Encoding",
            "Transfer-Encoding\u{99}",
            "Transfer-Encoding\u{99}\u{99}\u{99}",
            "\u{a0}Transfer-Encoding",
            "\u{a0}\u{a0}\u{a0}\u{a0}Transfer-Encoding",
        ];

        test_cases.iter().for_each(|e| {
            let important_header = http_token("transfer-encoding");
            let mut header = vec![];
            e.as_bytes().iter().for_each(|x| {
                // convert UTF8 to a sequence of arbitrary bytes with the 7-bit set
                header.push(match *x {
                    0xc0..=0xcf => 0x99,
                    x => x,
                });
            });
            let actual_distance = determine_similarity(important_header, header.as_slice());
            assert_eq!(
                actual_distance,
                TokenSimilarity::SameLetters,
                "similarity({}) != TokenSimilarity::SameLetters",
                to_quoted_ascii(e.as_bytes()),
            );
        });
    }

    #[test]
    fn test_valid_te() {
        let test_cases = vec![
            ("chunked", "chunked", true),
            ("compress", "compress", true),
            ("deflate", "deflate", true),
            ("gzip", "gzip", true),
            ("identity", "identity", true),
            ("chunked;", "chunked", true),
            ("chunked;extension1;extension2;", "chunked", true),
            (" CHUNKED ", "chunked", true),
            (" chunked", "chunked", true),
            (" chunked\t", "chunked", true),
            (" chunked\t\t;\t\t", "chunked", true),
            ("xchunked", "unreachable", false),
            ("x_chunked", "unreachable", false),
        ];

        test_cases.iter().for_each(|(te, canonical, valid)| {
            let valid_te = is_valid_te(http_token(te));
            match valid_te {
                Some(v) => {
                    assert!(*valid, "Expected {}={}", to_quoted_ascii(v), *valid);
                    assert!(
                        v.eq_ignore_ascii_case(http_token(canonical)),
                        "{} != {}",
                        to_quoted_ascii(v),
                        canonical
                    );
                }
                None => {
                    assert!(!*valid, "Expected {}={}", te, *valid);
                }
            }
        });
    }

    #[test]
    fn test_trim() {
        let test_cases = vec![
            ("   abc \t\t\t", "abc"),
            ("\t \t \t   abc \t\t\t", "abc"),
            ("    \t\t\t", ""),
            ("", ""),
        ];

        test_cases.iter().for_each(|(value, result)| {
            assert_eq!(rfc_whitespace_trim(http_token(value)), http_token(result));
        });
    }

    #[test]
    fn test_parse_num() {
        let test_cases = vec![
            ("0", Ok(0)),
            ("1000", Ok(1000)),
            ("\t\t\t1000  ", Ok(1000)),
            ("123456789", Ok(123456789)),
            ("18446744073709551615", Ok(18446744073709551615)),
            ("18446744073709551616", Err("64-bits overflow")),
            (
                "123456789123456789123456789123456789",
                Err("64-bits overflow"),
            ),
            ("123,123", Err("Not an integer")),
            ("123.123", Err("Not an integer")),
            ("-1", Err("Not an integer")),
        ];
        test_cases.iter().for_each(|(num_str, result)| {
            assert_eq!(parse_num(http_token(num_str)), *result);
        });
    }

    #[test]
    fn test_to_ascii() {
        let test_cases = vec![
            ("abc", "abc"),
            ("abc ", "abc "),
            ("abc\x01", "abc\\0x01"),
            ("abc\\x01", "abc\\\\x01"),
            ("\raaaa\tbbb\nccc", "\\raaaa\\tbbb\\nccc"),
            ("\nabc\u{7f}", "\\nabc\\0x7f"),
            ("\u{0}abc", "\\0x00abc"),
            (
                "\u{01}abc\u{02}abcefd901\u{03}",
                "\\0x01abc\\0x02abcefd901\\0x03",
            ),
        ];
        test_cases.iter().for_each(|(value, result)| {
            assert_eq!(
                to_quoted_ascii(http_token(value)),
                format!("\"{}\"", result)
            );
        });

        let bytes = [0x81, b'a', b'b', b'c', 0x82, b'0', b'1', b'2', 0x83];
        assert_eq!(
            to_quoted_ascii(&bytes),
            format!("\"{}\"", "\\0x81abc\\0x82012\\0x83")
        );
    }

    #[test]
    fn test_obfuscate_value() {
        let test_cases = vec![
            ("A private message", "_ _ _"),
            ("A private message\\r", "_ _ _\\\\_"),
            ("\rSensitive\tuser\ndata", "\\r_\\t_\\n_"),
            ("\nPassword", "\\n_"),
            ("\rPIN", "\\r_"),
            ("\u{0}DL-ID", "\\0x00_"),
            ("\u{01}SSN\u{02}card-number\u{03}", "\\0x01_\\0x02_\\0x03"),
        ];
        test_cases.iter().for_each(|(value, result)| {
            assert_eq!(
                obfuscate_value(http_token(value)),
                format!("\"{}\"", result)
            );
        });
    }

    #[test]
    fn test_to_ascii_large_value() {
        let large_value = std::iter::repeat('\u{1}').take(500).collect::<String>();
        assert!(to_quoted_ascii(http_token(large_value.as_str())).len() < MESSAGE_MAX_SIZE / 2);
    }

    #[test]
    fn test_obfuscate_large_value() {
        let large_value = std::iter::repeat('\u{1}').take(500).collect::<String>();
        assert!(obfuscate_value(http_token(large_value.as_str())).len() < MESSAGE_MAX_SIZE / 2);
    }

    #[test]
    fn test_tchar() {
        let delims = b"-_.!#$%&'*+^`|~";
        for c in 0..=255 {
            assert_eq!(
                is_rfc_tchar(c),
                c.is_ascii_alphanumeric() || delims.contains(&(c as u8)),
                "Didn't work out for {}",
                (c as char)
            );
        }
    }

    #[test]
    fn test_header_value() {
        for c in 0..=255 {
            assert_eq!(
                is_valid_header_value_char(c),
                is_rfc_vchar(c) || is_rfc_obs_text(c) || is_rfc_whitespace(c),
                "Didn't work out for {}",
                (c as char)
            );
        }
    }
}


///
/// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
///
/// Licensed under the Apache License, Version 2.0 (the "License").
/// You may not use this file except in compliance with the License.
/// A copy of the License is located at
///
///  http://aws.amazon.com/apache2.0
///
/// or in the "license" file accompanying this file. This file is distributed
/// on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either
/// express or implied. See the License for the specific language governing
/// permissions and limitations under the License.
///
use crate::http_token_utils::HttpToken;
use crate::metrics::{ClassificationMetricsSettings, LoggingSettings, TierMetricsSettings};
use crate::request_analyzer::{HttpRequestData, RequestAnalysisResult};
use std::hash::Hash;
use std::slice;

pub mod http_token_utils;
mod metrics;
mod raw_request_parser;
pub mod request_analyzer;
pub mod ultralight_rate_limiter;
use crate::SettingsReturnCode::{ACCEPTED, REJECTED};
use strum_macros::EnumIter;

/// The maximum length of an error message.
pub const MESSAGE_MAX_SIZE: usize = 300;

/// The classification of HTTP requests.
#[repr(C)]
#[derive(PartialOrd, PartialEq, Clone, Copy, Debug, Hash, Eq, EnumIter)]
pub enum RequestSafetyTier {
    /// All headers are RFC compliant.
    Compliant,
    /// Some headers are not RFC compliant, but there are no known security risks coming
    /// from these violations
    Acceptable,
    /// Different HTTP engines may interpret the request boundaries differently.
    Ambiguous,
    /// Either malformed or contains highly suspicious headers.
    Severe,
}

/// The classification of HTTP headers.
#[repr(C)]
#[derive(Copy, PartialOrd, PartialEq, Clone, Debug)]
pub enum HeaderSafetyTier {
    /// RFC compliant header name and value.
    Compliant,
    /// Either header name or value is not compliant.
    NonCompliant,
    /// Bad headers MUST be removed before sending to backends.
    Bad,
}

/// The reason why a request received a particular Request Safety Tier.
#[repr(C)]
#[derive(Clone, Copy, Debug, PartialEq, Hash, Eq, EnumIter)]
pub enum ClassificationReason {
    /// Initial value
    Compliant,
    /// Header specific reason
    EmptyHeader,
    SuspiciousHeader,
    NonCompliantHeader,
    BadHeader,
    /// URI specific reasons
    AmbiguousUri,
    SpaceInUri,
    BadUri,
    NonCompliantVersion,
    BadVersion,
    /// Content Length specific reasons
    GetHeadZeroContentLength,
    UndefinedContentLengthSemantics,
    MultipleContentLength,
    DuplicateContentLength,
    BadContentLength,
    /// Transfer Encoding specific reasons
    UndefinedTransferEncodingSemantics,
    MultipleTransferEncodingChunked,
    BadTransferEncoding,
    /// Both Transfer Encoding and Content Length present
    BothTeClPresent,
    /// Http Method related
    BadMethod,
    /// Request parsing issues
    NonCrLfLineTermination,
    MultilineHeader,
    PartialHeaderLine,
    MissingLastEmptyLine,
    MissingHeaderColon,
    MissingUri,
}

/// A pointer to a string.
#[repr(C)]
#[derive(Clone)]
pub struct ExtString {
    pub length: u32,
    pub data_ptr: *const i8,
}

/// Represents an HTTP header.
#[repr(C)]
pub struct ExtHttpHeader {
    name: ExtString,
    value: ExtString,
    /// After classification each header is marked accordingly.
    compliant: HeaderSafetyTier,
}

/// A collection of HTTP headers.
#[repr(C)]
pub struct ExtHttpHeaders {
    count: u32,
    pairs: *mut ExtHttpHeader,
}

/// Represents and HTTP request for analysis.
#[repr(C)]
pub struct ExtHttpRequestData {
    /// Http Method, e.g. GET, POST, PUT, etc.
    method: ExtString,
    /// Protocol version HTTP/1.0 or HTTP/1.1.
    version: ExtString,
    /// Request URI.
    uri: ExtString,
    /// Http headers (assuming they were parsed by the HTTP engine).
    headers: ExtHttpHeaders,
}

/// The result of request analysis.
#[repr(C)]
pub struct ClassificationVerdict {
    /// The request safety tier.
    tier: RequestSafetyTier,
    /// Classification reason for tier upgrade.
    reason: ClassificationReason,
    /// Error message (might be empty).
    message_length: u32,
    message_data: [u8; MESSAGE_MAX_SIZE],
}

/// Unit structure for emitting Metrics.
#[repr(C)]
#[derive(Clone)]
pub struct ExtRequestAnalysisMetricsUnit<T>
where
    T: Hash + Eq,
{
    /// Method name
    pub method: ExtString,
    /// Request safety tier.
    pub counter_type: T,
    /// Count for the request safety tier.
    pub count: u32,
}

/// The library can be configured only once.
/// The return code indicates if settings were accepted,
/// or it was a subsequent call.
#[repr(C)]
pub enum SettingsReturnCode {
    ACCEPTED,
    REJECTED,
}

/// Configuration for logging.
#[repr(C)]
pub struct ExtLoggingSettings {
    /// A callback for logging messages.
    pub callback: Option<extern "C" fn(RequestSafetyTier, u32, *const u8)>,
}

/// Structured Request Safety Tier Metrics reporting.
#[repr(C)]
pub struct ExtTierMetricsSettings {
    /// Time period for emitting metrics in seconds.
    pub period_seconds: usize,
    /// A callback for emitting structured metrics for HeaderSafetyTier.
    pub callback:
        Option<extern "C" fn(u32, *const ExtRequestAnalysisMetricsUnit<RequestSafetyTier>)>,
}

/// Structured Classification Reason Metrics reporting.
#[repr(C)]
pub struct ExtClassificationMetricsSettings {
    /// Time period for emitting metrics in seconds.
    pub period_seconds: usize,
    /// A callback for emitting structured metrics for ClassificationReason.
    pub callback:
        Option<extern "C" fn(u32, *const ExtRequestAnalysisMetricsUnit<ClassificationReason>)>,
}

impl ExtString {
    pub fn from_slice(s: &str) -> Self {
        debug_assert!(
            s.len() < 0xffff,
            "In context of HTTP headers we do not expect more than 64k of data"
        );
        Self {
            length: s.len() as u32,
            data_ptr: s.as_ptr() as *const i8,
        }
    }

    pub fn from_http_token(s: HttpToken) -> Self {
        debug_assert!(
            s.len() < 0xffff,
            "In context of HTTP headers we do not expect more than 64k of data"
        );
        Self {
            length: s.len() as u32,
            data_ptr: s.as_ptr() as *const i8,
        }
    }

    fn as_http_token(&self, name: &'static str) -> HttpToken {
        assert!(
            self.length == 0 || !self.data_ptr.is_null(),
            "Bad {}: length is {}, but the pointer is NULL",
            name,
            self.length
        );

        if self.length > 0 {
            // `HttpToken` is not going to outlive the pointer in the context of this library
            // as they exist only during analysis
            // Also we checked it for not being NULL.
            unsafe { slice::from_raw_parts(self.data_ptr as *const u8, self.length as usize) }
        } else {
            b""
        }
    }
}

impl ExtHttpHeaders {
    fn to_slice(&self) -> &[ExtHttpHeader] {
        assert!(
            self.count == 0 || !self.pairs.is_null(),
            "Headers count is {}, but the pointer is NULL",
            self.count
        );
        if self.count > 0 {
            // The slice is not going to outlive the underlying pointer.
            unsafe { slice::from_raw_parts(self.pairs, self.count as usize) }
        } else {
            &[]
        }
    }
}

/// Analyzes a given HTTP request.
/// # Arguments
/// * `request` a pointer to request
/// * `verdict` a pointer to verdict placeholder (being populated by this method)
/// Both arguments are being changed during execution of this method.
/// # Safety
/// As long as request is well-formed and verdict is a valid pointer.
/// Which means no NULLs are allowed for names or values. Pass an empty string instead:
/// Also headers must be fully initialized, e.g.
/// ```c
/// #define http_desync_guardian_string(str) { .length = sizeof(str) - 1, .data_ptr = (uint8_t *) (str) }
///
/// guardian_header = {
///    .name = http_desync_guardian_string("Connection"),
///    .value = http_desync_guardian_string(" keep-alive"),
/// },
/// ```
/// # Note
/// This method may abort execution, if any parameter is invalid. Which includes:
/// 1. Any input is `NULL`
/// 2. Header count > 0, but the pointer is `NULL`
/// 3. Any string has > 0 length, but the pointer is `NULL`
#[no_mangle]
pub extern "C" fn http_desync_guardian_analyze_request(
    request: Option<&mut ExtHttpRequestData>,
    verdict: Option<&mut ClassificationVerdict>,
) {
    let request = request.expect("Request must not be NULL");
    let verdict = verdict.expect("Verdict must not be NULL");

    let mut request_for_analysis = HttpRequestData::new(request);

    let verdict_internal = request_for_analysis.analyze_parsed_request();

    // copy for consumption in C
    let request_headers = if request.headers.count > 0 {
        // The slice is not going to outlive the underlying pointer.
        unsafe { slice::from_raw_parts_mut(request.headers.pairs, request.headers.count as usize) }
    } else {
        &mut []
    };

    debug_assert_eq!(request_headers.len(), request_for_analysis.headers.len());
    for (idx, analyzed_header) in request_for_analysis.headers.iter().enumerate() {
        request_headers[idx].compliant = analyzed_header.get_tier();
    }

    populate_external_verdict(verdict, verdict_internal);
}

/// Parses and analyzes a given HTTP request.
/// # Arguments
/// * `size` length of the request header. Constant.
/// * `request_buffer` a pointer to the request buffer. Constant.
/// * `verdict` a pointer to verdict placeholder (being populated by this method). Mutable.
///
/// # Safety
/// As long as `request_buffer` and `verdict` are valid pointers and the size is a positive integer.
///
/// # Note
/// This method may abort execution, if any parameter is invalid. Which includes:
/// 1. `request_buffer` is `NULL`
/// 1. `verdict` is `NULL`
/// 1. `size` is `0`
#[no_mangle]
#[allow(clippy::not_unsafe_ptr_arg_deref)]
pub extern "C" fn http_desync_guardian_analyze_raw_request(
    size: u32,
    request_buffer: *const u8,
    verdict: Option<&mut ClassificationVerdict>,
) {
    assert!(size > 0, "Request size cannot be 0");
    assert!(!request_buffer.is_null(), "Request buffer must not be NULL");
    let verdict = verdict.expect("Verdict must not be NULL");

    // `http_request_buffer` is not going to outlive the underlying pointer.
    // Also we checked it for not being NULL.
    let http_request_buffer = unsafe { slice::from_raw_parts(request_buffer, size as usize) };

    let verdict_internal = HttpRequestData::analyze_raw_request(http_request_buffer);

    populate_external_verdict(verdict, verdict_internal);
}

fn populate_external_verdict(
    verdict: &mut ClassificationVerdict,
    verdict_internal: RequestAnalysisResult,
) {
    verdict.tier = verdict_internal.tier;
    verdict.reason = verdict_internal.reason;
    if let Some(message) = verdict_internal.message {
        verdict.message_length = (MESSAGE_MAX_SIZE - 1).min(message.len()) as u32;

        unsafe {
            // we checked the length of the source and the destination
            std::ptr::copy(
                message.as_ptr(),
                verdict.message_data.as_mut_ptr(),
                verdict.message_length as usize,
            );
        }
    } else {
        verdict.message_length = 0;
    }
    // \0 string termination. Just in case it might be misused in sprintf with `%s` instead of `%.*s`
    verdict.message_data[verdict.message_length as usize] = 0;
}

/// Return request information with Customer sensitive data obfuscated
/// The callback must be non-null.
/// Populates a buffer with the data in an HTTP Request that has been analyzed.
/// # Arguments
/// * `request` a pointer to request
/// * `buffer_len` the size of the buffer
/// * `buffer` a raw pointer to the buffer being written to
/// `request` and `buffer` are being changed during execution of this method.
/// # Safety
/// Request needs to be well-formed.
/// Which means no NULLs are allowed for names or values. Pass an empty string instead:
/// `http_desync_guardian_string_t empty = {.length = 0, .data_ptr = NULL};`
/// Also headers must be fully initialized, e.g.
/// ```c
/// guardian_header = {
///    .name = http_desync_guardian_string("Connection"),
///    .value = http_desync_guardian_string(" keep-alive"),
/// },
/// ```
/// # Note
/// This method may abort execution, if any parameter is invalid. Which includes:
/// 1. Any input is `NULL`
/// 2. Header count > 0, but the pointer is `NULL`
/// 3. Any string has > 0 length, but the pointer is `NULL`
/// 4. `buffer_len` is less than or equal to 0
/// 5. `buffer` is `NULL`
#[no_mangle]
#[allow(clippy::not_unsafe_ptr_arg_deref)]
pub extern "C" fn http_desync_guardian_print_request(
    request: Option<&mut ExtHttpRequestData>,
    buffer_len: usize,
    buffer: *mut u8,
) -> u32 {
    let request = request.expect("Request must not be NULL");
    assert!(!buffer.is_null(), "Buffer must not be NULL");
    assert!(buffer_len > 0, "Buffer length cannot be empty");

    let request_data = HttpRequestData::new(request).to_string();
    let data_len: u32 = (buffer_len - 1).min(request_data.len()) as u32;
    unsafe {
        // we checked the length of the source and the destination
        std::ptr::copy(request_data.as_ptr(), buffer, data_len as usize);
        // \0 string termination. Just in case it might be misused in sprintf with `%s` instead of `%.*s`
        buffer.add(data_len as usize).write(0);
    }
    data_len
}

/// Configure logging parameters. Only first call matters (subsequent are ignored).
/// The `settings` and `settings.callback` must be non-null.
/// # Returns
/// Either `ACCEPTED` for the first call or `REJECTED` for any subsequent ones.
#[no_mangle]
pub extern "C" fn http_desync_guardian_initialize_logging_settings(
    settings: Option<&ExtLoggingSettings>,
) -> SettingsReturnCode {
    match LoggingSettings::set(settings.expect("Settings can not be NULL")) {
        Ok(_) => ACCEPTED,
        Err(_) => REJECTED,
    }
}

/// Configure Aggregated Request Safety Tier metrics reporting.
/// Only first call matters (subsequent calls are ignored).
/// The callback must be non-null.
/// # Returns
/// Either `ACCEPTED` for the first call or `REJECTED` for any subsequent ones.
#[no_mangle]
pub extern "C" fn http_desync_guardian_register_tier_metrics_callback(
    settings: Option<&ExtTierMetricsSettings>,
) -> SettingsReturnCode {
    match TierMetricsSettings::set(settings.expect("Settings can not be NULL")) {
        Ok(_) => ACCEPTED,
        Err(_) => REJECTED,
    }
}

/// Configure Aggregated Classification Reason metrics reporting.
/// Only first call matters (subsequent calls are ignored).
/// # Panics
/// If the parameter or the callback is `NULL`.
/// # Returns
/// Either `ACCEPTED` for the first call or `REJECTED` for any subsequent ones.
#[no_mangle]
pub extern "C" fn http_desync_guardian_register_classification_metrics_callback(
    settings: Option<&ExtClassificationMetricsSettings>,
) -> SettingsReturnCode {
    match ClassificationMetricsSettings::set(settings.expect("Settings can not be NULL")) {
        Ok(_) => ACCEPTED,
        Err(_) => REJECTED,
    }
}

#[cfg(test)]
mod tests {
    use crate::http_token_utils::{http_token, to_quoted_ascii, HttpToken};
    use crate::{
        http_desync_guardian_analyze_raw_request, http_desync_guardian_analyze_request,
        http_desync_guardian_initialize_logging_settings, http_desync_guardian_print_request,
        http_desync_guardian_register_classification_metrics_callback,
        http_desync_guardian_register_tier_metrics_callback, ClassificationReason,
        ClassificationVerdict, ExtClassificationMetricsSettings, ExtHttpHeader, ExtHttpHeaders,
        ExtHttpRequestData, ExtLoggingSettings, ExtRequestAnalysisMetricsUnit, ExtString,
        ExtTierMetricsSettings, HeaderSafetyTier, HttpRequestData, RequestSafetyTier,
        MESSAGE_MAX_SIZE,
    };
    use std::ptr::{null, null_mut};
    use std::sync::atomic::{AtomicU32, Ordering};

    static LOG_COUNT: AtomicU32 = AtomicU32::new(0);
    static TIER_COUNT: AtomicU32 = AtomicU32::new(0);
    static CLASSIFICATION_COUNT: AtomicU32 = AtomicU32::new(0);

    extern "C" fn handle_log_messages(tier: RequestSafetyTier, n: u32, msg: *const u8) {
        let string = ExtString {
            length: n,
            data_ptr: msg as *const i8,
        };
        LOG_COUNT.fetch_add(1, Ordering::SeqCst);
        println!(
            "{:?} {}",
            tier,
            to_quoted_ascii(string.as_http_token("Log message"))
        );
    }

    extern "C" fn handle_structured_tier_metrics(
        size: u32,
        tier_metrics_list: *const ExtRequestAnalysisMetricsUnit<RequestSafetyTier>,
    ) {
        let slice = unsafe { std::slice::from_raw_parts(tier_metrics_list, size as usize) };
        assert!(!slice.is_empty());

        TIER_COUNT.fetch_add(1, Ordering::SeqCst);
    }

    extern "C" fn handle_structured_classification_metrics(
        size: u32,
        classification_metrics_list: *const ExtRequestAnalysisMetricsUnit<ClassificationReason>,
    ) {
        let slice =
            unsafe { std::slice::from_raw_parts(classification_metrics_list, size as usize) };
        assert!(!slice.is_empty());

        CLASSIFICATION_COUNT.fetch_add(1, Ordering::SeqCst);
    }

    /// Simple test to verify serialization-deserialization
    /// of data structures between C and Rust.
    #[test]
    fn test_http_desync_guardian_analyze_request() {
        let mut header = ExtHttpHeader {
            name: ExtString::from_slice("Transfer-Encoding"),
            value: ExtString::from_slice("xchunked"),
            compliant: HeaderSafetyTier::Compliant,
        };
        let parts_mut = &mut header as *mut _;
        let mut request = ExtHttpRequestData {
            method: ExtString::from_slice("POST"),
            version: ExtString::from_slice("HTTP/1.1"),
            uri: ExtString::from_slice("/foo/bar"),
            headers: ExtHttpHeaders {
                count: 1,
                pairs: parts_mut,
            },
        };
        let mut verdict = ClassificationVerdict {
            tier: RequestSafetyTier::Compliant,
            reason: ClassificationReason::Compliant,
            message_length: 0,
            message_data: [0; MESSAGE_MAX_SIZE],
        };

        http_desync_guardian_analyze_request(Some(&mut request), Some(&mut verdict));

        assert_eq!(verdict.tier, RequestSafetyTier::Severe);
        assert!(verdict.message_length > 0);
        assert_eq!(verdict.reason, ClassificationReason::BadTransferEncoding);
        assert_eq!(
            HttpRequestData::new(&request)
                .headers
                .get(0)
                .unwrap()
                .get_tier(),
            HeaderSafetyTier::Bad
        );

        const BUFFER_LEN: usize = 4096;
        let mut buffer = [0; BUFFER_LEN];
        let request_data_len =
            http_desync_guardian_print_request(Some(&mut request), BUFFER_LEN, buffer.as_mut_ptr());
        let request_data_string =
            std::str::from_utf8(buffer.as_ref()).expect("ASCII -> UTF8 never fails");

        assert!(request_data_len > 0);
        assert!(request_data_string.contains("xchunked"));

        let msg_part = http_token("Transfer-Encoding");
        let found = find_message(&verdict, &msg_part);
        assert!(
            found,
            "{} was not found in the message",
            to_quoted_ascii(msg_part)
        );

        assert_eq!(
            request.headers.to_slice()[0].compliant,
            HeaderSafetyTier::Bad
        );
    }

    #[test]
    fn test_log_callback() {
        let logging_settings = ExtLoggingSettings {
            callback: Some(handle_log_messages),
        };

        http_desync_guardian_initialize_logging_settings(Some(&logging_settings));

        analyze_vanilla_request();

        assert_ne!(LOG_COUNT.load(Ordering::SeqCst), 0);
    }

    #[test]
    fn test_request_tier_metrics_callback() {
        let tier_metrics_settings = ExtTierMetricsSettings {
            period_seconds: 1,
            callback: Some(handle_structured_tier_metrics),
        };

        http_desync_guardian_register_tier_metrics_callback(Some(&tier_metrics_settings));

        analyze_vanilla_request();

        assert_ne!(TIER_COUNT.load(Ordering::SeqCst), 0);
    }

    #[test]
    fn test_classification_reason_metrics_callback() {
        let classification_metrics_settings = ExtClassificationMetricsSettings {
            period_seconds: 1,
            callback: Some(handle_structured_classification_metrics),
        };

        http_desync_guardian_register_classification_metrics_callback(Some(
            &classification_metrics_settings,
        ));

        analyze_vanilla_request();

        assert_ne!(CLASSIFICATION_COUNT.load(Ordering::SeqCst), 0);
    }

    fn analyze_vanilla_request() {
        let mut header = ExtHttpHeader {
            name: ExtString::from_slice("Transfer-Encoding"),
            value: ExtString::from_slice("xchunked"),
            compliant: HeaderSafetyTier::Compliant,
        };
        let parts_mut = &mut header as *mut _;
        let mut request = ExtHttpRequestData {
            method: ExtString::from_slice("POST"),
            version: ExtString::from_slice("HTTP/1.1"),
            uri: ExtString::from_slice("/foo/bar"),
            headers: ExtHttpHeaders {
                count: 1,
                pairs: parts_mut,
            },
        };
        let mut verdict = ClassificationVerdict {
            tier: RequestSafetyTier::Compliant,
            reason: ClassificationReason::Compliant,
            message_length: 0,
            message_data: [0; MESSAGE_MAX_SIZE],
        };

        http_desync_guardian_analyze_request(Some(&mut request), Some(&mut verdict));
    }

    #[test]
    fn test_http_desync_guardian_analyze_raw_request() {
        let mut verdict = ClassificationVerdict {
            tier: RequestSafetyTier::Compliant,
            reason: ClassificationReason::Compliant,
            message_length: 0,
            message_data: [0; MESSAGE_MAX_SIZE],
        };

        const GET_REQUEST: HttpToken =
            http_token("GET /foo/bar HTTP/1.1\r\nHost: localhost\x00GET /smuggle HTTP/1.1\r\n\r\n");

        http_desync_guardian_analyze_raw_request(
            GET_REQUEST.len() as u32,
            GET_REQUEST.as_ptr(),
            Some(&mut verdict),
        );

        assert_eq!(verdict.tier, RequestSafetyTier::Severe);
        assert!(verdict.message_length > 0);
        assert_eq!(verdict.reason, ClassificationReason::BadHeader);
        let msg_part = http_token("Host");
        let found = find_message(&mut verdict, &msg_part);

        assert!(
            found,
            "{} was not found in the message",
            to_quoted_ascii(msg_part)
        );
    }

    fn find_message(verdict: &ClassificationVerdict, msg_part: &[u8]) -> bool {
        let mut found = false;
        for i in 0..=verdict.message_length as usize - msg_part.len() {
            if &verdict.message_data[i..i + msg_part.len()] == msg_part {
                found = true;
                break;
            }
        }
        found
    }

    #[test]
    fn test_http_desync_guardian_validate_print_request_insufficient_buffer_length() {
        let mut header = ExtHttpHeader {
            name: ExtString::from_slice("Transfer-Encoding"),
            value: ExtString::from_slice("chunked"),
            compliant: HeaderSafetyTier::Compliant,
        };
        let parts_mut = &mut header as *mut _;
        let mut request = ExtHttpRequestData {
            method: ExtString::from_slice("GET"),
            version: ExtString::from_slice("HTTP/1.1"),
            uri: ExtString::from_slice("/foo/bar"),
            headers: ExtHttpHeaders {
                count: 1,
                pairs: parts_mut,
            },
        };

        const BUFFER_LEN: usize = 10;
        let mut buffer = [0; BUFFER_LEN];
        let request_data_len =
            http_desync_guardian_print_request(Some(&mut request), BUFFER_LEN, buffer.as_mut_ptr());

        assert_eq!(request_data_len, 9);
    }

    #[test]
    fn test_non_compliant_header_tier() {
        // Create a valid header with HeaderSafetyTier set to non-compliant
        let mut header = ExtHttpHeader {
            name: ExtString::from_slice("Transfer-Encoding"),
            value: ExtString::from_slice("chunked"),
            compliant: HeaderSafetyTier::NonCompliant,
        };
        let parts_mut = &mut header as *mut _;

        let request = &ExtHttpRequestData {
            method: ExtString::from_slice("GET"),
            version: ExtString::from_slice("HTTP/1.1"),
            uri: ExtString::from_slice("/foo/bar"),
            headers: ExtHttpHeaders {
                count: 1,
                pairs: parts_mut,
            },
        };

        assert_eq!(
            HttpRequestData::new(request)
                .headers
                .get(0)
                .unwrap()
                .get_tier(),
            HeaderSafetyTier::NonCompliant
        );
    }

    #[test]
    fn test_invalid_header_tier() {
        // Create an unsafe, invalid enum entry
        let mut invalid_tier: HeaderSafetyTier = HeaderSafetyTier::Compliant;
        let ptr = (&mut invalid_tier) as *mut _ as *mut u8;
        unsafe {
            ptr.write(255);
        }
        assert_ne!(invalid_tier, HeaderSafetyTier::Compliant);

        let mut header = ExtHttpHeader {
            name: ExtString::from_slice("Transfer-Encoding"),
            value: ExtString::from_slice("chunked"),
            compliant: invalid_tier,
        };
        let parts_mut = &mut header as *mut _;

        let request = &ExtHttpRequestData {
            method: ExtString::from_slice("GET"),
            version: ExtString::from_slice("HTTP/1.1"),
            uri: ExtString::from_slice("/foo/bar"),
            headers: ExtHttpHeaders {
                count: 1,
                pairs: parts_mut,
            },
        };

        assert_eq!(
            HttpRequestData::new(request)
                .headers
                .get(0)
                .unwrap()
                .get_tier(),
            HeaderSafetyTier::Compliant
        );
    }

    #[test]
    #[should_panic(expected = "Callback to send logs is required")]
    fn test_logger_callback_required() {
        let logging_settings = ExtLoggingSettings { callback: None };

        http_desync_guardian_initialize_logging_settings(Some(&logging_settings));
    }

    #[test]
    #[should_panic(expected = "Settings can not be NULL")]
    fn test_logger_settings_required() {
        http_desync_guardian_initialize_logging_settings(None);
    }

    #[test]
    #[should_panic(expected = "Callback to send tier metrics is required")]
    fn test_tier_metrics_callback_required() {
        let logging_settings = ExtTierMetricsSettings {
            period_seconds: 10,
            callback: None,
        };

        http_desync_guardian_register_tier_metrics_callback(Some(&logging_settings));
    }

    #[test]
    #[should_panic(expected = "Settings can not be NULL")]
    fn test_tier_metrics_settings_required() {
        http_desync_guardian_register_tier_metrics_callback(None);
    }

    #[test]
    #[should_panic(expected = "Callback to send classification metrics is required")]
    fn test_classification_metrics_callback_required() {
        let logging_settings = ExtClassificationMetricsSettings {
            period_seconds: 10,
            callback: None,
        };

        http_desync_guardian_register_classification_metrics_callback(Some(&logging_settings));
    }

    #[test]
    #[should_panic(expected = "Settings can not be NULL")]
    fn test_classification_metrics_settings_required() {
        http_desync_guardian_register_classification_metrics_callback(None);
    }

    #[test]
    #[should_panic(expected = "Request must not be NULL")]
    fn test_http_desync_guardian_validate_print_request_parameters_null_request() {
        http_desync_guardian_print_request(None, 1, [0].as_mut_ptr());
    }

    #[test]
    #[should_panic(expected = "Buffer must not be NULL")]
    fn test_http_desync_guardian_validate_print_request_parameters_null_buffer() {
        let mut request = new_request();
        http_desync_guardian_print_request(Some(&mut request), 1, std::ptr::null_mut());
    }

    #[test]
    #[should_panic(expected = "Buffer length cannot be empty")]
    fn test_http_desync_guardian_validate_print_request_parameters_invalid_buffer_length() {
        let mut request = new_request();
        http_desync_guardian_print_request(Some(&mut request), 0, [0].as_mut_ptr());
    }

    #[test]
    #[should_panic(expected = "Request must not be NULL")]
    fn test_http_desync_guardian_validate_request_analysis_parameters_null_request() {
        let mut verdict = new_verdict();
        http_desync_guardian_analyze_request(None, Some(&mut verdict));
    }

    #[test]
    #[should_panic(expected = "Verdict must not be NULL")]
    fn test_http_desync_guardian_validate_request_analysis_parameters_null_verdict() {
        let mut request = new_request();
        http_desync_guardian_analyze_request(Some(&mut request), None);
    }

    #[test]
    #[should_panic(expected = "Bad method: length is 3, but the pointer is NULL")]
    fn test_http_desync_guardian_validate_request_analysis_parameters_null_method() {
        let mut request = new_request();
        request.method = ExtString {
            length: 3,
            data_ptr: null(),
        };
        let mut verdict = new_verdict();
        http_desync_guardian_analyze_request(Some(&mut request), Some(&mut verdict));
    }

    #[test]
    #[should_panic(expected = "Bad version: length is 8, but the pointer is NULL")]
    fn test_http_desync_guardian_validate_request_analysis_parameters_null_version() {
        let mut request = new_request();
        request.version = ExtString {
            length: 8,
            data_ptr: null(),
        };
        let mut verdict = new_verdict();
        http_desync_guardian_analyze_request(Some(&mut request), Some(&mut verdict));
    }

    #[test]
    #[should_panic(expected = "Bad URI: length is 1, but the pointer is NULL")]
    fn test_http_desync_guardian_validate_request_analysis_parameters_null_uri() {
        let mut request = new_request();
        request.uri = ExtString {
            length: 1,
            data_ptr: null(),
        };
        let mut verdict = new_verdict();
        http_desync_guardian_analyze_request(Some(&mut request), Some(&mut verdict));
    }

    #[test]
    #[should_panic(expected = "Bad header name: length is 10, but the pointer is NULL")]
    fn test_http_desync_guardian_validate_request_analysis_parameters_null_header_name() {
        let mut header = ExtHttpHeader {
            name: ExtString {
                length: 10,
                data_ptr: null(),
            },
            value: ExtString::from_slice("chunked"),
            compliant: HeaderSafetyTier::Compliant,
        };
        let parts_mut = &mut header as *mut _;
        let mut request = new_request();
        request.headers = ExtHttpHeaders {
            count: 1,
            pairs: parts_mut,
        };
        let mut verdict = new_verdict();
        http_desync_guardian_analyze_request(Some(&mut request), Some(&mut verdict));
    }

    #[test]
    #[should_panic(expected = "Bad header value: length is 10, but the pointer is NULL")]
    fn test_http_desync_guardian_validate_request_analysis_parameters_null_header_value() {
        let mut header = ExtHttpHeader {
            name: ExtString::from_slice("X-My-Header"),
            value: ExtString {
                length: 10,
                data_ptr: null(),
            },
            compliant: HeaderSafetyTier::Compliant,
        };
        let parts_mut = &mut header as *mut _;
        let mut request = new_request();
        request.headers = ExtHttpHeaders {
            count: 1,
            pairs: parts_mut,
        };
        let mut verdict = new_verdict();
        http_desync_guardian_analyze_request(Some(&mut request), Some(&mut verdict));
    }

    #[test]
    #[should_panic(expected = "Request buffer must not be NULL")]
    fn test_http_desync_guardian_validate_request_raw_request_null_buffer() {
        let mut verdict = new_verdict();
        http_desync_guardian_analyze_raw_request(100, null(), Some(&mut verdict));
    }

    #[test]
    #[should_panic(expected = "Verdict must not be NULL")]
    fn test_http_desync_guardian_validate_request_raw_request_null_verdict() {
        http_desync_guardian_analyze_raw_request(100, [0].as_ptr(), None);
    }

    #[test]
    #[should_panic(expected = "Request size cannot be 0")]
    fn test_http_desync_guardian_validate_request_raw_request_zero_buffer_size() {
        let mut verdict = new_verdict();
        http_desync_guardian_analyze_raw_request(0, [0].as_ptr(), Some(&mut verdict));
    }

    fn new_verdict() -> ClassificationVerdict {
        ClassificationVerdict {
            tier: RequestSafetyTier::Compliant,
            reason: ClassificationReason::Compliant,
            message_length: 0,
            message_data: [0; MESSAGE_MAX_SIZE],
        }
    }

    fn new_request() -> ExtHttpRequestData {
        ExtHttpRequestData {
            method: ExtString::from_slice("GET"),
            version: ExtString::from_slice("HTTP/1.1"),
            uri: ExtString::from_slice("/"),
            headers: ExtHttpHeaders {
                count: 0,
                pairs: null_mut(),
            },
        }
    }
}


///
/// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
///
/// Licensed under the Apache License, Version 2.0 (the "License").
/// You may not use this file except in compliance with the License.
/// A copy of the License is located at
///
///  http://aws.amazon.com/apache2.0
///
/// or in the "license" file accompanying this file. This file is distributed
/// on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either
/// express or implied. See the License for the specific language governing
/// permissions and limitations under the License.
///
use crate::http_token_utils::{http_token, HttpToken};
use crate::request_analyzer::{HttpRequestData, RequestAnalysisResult};
use crate::ultralight_rate_limiter::UltraLightRateLimiter;
use crate::{
    ClassificationReason, ExtClassificationMetricsSettings, ExtLoggingSettings,
    ExtRequestAnalysisMetricsUnit, ExtString, ExtTierMetricsSettings, RequestSafetyTier,
    MESSAGE_MAX_SIZE,
};
use core::hash::Hash;
use lazy_static::lazy_static;
use smallvec::SmallVec;
use std::collections::HashMap;
use std::sync::atomic::AtomicPtr;
use std::sync::atomic::{AtomicU32, AtomicU64, Ordering};
use std::time::Instant;

const LOG_MESSAGE_RATE_LIMIT: f64 = 100. / 1.;
/// How many entries we keep on stack before spilling over to the heap.
const METRICS_VECTOR_SIZE: usize = 128;
pub const MONITORED_METHODS: &[&str] = &[
    "GET", "PUT", "POST", "PATCH", "DELETE", "HEAD", "OPTIONS", "OTHER",
];

/// Static logging settings (initialized once by an external caller).
static LOGGER_SETTINGS: AtomicSettings<LoggingSettings> = AtomicSettings::empty();
static TIER_METRICS_SETTINGS: AtomicSettings<TierMetricsSettings> = AtomicSettings::empty();
static CLASSIFICATION_METRICS_SETTINGS: AtomicSettings<ClassificationMetricsSettings> =
    AtomicSettings::empty();

pub struct AtomicSettings<T> {
    settings: AtomicPtr<T>,
}

impl<T> AtomicSettings<T> {
    pub const fn empty() -> Self {
        Self {
            settings: AtomicPtr::new(std::ptr::null_mut()),
        }
    }

    #[inline(always)]
    pub fn get(&self) -> Option<&'static T> {
        let ptr = self.settings.load(Ordering::Relaxed);
        if ptr.is_null() {
            None
        } else {
            // a pointer is set only once, that's why it's safe to dereference it
            unsafe { Some(&(*ptr)) }
        }
    }

    /// A test version of the `store` method that always sets the value
    #[cfg(test)]
    pub fn store(&self, new_value: T) -> Result<(), &str> {
        // in tests we allow to re-write the value
        self.settings
            .store(Box::into_raw(Box::new(new_value)), Ordering::Relaxed);
        Ok(())
    }

    /// Sets the value if it was not set before.
    #[cfg(not(test))]
    // cov: begin-ignore-line
    pub fn store(&self, new_value: T) -> Result<(), &str> {
        // we allow the value to be set only once in production
        // to avoid dealing with race conditions during swaps
        // or overhead to ensure there are no race conditions
        // a simple model - write once, read-only afterwards
        // value is set only if settings were originally null.
        if self
            .settings
            .compare_and_swap(
                std::ptr::null_mut(),
                Box::into_raw(Box::new(new_value)),
                Ordering::Relaxed,
            )
            .is_null()
        {
            Ok(())
        } else {
            Err("Can be set only once.")
        }
    }
    // cov: end-ignore-line
}

lazy_static! {
    pub static ref TIER_STATS: RequestAnalysisStatisticsStore<RequestSafetyTier> =
        RequestAnalysisStatisticsStore::<RequestSafetyTier>::new();
}

lazy_static! {
    pub static ref CLASSIFICATION_STATS: RequestAnalysisStatisticsStore<ClassificationReason> =
        RequestAnalysisStatisticsStore::<ClassificationReason>::new();
}

lazy_static! {
    pub static ref START_TIME: Instant = Instant::now();
}

/// Logging settings
pub struct LoggingSettings {
    rate_limiter: UltraLightRateLimiter,
    callback: extern "C" fn(RequestSafetyTier, u32, *const u8),
}

/// Request Safety Tier metrics settings
pub struct TierMetricsSettings {
    rate_limiter: UltraLightRateLimiter,
    callback: extern "C" fn(u32, *const ExtRequestAnalysisMetricsUnit<RequestSafetyTier>),
}

/// Classification Reason metrics settings
pub struct ClassificationMetricsSettings {
    rate_limiter: UltraLightRateLimiter,
    callback: extern "C" fn(u32, *const ExtRequestAnalysisMetricsUnit<ClassificationReason>),
}

pub struct RequestAnalysisCounterPerMethod<T>
where
    T: Hash + Eq + strum::IntoEnumIterator,
{
    method: HttpToken<'static>,
    counters_map: HashMap<T, AtomicU32>,
}

/// Unit structure for emitting Metrics.
pub struct RequestAnalysisMetricsUnit<T>
where
    T: Hash + Eq,
{
    /// Method name
    pub method: HttpToken<'static>,
    /// Request safety tier.
    pub counter_type: T,
    /// Count for the request safety tier.
    pub count: u32,
}

/// Contains aggregated statistics for processing requests metrics in a HashMap.
/// Such as counter per category, rejection reasons, etc.
/// Mapped to the corresponding RequestSafetyTier.
pub struct RequestAnalysisStatisticsStore<T>
where
    T: Hash + Eq + strum::IntoEnumIterator,
{
    stats: Vec<RequestAnalysisCounterPerMethod<T>>,
}

pub trait CounterType {
    fn get_counter_type(result: &RequestAnalysisResult) -> Self;
}

impl CounterType for RequestSafetyTier {
    #[cfg_attr(feature = "coverage", inline(never))]
    #[cfg_attr(not(feature = "coverage"), inline(always))]
    fn get_counter_type(result: &RequestAnalysisResult) -> Self {
        result.tier
    }
}

impl CounterType for ClassificationReason {
    #[cfg_attr(feature = "coverage", inline(never))]
    #[cfg_attr(not(feature = "coverage"), inline(always))]
    fn get_counter_type(result: &RequestAnalysisResult) -> Self {
        result.reason
    }
}

pub trait MetricsCallback<T>
where
    T: Hash + Eq,
{
    fn is_enabled() -> bool;
    fn get_metrics_callback() -> Option<extern "C" fn(u32, *const ExtRequestAnalysisMetricsUnit<T>)>;
}

impl MetricsCallback<RequestSafetyTier> for RequestSafetyTier {
    #[inline(always)]
    fn is_enabled() -> bool {
        TierMetricsSettings::get().is_some()
    }

    #[inline(always)]
    fn get_metrics_callback(
    ) -> Option<extern "C" fn(u32, *const ExtRequestAnalysisMetricsUnit<RequestSafetyTier>)> {
        if let Some(settings) = TierMetricsSettings::get() {
            settings.rate_limiter.try_acquire_value(settings.callback)
        } else {
            None
        }
    }
}

impl MetricsCallback<ClassificationReason> for ClassificationReason {
    #[inline(always)]
    fn is_enabled() -> bool {
        ClassificationMetricsSettings::get().is_some()
    }

    #[inline(always)]
    fn get_metrics_callback(
    ) -> Option<extern "C" fn(u32, *const ExtRequestAnalysisMetricsUnit<ClassificationReason>)>
    {
        if let Some(settings) = ClassificationMetricsSettings::get() {
            settings.rate_limiter.try_acquire_value(settings.callback)
        } else {
            None
        }
    }
}

impl LoggingSettings {
    /// Initializes the logging settings.
    /// The function is expected to be called once, any subsequent calls don't change the settings.
    ///
    /// # Parameters
    /// `settings` - a settings struct
    /// The `callback` must be non-null.
    pub fn set(settings: &ExtLoggingSettings) -> Result<(), &str> {
        let callback = settings
            .callback
            .expect("Callback to send logs is required");
        let settings = LoggingSettings {
            rate_limiter: UltraLightRateLimiter::new(
                LOG_MESSAGE_RATE_LIMIT,
                get_current_time_millis,
            ),
            callback,
        };
        LOGGER_SETTINGS.store(settings)
    }

    #[inline(always)]
    pub fn log_message(tier: RequestSafetyTier, msg: &str) {
        if let Some(s) = LOGGER_SETTINGS.get() {
            s.try_log_message(tier, msg);
        }
    }

    #[inline(always)]
    fn try_log_message(&self, tier: RequestSafetyTier, msg: &str) {
        if self.rate_limiter.try_acquire() {
            (self.callback)(tier, msg.len().min(MESSAGE_MAX_SIZE) as u32, msg.as_ptr());
        }
    }
}

impl TierMetricsSettings {
    /// Initializes Request Safety Tier Metrics settings.
    /// The function is expected to be called once, any subsequent calls don't change the settings.
    ///
    /// # Parameters
    /// `settings` - a settings struct
    /// This method will copy the values (so the original pointers may be freed).
    /// The `callback` must be non-null.
    pub fn set(settings: &ExtTierMetricsSettings) -> Result<(), &str> {
        let period_seconds = settings.period_seconds;
        let callback = settings
            .callback
            .expect("Callback to send tier metrics is required");

        let settings = TierMetricsSettings {
            rate_limiter: UltraLightRateLimiter::new(
                1. / period_seconds as f64,
                get_current_time_millis,
            ),
            callback,
        };
        TIER_METRICS_SETTINGS.store(settings)
    }

    #[inline(always)]
    fn get() -> Option<&'static Self> {
        TIER_METRICS_SETTINGS.get()
    }
}

impl ClassificationMetricsSettings {
    /// Initializes Classification Reason Metrics settings.
    /// The function is expected to be called once, any subsequent calls don't change the settings.
    ///
    /// # Parameters
    /// `settings` - a settings struct
    /// This method will copy the values (so the original pointers may be freed).
    /// The `callback` must be non-null.
    pub fn set(settings: &ExtClassificationMetricsSettings) -> Result<(), &str> {
        let period_seconds = settings.period_seconds;
        let callback = settings
            .callback
            .expect("Callback to send classification metrics is required");

        let settings = ClassificationMetricsSettings {
            rate_limiter: UltraLightRateLimiter::new(
                1. / period_seconds as f64,
                get_current_time_millis,
            ),
            callback,
        };
        CLASSIFICATION_METRICS_SETTINGS.store(settings)
    }

    #[inline(always)]
    fn get() -> Option<&'static Self> {
        CLASSIFICATION_METRICS_SETTINGS.get()
    }
}

impl<T: strum::IntoEnumIterator> RequestAnalysisCounterPerMethod<T>
where
    T: Hash + Eq,
{
    fn new(method: HttpToken<'static>) -> Self {
        let counters_map = T::iter().map(|tier| (tier, Default::default())).collect();

        Self {
            method,
            counters_map,
        }
    }
}

impl<T> RequestAnalysisStatisticsStore<T>
where
    T: Hash + Eq + CounterType + MetricsCallback<T> + strum::IntoEnumIterator + std::fmt::Debug,
{
    fn new() -> Self {
        let stats = MONITORED_METHODS
            .iter()
            .map(|method| RequestAnalysisCounterPerMethod::<T>::new(http_token(method)))
            .collect();

        Self { stats }
    }

    pub fn update_counters(&self, request: &HttpRequestData, result: &RequestAnalysisResult) {
        if !<T as self::MetricsCallback<T>>::is_enabled() {
            // if metrics were not initialized, do nothing
            // as the app is supposed to handle logs/metrics itself
            return;
        }
        // This lookup is 5x-20x faster than HashMap
        // for a small set of methods.
        let counters = self
            .stats
            .iter()
            .find(|m| m.method == request.method)
            .unwrap_or(&self.stats[self.stats.len() - 1]);

        let counter_type = &counters.counters_map[&self::CounterType::get_counter_type(result)];
        counter_type.fetch_add(1, Ordering::Relaxed);

        self.emit_granular_metrics_event();
    }

    pub fn get_and_reset_metrics_snapshot(
        &self,
    ) -> SmallVec<[RequestAnalysisMetricsUnit<T>; METRICS_VECTOR_SIZE]> {
        let mut metrics_collection = SmallVec::new();

        // Iterate through the stats vector and extract relevant tier counts
        // for all the methods to aggregate them
        self.stats.iter().for_each(|counter_aggregate| {
            for counter_type in T::iter() {
                let count =
                    counter_aggregate.counters_map[&counter_type].fetch_and(0, Ordering::Relaxed);
                if count > 0 {
                    metrics_collection.push(RequestAnalysisMetricsUnit {
                        method: counter_aggregate.method,
                        counter_type,
                        count,
                    })
                }
            }
        });

        metrics_collection
    }

    fn emit_granular_metrics_event(&self) {
        if let Some(metrics_callback) = <T as self::MetricsCallback<T>>::get_metrics_callback() {
            let snapshot: SmallVec<[ExtRequestAnalysisMetricsUnit<T>; METRICS_VECTOR_SIZE]> = self
                .get_and_reset_metrics_snapshot()
                .into_iter()
                .map(|item| {
                    ExtRequestAnalysisMetricsUnit::<T>::new(
                        ExtString::from_http_token(item.method),
                        item.counter_type,
                        item.count,
                    )
                })
                .collect();

            if !snapshot.is_empty() {
                (metrics_callback)(snapshot.len() as u32, snapshot.as_ptr());
            }
        }
    }
}

impl<T> ExtRequestAnalysisMetricsUnit<T>
where
    T: Hash + Eq,
{
    fn new(method: ExtString, counter_type: T, count: u32) -> Self {
        Self {
            method,
            counter_type,
            count,
        }
    }
}

/// Monotonic clock function, returning `ms` since startup.
/// Makes a system call only in 1/16 of cases (coarse-grained time, good enough)
static LAST_TIME: AtomicU64 = AtomicU64::new(0);
#[cfg(not(test))]
static SKIPS: AtomicU64 = AtomicU64::new(0);

#[cfg(not(test))]
// cov: begin-ignore-line
fn get_current_time_millis() -> u64 {
    let start_time = *START_TIME;
    let x = SKIPS.fetch_add(1, Ordering::Relaxed);
    if x > LOG_MESSAGE_RATE_LIMIT as u64 >> 4 {
        SKIPS.fetch_and(0, Ordering::Relaxed);
    }
    if x == 1 {
        let current = Instant::now()
            .saturating_duration_since(start_time)
            .as_millis() as u64;
        LAST_TIME.store(current, Ordering::Relaxed);
        current
    } else {
        LAST_TIME.load(Ordering::Relaxed)
    }
}
// cov: end-ignore-line

#[cfg(test)]
fn get_current_time_millis() -> u64 {
    let current = LAST_TIME.load(Ordering::Relaxed);
    // move by 10 seconds in tests on each call
    LAST_TIME.store(current + 10_000, Ordering::Relaxed);
    current
}


///
/// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
///
/// Licensed under the Apache License, Version 2.0 (the "License").
/// You may not use this file except in compliance with the License.
/// A copy of the License is located at
///
///  http://aws.amazon.com/apache2.0
///
/// or in the "license" file accompanying this file. This file is distributed
/// on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either
/// express or implied. See the License for the specific language governing
/// permissions and limitations under the License.
///
use crate::http_token_utils::{http_token, is_rfc_tchar, is_rfc_whitespace, HttpToken};

pub const LF: u8 = b'\n';
pub const SP: u8 = b' ';
pub const COLON: u8 = b':';
const CR: u8 = b'\r';
const EMPTY_TOKEN: HttpToken = http_token("");

/// A window view on an HttpToken.
pub struct RequestBuffer<'a> {
    buf: HttpToken<'a>,
    pos: usize,
    end: usize,
}

impl<'a> RequestBuffer<'a> {
    #[cfg_attr(feature = "coverage", inline(never))]
    #[cfg_attr(not(feature = "coverage"), inline(always))]
    pub fn new(buf: HttpToken<'a>) -> Self {
        Self {
            buf,
            pos: 0,
            end: buf.len(),
        }
    }

    #[cfg_attr(feature = "coverage", inline(never))]
    #[cfg_attr(not(feature = "coverage"), inline(always))]
    fn slice(&self, token_begin: usize, token_end: usize) -> Self {
        Self {
            buf: self.buf,
            pos: token_begin,
            end: token_end,
        }
    }

    /// If there is nothing left to process.
    #[cfg_attr(feature = "coverage", inline(never))]
    #[cfg_attr(not(feature = "coverage"), inline(always))]
    pub fn is_done(&self) -> bool {
        self.pos >= self.end
    }

    /// If it had less characters than expected.
    /// E.g. an expected delimiter was not there.
    #[cfg_attr(feature = "coverage", inline(never))]
    #[cfg_attr(not(feature = "coverage"), inline(always))]
    pub fn is_partial(&self) -> bool {
        self.pos > self.end
    }

    /// If the first character an RFC whitespace (SP|HT).
    #[cfg_attr(feature = "coverage", inline(never))]
    #[cfg_attr(not(feature = "coverage"), inline(always))]
    pub fn starts_with_rfc_whitespace(&self) -> bool {
        debug_assert!(self.pos < self.end);
        is_rfc_whitespace(self.buf[self.pos])
    }

    /// Converts it to an HttpToken.
    #[cfg_attr(feature = "coverage", inline(never))]
    #[cfg_attr(not(feature = "coverage"), inline(always))]
    pub fn as_http_token(&self) -> HttpToken<'a> {
        if self.pos < self.end {
            &self.buf[self.pos..self.end]
        } else {
            EMPTY_TOKEN
        }
    }

    /// If the line was terminated with CRLF (true) or just LF (false).
    /// Also trims CR if it was there. (LF is already pre-trimmed).
    #[cfg_attr(feature = "coverage", inline(never))]
    #[cfg_attr(not(feature = "coverage"), inline(always))]
    pub fn trim_last_cr(&mut self) -> bool {
        if self.end > self.pos && self.buf[self.end - 1] == CR {
            self.end -= 1;
            true
        } else {
            false
        }
    }

    /// Take the next token till the delimiter.
    /// The delimiter is not included.
    #[cfg_attr(feature = "coverage", inline(never))]
    #[cfg_attr(not(feature = "coverage"), inline(always))]
    pub fn next_token(&mut self, delimiter: u8) -> Self {
        let mut current = self.pos;
        while current < self.end && self.buf[current] != delimiter {
            current += 1;
        }
        let token_begin = self.pos;
        let token_end = current;

        self.pos = current + 1;

        self.slice(token_begin, token_end)
    }

    /// Take the last token till the delimiter.
    /// The delimiter is not included.
    pub fn last_token(&mut self, delimiter: u8) -> Self {
        let mut current = if self.end > self.pos {
            self.end - 1
        } else {
            self.pos
        };
        // right-trim: ignore non-tchars on the right
        let mut skip_non_tchars = true;
        while self.pos < current && (self.buf[current] != delimiter || skip_non_tchars) {
            skip_non_tchars &= !is_rfc_tchar(self.buf[current]);
            current -= 1;
        }

        let token_begin;
        let token_end;
        if current >= self.pos && current < self.end {
            token_begin = if self.buf[current] == delimiter {
                current + 1
            } else {
                current
            };
            token_end = self.end;
            self.end = token_begin - 1;
        } else {
            // partial token
            token_begin = 1;
            token_end = 0;
        }

        self.slice(token_begin, token_end)
    }
}

#[cfg(test)]
mod tests {
    use crate::http_token_utils::{http_token, to_quoted_ascii, HttpToken};
    use crate::raw_request_parser::{RequestBuffer, EMPTY_TOKEN, LF, SP};

    const GET_REQUEST: HttpToken = http_token("GET /foo/bar HTTP/1.1\r\nHost: localhost\r\n\r\n");

    #[test]
    fn test_parse_request_line() {
        let mut buffer = RequestBuffer::new(GET_REQUEST);
        let mut request_line = buffer.next_token(LF);
        assert_eq!(
            http_token("GET /foo/bar HTTP/1.1\r"),
            request_line.as_http_token(),
            "{}",
            to_quoted_ascii(request_line.as_http_token())
        );
        assert!(request_line.trim_last_cr());
        assert_eq!(
            http_token("GET /foo/bar HTTP/1.1"),
            request_line.as_http_token(),
            "{}",
            to_quoted_ascii(request_line.as_http_token())
        );

        let method = request_line.next_token(SP);
        let version = request_line.last_token(SP);
        let uri = request_line;

        assert_eq!(
            http_token("GET"),
            method.as_http_token(),
            "{}",
            to_quoted_ascii(method.as_http_token())
        );
        assert_eq!(
            http_token("/foo/bar"),
            uri.as_http_token(),
            "{}",
            to_quoted_ascii(uri.as_http_token())
        );
        assert_eq!(
            http_token("HTTP/1.1"),
            version.as_http_token(),
            "{}",
            to_quoted_ascii(version.as_http_token())
        );
    }

    #[test]
    fn test_parse_line_by_line() {
        let mut buffer = RequestBuffer::new(GET_REQUEST);
        let mut request_line = buffer.next_token(LF);
        assert_eq!(
            http_token("GET /foo/bar HTTP/1.1\r"),
            request_line.as_http_token(),
            "{}",
            to_quoted_ascii(request_line.as_http_token())
        );
        assert!(request_line.trim_last_cr());
        assert_eq!(
            http_token("GET /foo/bar HTTP/1.1"),
            request_line.as_http_token(),
            "{}",
            to_quoted_ascii(request_line.as_http_token())
        );

        let mut header = buffer.next_token(LF);
        assert_eq!(
            http_token("Host: localhost\r"),
            header.as_http_token(),
            "{}",
            to_quoted_ascii(header.as_http_token())
        );
        assert!(header.trim_last_cr());
        assert_eq!(
            http_token("Host: localhost"),
            header.as_http_token(),
            "{}",
            to_quoted_ascii(header.as_http_token())
        );

        let mut empty = buffer.next_token(LF);
        assert_eq!(
            http_token("\r"),
            empty.as_http_token(),
            "{}",
            to_quoted_ascii(empty.as_http_token())
        );
        assert!(empty.trim_last_cr());
        assert_eq!(
            http_token(""),
            empty.as_http_token(),
            "{}",
            to_quoted_ascii(empty.as_http_token())
        );

        assert!(buffer.is_done());
    }

    #[test]
    fn test_parse_non_compliant_request_line() {
        // extra spaces after the version
        let mut buffer = RequestBuffer::new(http_token(
            "GET /foo/bar HTTP/1.1 \t \r\nHost: localhost\r\n\r\n",
        ));
        let mut request_line = buffer.next_token(LF);
        assert!(request_line.trim_last_cr());

        let method = request_line.next_token(SP);
        let version = request_line.last_token(SP);
        let uri = request_line;

        assert_eq!(
            http_token("GET"),
            method.as_http_token(),
            "{}",
            to_quoted_ascii(method.as_http_token())
        );
        assert_eq!(
            http_token("/foo/bar"),
            uri.as_http_token(),
            "{}",
            to_quoted_ascii(uri.as_http_token())
        );
        assert_eq!(
            http_token("HTTP/1.1 \t "),
            version.as_http_token(),
            "{}",
            to_quoted_ascii(version.as_http_token())
        );
    }

    #[test]
    fn test_parse_non_tchar_request_line() {
        // non-sensical characters after the version
        let mut buffer = RequestBuffer::new(http_token(
            "GET /foo/bar HTTP/1.1 Ð°Ð±Ñ€Ð° ÐºÐ°Ð´Ð°Ð±Ñ€Ð°\r\nHost: localhost\r\n\r\n",
        ));
        let mut request_line = buffer.next_token(LF);
        assert!(request_line.trim_last_cr());

        let method = request_line.next_token(SP);
        let version = request_line.last_token(SP);
        let uri = request_line;

        assert_eq!(
            http_token("GET"),
            method.as_http_token(),
            "{}",
            to_quoted_ascii(method.as_http_token())
        );
        assert_eq!(
            http_token("/foo/bar"),
            uri.as_http_token(),
            "{}",
            to_quoted_ascii(uri.as_http_token())
        );
        assert!(
            version.as_http_token().starts_with(http_token("HTTP/1.1")),
            "{}",
            to_quoted_ascii(version.as_http_token())
        );
    }

    #[test]
    fn test_parse_non_compliant_terminator_request_line() {
        // extra spaces after the version
        let mut buffer = RequestBuffer::new(http_token(
            "GET /foo/bar HTTP/1.1 \t \nHost: localhost\r\n\r\n",
        ));
        let mut request_line = buffer.next_token(LF);
        assert!(!request_line.trim_last_cr());

        let method = request_line.next_token(SP);
        let version = request_line.last_token(SP);
        let uri = request_line;

        assert_eq!(
            http_token("GET"),
            method.as_http_token(),
            "{}",
            to_quoted_ascii(method.as_http_token())
        );
        assert_eq!(
            http_token("/foo/bar"),
            uri.as_http_token(),
            "{}",
            to_quoted_ascii(uri.as_http_token())
        );
        assert_eq!(
            http_token("HTTP/1.1 \t "),
            version.as_http_token(),
            "{}",
            to_quoted_ascii(version.as_http_token())
        );
    }

    #[test]
    fn test_parse_non_missing_empty_line() {
        // extra spaces after the version
        let mut buffer =
            RequestBuffer::new(http_token("GET /foo/bar HTTP/1.1\r\nHost: localhost\r\n"));
        let mut request_line = buffer.next_token(LF);
        assert!(request_line.trim_last_cr());

        let mut header_line = buffer.next_token(LF);
        assert!(header_line.trim_last_cr());

        assert!(!header_line.is_done());
        assert!(buffer.is_done());
    }

    #[test]
    fn test_parse_missing_uri() {
        // extra spaces after the version
        let mut buffer = RequestBuffer::new(http_token("GET HTTP/1.1"));
        let method = buffer.next_token(SP);
        assert_eq!(
            http_token("GET"),
            method.as_http_token(),
            "{}",
            to_quoted_ascii(method.as_http_token())
        );
        let version = buffer.last_token(SP);
        assert_eq!(
            http_token("HTTP/1.1"),
            version.as_http_token(),
            "{}",
            to_quoted_ascii(version.as_http_token())
        );
        assert!(buffer.is_partial());
    }

    #[test]
    fn test_parse_empty_uri() {
        // extra spaces after the version
        let mut buffer = RequestBuffer::new(http_token("GET  HTTP/1.1"));
        let method = buffer.next_token(SP);
        assert_eq!(
            http_token("GET"),
            method.as_http_token(),
            "{}",
            to_quoted_ascii(method.as_http_token())
        );
        let version = buffer.last_token(SP);
        assert_eq!(
            http_token("HTTP/1.1"),
            version.as_http_token(),
            "{}",
            to_quoted_ascii(version.as_http_token())
        );
        assert!(buffer.is_done());
    }

    #[test]
    fn test_parse_empty_uri_bad_delimiters() {
        // extra spaces after the version
        let mut buffer = RequestBuffer::new(http_token("GET \tHTTP/1.1"));
        let method = buffer.next_token(SP);
        assert_eq!(
            http_token("GET"),
            method.as_http_token(),
            "{}",
            to_quoted_ascii(method.as_http_token())
        );
        let version = buffer.last_token(SP);
        assert_eq!(
            http_token("\tHTTP/1.1"),
            version.as_http_token(),
            "{}",
            to_quoted_ascii(version.as_http_token())
        );
        assert!(buffer.is_partial());
    }

    #[test]
    fn test_parse_partial() {
        // extra spaces after the version
        let mut buffer = RequestBuffer::new(http_token("GET /foo/bar HTTP/1.1\r\nHost: localhost"));
        let mut request_line = buffer.next_token(LF);
        assert!(request_line.trim_last_cr());

        let mut header_line = buffer.next_token(LF);
        assert!(!header_line.trim_last_cr());

        assert!(!header_line.is_done());
        assert!(buffer.is_partial());
    }

    #[test]
    fn test_parse_request_line_http_09() {
        // extra spaces after the version
        let mut buffer = RequestBuffer::new(http_token("GET /foo/bar\r\nHost: localhost\r\n\r\n"));
        let mut request_line = buffer.next_token(LF);
        assert!(request_line.trim_last_cr());

        let method = request_line.next_token(SP);
        let mut version = request_line.last_token(SP);
        let mut uri = request_line;

        assert!(uri.is_partial());

        // there is no version
        uri = version;
        version = RequestBuffer::new(EMPTY_TOKEN);

        assert_eq!(
            http_token("GET"),
            method.as_http_token(),
            "{}",
            to_quoted_ascii(method.as_http_token())
        );
        assert_eq!(
            http_token("/foo/bar"),
            uri.as_http_token(),
            "{}",
            to_quoted_ascii(uri.as_http_token())
        );
        assert_eq!(
            http_token(""),
            version.as_http_token(),
            "{}",
            to_quoted_ascii(version.as_http_token())
        );
    }
}