//! Types that can be used for filtering OpenPGP packets.

use std::{fmt::Display, str::FromStr};

use addr::parse_domain_name;
use const_hex::{decode_to_slice, encode};
use serde::{Deserialize, Serialize};

use crate::Error;

/// The match for an OpenPGP User ID.
#[derive(Clone, Debug, Deserialize, Eq, Hash, Ord, PartialEq, PartialOrd, Serialize)]
pub struct DomainName(String);

impl FromStr for DomainName {
    type Err = Error;

    fn from_str(s: &str) -> Result<Self, Self::Err> {
        let name = parse_domain_name(s).map_err(|source| {
            crate::config::technology::openpgp::Error::InvalidDomainName {
                domain_name: s.to_string(),
                message: source.to_string(),
            }
        })?;
        // Fail if the domain name does not have a root (e.g. only "example" instead of
        // "example.org").
        if name.root().is_none() {
            return Err(
                crate::config::technology::openpgp::Error::InvalidDomainName {
                    domain_name: s.to_string(),
                    message: "The domain cannot be a top-level domain".to_string(),
                }
                .into(),
            );
        }

        Ok(Self(name.to_string()))
    }
}

impl Display for DomainName {
    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
        write!(f, "{}", self.0)
    }
}

/// An OpenPGP fingerprint.
#[derive(Clone, Debug, Deserialize, Eq, Hash, Ord, PartialEq, PartialOrd, Serialize)]
#[serde(into = "String", try_from = "String")]
pub enum OpenpgpFingerprint {
    /// An OpenPGP V4 fingerprint.
    V4([u8; 20]),

    /// An OpenPGP V6 fingerprint.
    V6([u8; 32]),
}

impl AsRef<[u8]> for OpenpgpFingerprint {
    fn as_ref(&self) -> &[u8] {
        match self {
            Self::V4(bytes) => bytes,
            Self::V6(bytes) => bytes,
        }
    }
}

impl From<OpenpgpFingerprint> for String {
    fn from(value: OpenpgpFingerprint) -> Self {
        value.to_string()
    }
}

impl FromStr for OpenpgpFingerprint {
    type Err = Error;

    fn from_str(s: &str) -> Result<Self, Self::Err> {
        match s.len() {
            40 => {
                let mut output = [0u8; 20];
                decode_to_slice(s, &mut output).map_err(|source| {
                    crate::config::technology::openpgp::Error::InvalidOpenpgpFingerprint {
                        fingerprint: s.into(),
                        context: "creating an OpenPGP v4 fingerprint".to_string(),
                        source,
                    }
                })?;
                Ok(Self::V4(output))
            }
            64 => {
                let mut output = [0u8; 32];
                decode_to_slice(s, &mut output).map_err(|source| {
                    crate::config::technology::openpgp::Error::InvalidOpenpgpFingerprint {
                        fingerprint: s.into(),
                        context: "creating an OpenPGP v6 fingerprint".to_string(),
                        source,
                    }
                })?;
                Ok(Self::V6(output))
            }
            _ => Err(
                crate::config::technology::openpgp::Error::InvalidOpenpgpFingerprintLength {
                    fingerprint: s.into(),
                }
                .into(),
            ),
        }
    }
}

impl TryFrom<String> for OpenpgpFingerprint {
    type Error = Error;

    fn try_from(value: String) -> Result<Self, Self::Error> {
        Self::from_str(&value)
    }
}

impl Display for OpenpgpFingerprint {
    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
        write!(
            f,
            "{}",
            match self {
                Self::V4(bytes) => encode(bytes),
                Self::V6(bytes) => encode(bytes),
            }
        )
    }
}

#[cfg(test)]
mod tests {
    use rstest::rstest;
    use testresult::TestResult;

    use super::*;

    #[rstest]
    #[case("example.org")]
    #[case("sub.example.org")]
    fn domain_name_from_str_succeeds(#[case] name: &str) -> TestResult {
        let _domain = DomainName::from_str(name)?;
        Ok(())
    }

