use std::borrow::Cow;
use std::error::Error;
use std::convert::From;
use std::str::Utf8Error;
use std::fmt;

#[allow(unused_imports, deprecated)]
use std::ascii::AsciiExt;

#[cfg(feature = "percent-encode")]
use percent_encoding::percent_decode;
use time::{parsing::Parsable, macros::format_description};
use time::{PrimitiveDateTime, Duration, OffsetDateTime};

use crate::{Cookie, SameSite, CookieStr};

// The three formats spec'd in http://tools.ietf.org/html/rfc2616#section-3.3.1.
// Additional ones as encountered in the real world.
#[allow(deprecated)]
pub type FormatItem<'a> = time::format_description::FormatItem<'a>;

pub static FMT1: &[FormatItem<'_>] = &[
    FormatItem::Optional(&FormatItem::Compound(format_description!("[weekday repr:short], "))),
    FormatItem::Compound(format_description!("[day] [month repr:short] [year padding:none] [hour]:[minute]:[second] GMT")),
];

pub static FMT3: &[FormatItem<'_>] = &[
    FormatItem::Optional(&FormatItem::Compound(format_description!("[weekday repr:short] "))),
    FormatItem::Compound(format_description!("[month repr:short] [day padding:space] [hour]:[minute]:[second] [year padding:none]")),
];

pub static FMT4: &[FormatItem<'_>] = &[
    FormatItem::Optional(&FormatItem::Compound(format_description!("[weekday repr:short], "))),
    FormatItem::Compound(format_description!("[day]-[month repr:short]-[year padding:none] [hour]:[minute]:[second] GMT")),
];

/// Enum corresponding to a parsing error.
#[derive(Debug, PartialEq, Eq, Clone, Copy)]
#[non_exhaustive]
pub enum ParseError {
    /// The cookie did not contain a name/value pair.
    MissingPair,
    /// The cookie's name was empty.
    EmptyName,
    /// Decoding the cookie's name or value resulted in invalid UTF-8.
    Utf8Error(Utf8Error),
}

impl ParseError {
    /// Returns a description of this error as a string
    pub fn as_str(&self) -> &'static str {
        match *self {
            ParseError::MissingPair => "the cookie is missing a name/value pair",
            ParseError::EmptyName => "the cookie's name is empty",
            ParseError::Utf8Error(_) => {
                "decoding the cookie's name or value resulted in invalid UTF-8"
            }
        }
    }
}

impl fmt::Display for ParseError {
    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
        write!(f, "{}", self.as_str())
    }
}

impl From<Utf8Error> for ParseError {
    fn from(error: Utf8Error) -> ParseError {
        ParseError::Utf8Error(error)
    }
}

impl Error for ParseError {
    fn description(&self) -> &str {
        self.as_str()
    }
}

#[cfg(feature = "percent-encode")]
fn name_val_decoded(
    name: &str,
    val: &str
) -> Result<Option<(CookieStr<'static>, CookieStr<'static>)>, ParseError> {
    let decoded_name = percent_decode(name.as_bytes()).decode_utf8()?;
    let decoded_value = percent_decode(val.as_bytes()).decode_utf8()?;

    if let (&Cow::Borrowed(_), &Cow::Borrowed(_)) = (&decoded_name, &decoded_value) {
         Ok(None)
    } else {
        let name = CookieStr::Concrete(Cow::Owned(decoded_name.into()));
        let val = CookieStr::Concrete(Cow::Owned(decoded_value.into()));
        Ok(Some((name, val)))
    }
}

#[cfg(not(feature = "percent-encode"))]
fn name_val_decoded(
    _: &str,
    _: &str
) -> Result<Option<(CookieStr<'static>, CookieStr<'static>)>, ParseError> {
    unreachable!("This function should never be called with 'percent-encode' disabled!")
}

// This function does the real parsing but _does not_ set the `cookie_string` in
// the returned cookie object. This only exists so that the borrow to `s` is
// returned at the end of the call, allowing the `cookie_string` field to be
// set in the outer `parse` function.
fn parse_inner<'c>(s: &str, decode: bool) -> Result<Cookie<'c>, ParseError> {
    let mut attributes = s.split(';');

    // Determine the name = val.
    let key_value = attributes.next().expect("first str::split().next() returns Some");
    let (name, value) = match key_value.find('=') {
        Some(i) => (key_value[..i].trim(), key_value[(i + 1)..].trim()),
        None => return Err(ParseError::MissingPair)
    };

    if name.is_empty() {
        return Err(ParseError::EmptyName);
    }

    // If there is nothing to decode, or we're not decoding, use indexes.
    let indexed_names = |s, name, value| {
        let name = CookieStr::indexed(name, s).expect("name sub");
        let value = CookieStr::indexed(value, s).expect("value sub");
        (name, value)
    };

    // Create a cookie with all of the defaults. We'll fill things in while we
    // iterate through the parameters below.
    let (name, value) = if decode {
        match name_val_decoded(name, value)? {
            Some((name, value)) => (name, value),
            None => indexed_names(s, name, value)
        }
    } else {
        indexed_names(s, name, value)
    };

    let mut cookie: Cookie<'c> = Cookie {
        name, value,
        cookie_string: None,
        expires: None,
        max_age: None,
        domain: None,
        path: None,
        secure: None,
        http_only: None,
        same_site: None,
        partitioned: None,
    };

    for attr in attributes {
        let (key, value) = match attr.find('=') {
            Some(i) => (attr[..i].trim(), Some(attr[(i + 1)..].trim())),
            None => (attr.trim(), None),
        };

        match (&*key.to_ascii_lowercase(), value) {
            ("secure", _) => cookie.secure = Some(true),
            ("httponly", _) => cookie.http_only = Some(true),
            ("max-age", Some(mut v)) => cookie.max_age = {
                let is_negative = v.starts_with('-');
                if is_negative {
                    v = &v[1..];
                }

                if !v.chars().all(|d| d.is_digit(10)) {
                    continue
                }

                // From RFC 6265 5.2.2: neg values indicate that the earliest
                // expiration should be used, so set the max age to 0 seconds.
                if is_negative {
                    Some(Duration::ZERO)
                } else {
                    Some(v.parse::<i64>()
                        .map(Duration::seconds)
                        .unwrap_or_else(|_| Duration::seconds(i64::max_value())))
                }
            },
            ("domain", Some(d)) if !d.is_empty() => {
                cookie.domain = Some(CookieStr::indexed(d, s).expect("domain sub"));
            },
            ("path", Some(v)) => {
                cookie.path = Some(CookieStr::indexed(v, s).expect("path sub"));
            },
            ("samesite", Some(v)) => {
                if v.eq_ignore_ascii_case("strict") {
                    cookie.same_site = Some(SameSite::Strict);
                } else if v.eq_ignore_ascii_case("lax") {
                    cookie.same_site = Some(SameSite::Lax);
                } else if v.eq_ignore_ascii_case("none") {
                    cookie.same_site = Some(SameSite::None);
                } else {
                    // We do nothing here, for now. When/if the `SameSite`
                    // attribute becomes standard, the spec says that we should
                    // ignore this cookie, i.e, fail to parse it, when an
                    // invalid value is passed in. The draft is at
                    // http://httpwg.org/http-extensions/draft-ietf-httpbis-cookie-same-site.html.
                }
            }
            ("partitioned", _) => cookie.partitioned = Some(true),
            ("expires", Some(v)) => {
                let tm = parse_date(v, &FMT1)
                    .or_else(|_| parse_fmt2(v))
                    .or_else(|_| parse_date(v, &FMT3))
                    .or_else(|_| parse_date(v, &FMT4));

                if let Ok(time) = tm {
                    cookie.expires = Some(time.into())
                }
            }
            _ => {
                // We're going to be permissive here. If we have no idea what
                // this is, then it's something nonstandard. We're not going to
                // store it (because it's not compliant), but we're also not
                // going to emit an error.
            }
        }
    }

    Ok(cookie)
}

pub(crate) fn parse_cookie<'c, S>(cow: S, decode: bool) -> Result<Cookie<'c>, ParseError>
    where S: Into<Cow<'c, str>>
{
    let s = cow.into();
    let mut cookie = parse_inner(&s, decode)?;
    cookie.cookie_string = Some(s);
    Ok(cookie)
}