    #[rstest]
    #[case("example foo")]
    #[case("fail")]
    #[case("_")]
    fn domain_name_from_str_fails(#[case] name: &str) -> TestResult {
        match DomainName::from_str(name) {
            Err(Error::OpenpgpTechnology(
                crate::config::technology::openpgp::Error::InvalidDomainName { .. },
            )) => {}
            Err(error) => panic!(
                "Expected to fail with an Error::InvalidDomainName, but failed with another error instead: {error}"
            ),
            Ok(name) => panic!(
                "Expected to fail with an Error::InvalidDomainName, but succeeded instead: {name:?}"
            ),
        }

        Ok(())
    }

    #[rstest]
    #[case::v4("f1d2d2f924e986ac86fdf7b36c94bcdf32beec15")]
    #[case::v6("b5bb9d8014a0f9b1d61e21e796d78dccdf1352f23cd32812f4850b878ae4944c")]
    fn lowercase_openpgp_fingerprint_roundtrip_succeeds(#[case] input: &str) -> TestResult {
        let fingerprint = OpenpgpFingerprint::from_str(input)?;
        assert_eq!(fingerprint.to_string(), input);
        Ok(())
    }

    #[rstest]
    #[case::v4(
        "f1d2d2f924e986ac86fdf7b36c94bcdf32beec15",
        &[241, 210, 210, 249, 36, 233, 134, 172, 134, 253, 247, 179, 108, 148, 188, 223, 50, 190, 236, 21]
    )]
    #[case::v6(
        "b5bb9d8014a0f9b1d61e21e796d78dccdf1352f23cd32812f4850b878ae4944c",
        &[
            181, 187, 157, 128, 20, 160, 249, 177, 214, 30, 33, 231, 150, 215, 141, 204, 223, 19,
            82, 242, 60, 211, 40, 18, 244, 133, 11, 135, 138, 228, 148, 76,
        ]
    )]
    fn openpgp_fingerprint_as_ref_matches(
        #[case] input: &str,
        #[case] expected: &[u8],
    ) -> TestResult {
        let fingerprint = OpenpgpFingerprint::from_str(input)?;
        assert_eq!(fingerprint.as_ref(), expected);
        Ok(())
    }

    #[rstest]
    #[case::wrong_chars_for_v4("XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX")]
    #[case::wrong_chars_for_v6("XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX")]
    fn openpgp_fingerprint_from_str_fails_on_chars(#[case] input: &str) -> TestResult {
        match OpenpgpFingerprint::from_str(input) {
            Err(Error::OpenpgpTechnology(
                crate::config::technology::openpgp::Error::InvalidOpenpgpFingerprint { .. },
            )) => {}
            Err(error) => panic!(
                "Expected to fail with an Error::InvalidOpenpgpFingerprint, but failed with another error instead: {error}"
            ),
            Ok(name) => panic!(
                "Expected to fail with an Error::InvalidOpenpgpFingerprint, but succeeded instead: {name:?}"
            ),
        }

        Ok(())
    }

    #[rstest]
    #[case::too_long_for_v4("f1d2d2f924e986ac86fdf7b36c94bcdf32beec15234234")]
    #[case::too_short_for_v4("f1d2d2f924e986ac86fdf7b36c94bcdf32")]
    #[case::too_long_for_v6(
        "b5bb9d8014a0f9b1d61e21e796d78dccdf1352f23cd32812f4850b878ae4944c2234234"
    )]
    #[case::too_short_for_v6("b5bb9d8014a0f9b1d61e21e796d78dccdf1352f23cd32812f4850b878")]
    fn openpgp_fingerprint_from_str_fails_on_length(#[case] input: &str) -> TestResult {
        match OpenpgpFingerprint::from_str(input) {
            Err(Error::OpenpgpTechnology(
                crate::config::technology::openpgp::Error::InvalidOpenpgpFingerprintLength {
                    ..
                },
            )) => {}
            Err(error) => panic!(
                "Expected to fail with an Error::InvalidOpenpgpFingerprint, but failed with another error instead: {error}"
            ),
            Ok(name) => panic!(
                "Expected to fail with an Error::InvalidOpenpgpFingerprint, but succeeded instead: {name:?}"
            ),
        }

        Ok(())
    }
}