// `time` versions before 0.3.35 cannot publicly parse a `repr:last_two` year
// into a `PrimitiveDateTime`. FMT2 therefore parses a full, unpadded year,
// while this function preserves the original unsigned two-digit syntax.
fn parse_fmt2(string: &str) -> Result<OffsetDateTime, time::Error> {
    static FMT2: &[FormatItem<'_>] = &[
        FormatItem::Optional(&FormatItem::Compound(format_description!("[weekday], "))),
        FormatItem::Compound(format_description!("[day]-[month repr:short]-[year padding:none] [hour]:[minute]:[second] GMT")),
    ];
    const TRAILING_LEN: usize = "00:00:00 GMT".len();

    let invalid_year = time::error::ParseFromDescription::InvalidComponent("year");
    let bytes = string.as_bytes();
    let year_end = string.len().checked_sub(TRAILING_LEN).ok_or(invalid_year)?;
    let year_start = year_end.checked_sub(4).ok_or(invalid_year)?;
    let year = bytes.get(year_start..year_end).ok_or(invalid_year)?;

    if !matches!(year, [b'-', y1, y2, b' '] if y1.is_ascii_digit() && y2.is_ascii_digit()) {
        return Err(invalid_year.into());
    }

    // Reject `--YY`, which the full-year format interprets as year -YY.
    if bytes.get(year_start.checked_sub(1).ok_or(invalid_year)?) == Some(&b'-') {
        return Err(invalid_year.into());
    }

    parse_date(string, &FMT2)
}

pub(crate) fn parse_date(s: &str, format: &impl Parsable) -> Result<OffsetDateTime, time::Error> {
    let datetime = PrimitiveDateTime::parse(s, format)?;
    let datetime = match datetime.year() {
        y @ 0..=69 => datetime.replace_year(y + 2000)?,
        y @ 70..=99 => datetime.replace_year(y + 1900)?,
        _ => datetime,
    };

    Ok(datetime.assume_utc())
}

#[cfg(test)]
mod tests {
    use super::parse_date;
    use crate::{Cookie, SameSite};
    use time::Duration;

    macro_rules! assert_eq_parse {
        ($string:expr, $expected:expr) => (
            let cookie = match Cookie::parse($string) {
                Ok(cookie) => cookie,
                Err(e) => panic!("Failed to parse {:?}: {:?}", $string, e)
            };

            assert_eq!(cookie, $expected);
        )
    }

    macro_rules! assert_ne_parse {
        ($string:expr, $expected:expr) => (
            let cookie = match Cookie::parse($string) {
                Ok(cookie) => cookie,
                Err(e) => panic!("Failed to parse {:?}: {:?}", $string, e)
            };

            assert_ne!(cookie, $expected);
        )
    }

    #[test]
    fn parse_same_site() {
        let expected = Cookie::build(("foo", "bar")).same_site(SameSite::Lax);
        assert_eq_parse!("foo=bar; SameSite=Lax", expected);
        assert_eq_parse!("foo=bar; SameSite=lax", expected);
        assert_eq_parse!("foo=bar; SameSite=LAX", expected);
        assert_eq_parse!("foo=bar; samesite=Lax", expected);
        assert_eq_parse!("foo=bar; SAMESITE=Lax", expected);

        let expected = Cookie::build(("foo", "bar")).same_site(SameSite::Strict);
        assert_eq_parse!("foo=bar; SameSite=Strict", expected);
        assert_eq_parse!("foo=bar; SameSITE=Strict", expected);
        assert_eq_parse!("foo=bar; SameSite=strict", expected);
        assert_eq_parse!("foo=bar; SameSite=STrICT", expected);
        assert_eq_parse!("foo=bar; SameSite=STRICT", expected);

        let expected = Cookie::build(("foo", "bar")).same_site(SameSite::None);
        assert_eq_parse!("foo=bar; SameSite=None", expected);
        assert_eq_parse!("foo=bar; SameSITE=none", expected);
        assert_eq_parse!("foo=bar; SameSite=NOne", expected);
        assert_eq_parse!("foo=bar; SameSite=nOne", expected);
    }

    #[test]
    fn parse() {
        assert!(Cookie::parse("bar").is_err());
        assert!(Cookie::parse("=bar").is_err());
        assert!(Cookie::parse(" =bar").is_err());
        assert!(Cookie::parse("foo=").is_ok());

        let expected = Cookie::new("foo", "bar=baz");
        assert_eq_parse!("foo=bar=baz", expected);

        let expected = Cookie::new("foo", "\"\"bar\"\"");
        assert_eq_parse!("foo=\"\"bar\"\"", expected);

        let expected = Cookie::new("foo", "\"bar");
        assert_eq_parse!("foo=  \"bar", expected);
        assert_eq_parse!("foo=\"bar  ", expected);
        assert_ne_parse!("foo=\"\"bar\"", expected);
        assert_ne_parse!("foo=\"\"bar  \"", expected);
        assert_ne_parse!("foo=\"\"bar  \"  ", expected);

        let expected = Cookie::new("foo", "bar\"");
        assert_eq_parse!("foo=bar\"", expected);
        assert_ne_parse!("foo=\"bar\"\"", expected);
        assert_ne_parse!("foo=\"  bar\"\"", expected);
        assert_ne_parse!("foo=\"  bar\"  \"  ", expected);

        let expected = Cookie::build(("foo", "bar")).partitioned(true).build();
        assert_eq_parse!("foo=bar; partitioned", expected);
        assert_eq_parse!("foo=bar; Partitioned", expected);
        assert_eq_parse!("foo=bar; PARTITIONED", expected);

        let mut expected = Cookie::new("foo", "bar");
        assert_eq_parse!("foo=bar", expected);
        assert_eq_parse!("foo = bar", expected);
        assert_eq_parse!(" foo=bar ", expected);
        assert_eq_parse!(" foo=bar ;Domain=", expected);
        assert_eq_parse!(" foo=bar ;Domain= ", expected);
        assert_eq_parse!(" foo=bar ;Ignored", expected);
        assert_ne_parse!("foo=\"bar\"", expected);
        assert_ne_parse!(" foo=\"bar   \" ", expected);

        let mut unexpected = Cookie::build(("foo", "bar")).http_only(false).build();
        assert_ne_parse!(" foo=bar ;HttpOnly", unexpected);
        assert_ne_parse!(" foo=bar; httponly", unexpected);

        expected.set_http_only(true);
        assert_eq_parse!(" foo=bar ;HttpOnly", expected);
        assert_eq_parse!(" foo=bar ;httponly", expected);
        assert_eq_parse!(" foo=bar ;HTTPONLY=whatever", expected);
        assert_eq_parse!(" foo=bar ; sekure; HTTPONLY", expected);

        expected.set_secure(true);
        assert_eq_parse!(" foo=bar ;HttpOnly; Secure", expected);
        assert_eq_parse!(" foo=bar ;HttpOnly; Secure=aaaa", expected);

        unexpected.set_http_only(true);
        unexpected.set_secure(true);
        assert_ne_parse!(" foo=bar ;HttpOnly; skeure", unexpected);
        assert_ne_parse!(" foo=bar ;HttpOnly; =secure", unexpected);
        assert_ne_parse!(" foo=bar ;HttpOnly;", unexpected);

        unexpected.set_secure(false);
        assert_ne_parse!(" foo=bar ;HttpOnly; secure", unexpected);
        assert_ne_parse!(" foo=bar ;HttpOnly; secure", unexpected);
        assert_ne_parse!(" foo=bar ;HttpOnly; secure", unexpected);

        expected.set_max_age(Duration::ZERO);
        assert_eq_parse!(" foo=bar ;HttpOnly; Secure; Max-Age=0", expected);
        assert_eq_parse!(" foo=bar ;HttpOnly; Secure; Max-Age = 0 ", expected);
        assert_eq_parse!(" foo=bar ;HttpOnly; Secure; Max-Age=-1", expected);
        assert_eq_parse!(" foo=bar ;HttpOnly; Secure; Max-Age = -1 ", expected);

        expected.set_max_age(Duration::minutes(1));
        assert_eq_parse!(" foo=bar ;HttpOnly; Secure; Max-Age=60", expected);
        assert_eq_parse!(" foo=bar ;HttpOnly; Secure; Max-Age =   60 ", expected);

        expected.set_max_age(Duration::seconds(4));
        assert_eq_parse!(" foo=bar ;HttpOnly; Secure; Max-Age=4", expected);
        assert_eq_parse!(" foo=bar ;HttpOnly; Secure; Max-Age = 4 ", expected);

        unexpected.set_secure(true);
        unexpected.set_max_age(Duration::minutes(1));
        assert_ne_parse!(" foo=bar ;HttpOnly; Secure; Max-Age=122", unexpected);
        assert_ne_parse!(" foo=bar ;HttpOnly; Secure; Max-Age = 38 ", unexpected);
        assert_ne_parse!(" foo=bar ;HttpOnly; Secure; Max-Age=51", unexpected);
        assert_ne_parse!(" foo=bar ;HttpOnly; Secure; Max-Age = -1 ", unexpected);
        assert_ne_parse!(" foo=bar ;HttpOnly; Secure; Max-Age = 0", unexpected);

        expected.set_path("/");
        assert_eq_parse!("foo=bar;HttpOnly; Secure; Max-Age=4; Path=/", expected);
        assert_eq_parse!("foo=bar;HttpOnly; Secure; Max-Age=4;Path=/", expected);

        expected.set_path("/foo");
        assert_eq_parse!("foo=bar;HttpOnly; Secure; Max-Age=4; Path=/foo", expected);
        assert_eq_parse!("foo=bar;HttpOnly; Secure; Max-Age=4;Path=/foo", expected);
        assert_eq_parse!("foo=bar;HttpOnly; Secure; Max-Age=4;path=/foo", expected);
        assert_eq_parse!("foo=bar;HttpOnly; Secure; Max-Age=4;path = /foo", expected);

        unexpected.set_max_age(Duration::seconds(4));
        unexpected.set_path("/bar");
        assert_ne_parse!("foo=bar;HttpOnly; Secure; Max-Age=4; Path=/foo", unexpected);
        assert_ne_parse!("foo=bar;HttpOnly; Secure; Max-Age=4;Path=/baz", unexpected);

        expected.set_domain("www.foo.com");
        assert_eq_parse!(" foo=bar ;HttpOnly; Secure; Max-Age=4; Path=/foo; \
            Domain=www.foo.com", expected);

        expected.set_domain("foo.com");
        assert_eq_parse!(" foo=bar ;HttpOnly; Secure; Max-Age=4; Path=/foo; \
            Domain=foo.com", expected);
        assert_eq_parse!(" foo=bar ;HttpOnly; Secure; Max-Age=4; Path=/foo; \
            Domain=FOO.COM", expected);

        expected.set_domain(".foo.com");
        assert_eq_parse!(" foo=bar ;HttpOnly; Secure; Max-Age=4; Path=/foo; \
            Domain=.foo.com", expected);
        assert_eq_parse!(" foo=bar ;HttpOnly; Secure; Max-Age=4; Path=/foo; \
            Domain=.FOO.COM", expected);

        unexpected.set_path("/foo");
        unexpected.set_domain("bar.com");
        assert_ne_parse!(" foo=bar ;HttpOnly; Secure; Max-Age=4; Path=/foo; \
            Domain=foo.com", unexpected);
        assert_ne_parse!(" foo=bar ;HttpOnly; Secure; Max-Age=4; Path=/foo; \
            Domain=FOO.COM", unexpected);

        let time_str = "Wed, 21 Oct 2015 07:28:00 GMT";
        let expires = parse_date(time_str, &super::FMT1).unwrap();
        expected.set_expires(expires);
        assert_eq_parse!(" foo=bar ;HttpOnly; Secure; Max-Age=4; Path=/foo; \
            Domain=foo.com; Expires=Wed, 21 Oct 2015 07:28:00 GMT", expected);

        unexpected.set_domain("foo.com");
        let bad_expires = parse_date(time_str, &super::FMT1).unwrap();
        expected.set_expires(bad_expires);
        assert_ne_parse!(" foo=bar ;HttpOnly; Secure; Max-Age=4; Path=/foo; \
            Domain=foo.com; Expires=Wed, 21 Oct 2015 07:28:00 GMT", unexpected);
    }

    #[test]
    fn parse_abbreviated_years() {
        let tests = [
            ("foo=bar; expires=Thu, 10-Sep-20 20:00:00 GMT", 2020),
            ("foo=bar; expires=Thu, 10-Sep-68 20:00:00 GMT", 2068),
            ("foo=bar; expires=Thu, 10-Sep-69 20:00:00 GMT", 2069),
            ("foo=bar; expires=Thu, 10-Sep-99 20:00:00 GMT", 1999),
            ("foo=bar; expires=Thu, 10-Sep-2069 20:00:00 GMT", 2069),
            ("foo=bar; expires=Thu, 10-Sep-0 20:00:00 GMT", 2000),
            ("foo=bar; expires=Thu, 10-Sep-00 20:00:00 GMT", 2000),
            ("foo=bar; expires=Thu, 10-Sep-70 20:00:00 GMT", 1970),
        ];

        for (cookie_str, expected_year) in tests {
            let cookie = Cookie::parse(cookie_str).expect("valid cookie string");
            let expiry = cookie.expires_datetime().expect("cookie has expiration");
            assert_eq!(expiry.year(), expected_year);
        }
    }

    #[test]
    fn long_weekday_requires_two_digit_year() {
        for year in &["0", "000", "1994", "+94", "-94"] {
            let input = format!("foo=bar; expires=Sunday, 06-Nov-{} 08:49:37 GMT", year);
            let cookie = Cookie::parse(input).expect("valid cookie string");
            assert!(cookie.expires_datetime().is_none());
        }
    }

    #[test]
    fn parse_variant_date_fmts() {
        let expected = time::macros::datetime!(1994-11-06 8:49:37 UTC);
        let strings = [
            "foo=bar; expires=Sun, 06 Nov 1994 08:49:37 GMT",
            "foo=bar; expires=06 Nov 1994 08:49:37 GMT",
            "foo=bar; expires=Sunday, 06-Nov-94 08:49:37 GMT",
            "foo=bar; expires=06-Nov-94 08:49:37 GMT",
            "foo=bar; expires=Sun Nov  6 08:49:37 1994",
            "foo=bar; expires=Nov  6 08:49:37 1994",
            "foo=bar; expires=06-Nov-1994 08:49:37 GMT",
        ];

        for cookie_str in strings {
            let cookie = Cookie::parse(cookie_str).unwrap();
            assert_eq!(cookie.expires_datetime(), Some(expected));
        }
    }

    #[test]
    fn parse_very_large_max_ages() {
        let mut expected = Cookie::build(("foo", "bar"))
            .max_age(Duration::seconds(i64::max_value()))
            .build();

        let string = format!("foo=bar; Max-Age={}", 1u128 << 100);
        assert_eq_parse!(&string, expected);

        expected.set_max_age(Duration::seconds(0));
        assert_eq_parse!("foo=bar; Max-Age=-129", expected);

        let string = format!("foo=bar; Max-Age=-{}", 1u128 << 100);
        assert_eq_parse!(&string, expected);

        let string = format!("foo=bar; Max-Age=-{}", i64::max_value());
        assert_eq_parse!(&string, expected);

        let string = format!("foo=bar; Max-Age={}", i64::max_value());
        expected.set_max_age(Duration::seconds(i64::max_value()));
        assert_eq_parse!(&string, expected);
    }

    #[test]
    fn odd_characters() {
        let expected = Cookie::new("foo", "b%2Fr");
        assert_eq_parse!("foo=b%2Fr", expected);
    }

    #[test]
    #[cfg(feature = "percent-encode")]
    fn odd_characters_encoded() {
        let expected = Cookie::new("foo", "b/r");
        let cookie = match Cookie::parse_encoded("foo=b%2Fr") {
            Ok(cookie) => cookie,
            Err(e) => panic!("Failed to parse: {:?}", e)
        };

        assert_eq!(cookie, expected);
    }

    #[test]
    fn do_not_panic_on_large_max_ages() {
        let max_seconds = Duration::MAX.whole_seconds();
        let expected = Cookie::build(("foo", "bar"))
            .max_age(Duration::seconds(max_seconds));

        let too_many_seconds = (max_seconds as u64) + 1;
        assert_eq_parse!(format!(" foo=bar; Max-Age={:?}", too_many_seconds), expected);
    }
}
