use crate::{RawAlgorithmIdentifier, oids};
use core::fmt;
use picky_asn1::wrapper::OctetStringAsn1;
use serde::{de, ser};

/// Represents `DigestInfo` ASN.1 structure from
/// [PKCS #7](https://datatracker.ietf.org/doc/html/rfc2315#section-9.4) with set of algorithms
/// used with PKCS#12 standard
///
/// ```not_rust
/// DigestInfo ::= SEQUENCE {
///     digestAlgorithm DigestAlgorithmIdentifier,
///     digest Digest }
///
/// Digest ::= OCTET STRING
/// ```
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct Pkcs12DigestInfo {
    pub digest_algorithm: Pkcs12DigestAlgorithm,
    pub digest: OctetStringAsn1,
}

impl ser::Serialize for Pkcs12DigestInfo {
    fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
    where
        S: ser::Serializer,
    {
        use ser::SerializeSeq;

        let mut seq = serializer.serialize_seq(Some(2))?;
        let algorithm: RawAlgorithmIdentifier = self.digest_algorithm.clone().into();
        seq.serialize_element(&algorithm)?;
        seq.serialize_element(&self.digest)?;
        seq.end()
    }
}

impl<'de> de::Deserialize<'de> for Pkcs12DigestInfo {
    fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
    where
        D: de::Deserializer<'de>,
    {
        struct Pkcs12DigestInfoVisitor;

        impl<'de> de::Visitor<'de> for Pkcs12DigestInfoVisitor {
            type Value = Pkcs12DigestInfo;

            fn expecting(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
                formatter.write_str("a valid DER-encoded Pkcs12DigestInfo")
            }

            fn visit_seq<A>(self, mut seq: A) -> Result<Self::Value, A::Error>
            where
                A: de::SeqAccess<'de>,
            {
                let algorithm_identifier: RawAlgorithmIdentifier =
                    seq_next_element!(seq, RawAlgorithmIdentifier, "pkcs12 digest info algorithm identifier");

                let digest = seq_next_element!(seq, OctetStringAsn1, "pkcs12 digest");

                Ok(Pkcs12DigestInfo {
                    digest_algorithm: algorithm_identifier.into(),
                    digest,
                })
            }
        }

        deserializer.deserialize_seq(Pkcs12DigestInfoVisitor)
    }
}

/// Narrowed-down `AlgorithmIdentifier` projection for representing digest algorithms used in
/// PKCS#12 standard.
///
/// NOTE: Most used algorithms which are likely to be encountered in the
/// wild are `SHA1` and `SHA256`
///
/// ```not_rust
/// DigestAlgorithmIdentifier ::= AlgorithmIdentifier
/// ```
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum Pkcs12DigestAlgorithm {
    Sha1,
    Sha224,
    Sha256,
    Sha384,
    Sha512,
    Unknown(RawAlgorithmIdentifier),
}

impl From<Pkcs12DigestAlgorithm> for RawAlgorithmIdentifier {
    fn from(algorithm: Pkcs12DigestAlgorithm) -> Self {
        let oid = match algorithm {
            Pkcs12DigestAlgorithm::Sha1 => oids::sha1(),
            Pkcs12DigestAlgorithm::Sha224 => oids::sha224(),
            Pkcs12DigestAlgorithm::Sha256 => oids::sha256(),
            Pkcs12DigestAlgorithm::Sha384 => oids::sha384(),
            Pkcs12DigestAlgorithm::Sha512 => oids::sha512(),
            Pkcs12DigestAlgorithm::Unknown(algorithm) => {
                return algorithm;
            }
        };

        RawAlgorithmIdentifier::from_parts(oid, None)
    }
}

impl From<RawAlgorithmIdentifier> for Pkcs12DigestAlgorithm {
    fn from(algorithm: RawAlgorithmIdentifier) -> Self {
        let oid: String = algorithm.algorithm().into();
        match oid.as_str() {
            oids::SHA1 => Pkcs12DigestAlgorithm::Sha1,
            oids::SHA224 => Pkcs12DigestAlgorithm::Sha224,
            oids::SHA256 => Pkcs12DigestAlgorithm::Sha256,
            oids::SHA384 => Pkcs12DigestAlgorithm::Sha384,
            oids::SHA512 => Pkcs12DigestAlgorithm::Sha512,
            _ => Pkcs12DigestAlgorithm::Unknown(algorithm),
        }
    }
}

#[cfg(test)]
pub(crate) mod test_data {
    /// Standard `DigestInfo` generated by OpenSSL (SHA256)
    pub const PKCS12_DIGEST_INFO: &[u8] = &[
        0x30, 0x31, 0x30, 0x0D, 0x06, 0x09, 0x60, 0x86, 0x48, 0x01, 0x65, 0x03, 0x04, 0x02, 0x01, 0x05, 0x00, 0x04,
        0x20, 0x82, 0xCA, 0xC9, 0xD6, 0x3B, 0xA4, 0x47, 0x81, 0xEB, 0xAB, 0x3B, 0xBF, 0x72, 0x72, 0x34, 0x7B, 0x1B,
        0x0C, 0x10, 0x80, 0x38, 0x95, 0xB2, 0x43, 0x07, 0x72, 0xD5, 0x72, 0xEB, 0x01, 0x63, 0x64,
    ];

    /// Microsoft does not encode NULL algorithm parameters for DigestInfo (in other places it does)
    /// Just to be safe - we check that we could parse such files correctly (although, for safe of
    /// simplicity, we always encode NULL parameters for DigestInfo, so re-encoded files will differ)
    pub const PKCS12_DIGEST_INFO_MS_BUG: &[u8] = &[
        0x30, 0x1F, 0x30, 0x07, 0x06, 0x05, 0x2B, 0x0E, 0x03, 0x02, 0x1A, 0x04, 0x14, 0x9B, 0x9F, 0x55, 0x86, 0xD8,
        0x5A, 0x38, 0x61, 0xA2, 0x2D, 0x40, 0x2F, 0xE7, 0x89, 0x66, 0x9C, 0x64, 0x02, 0x76, 0x81,
    ];
}

#[cfg(test)]
mod tests {
    use super::test_data::*;
    use super::*;

    use base64::Engine as _;
    use base64::engine::general_purpose;
    use expect_test::expect;

    #[test]
    fn pkcs12_digest_info_roundtrip() {
        let decoded: Pkcs12DigestInfo = picky_asn1_der::from_bytes(PKCS12_DIGEST_INFO).unwrap();
        expect![[r#"
            Pkcs12DigestInfo {
                digest_algorithm: Sha256,
                digest: OctetString(0x82CAC9D63BA44781EBAB3BBF7272347B1B0C10803895B2430772D572EB016364),
            }
        "#]]
        .assert_debug_eq(&decoded);

        check_serde!(decoded: Pkcs12DigestInfo in PKCS12_DIGEST_INFO);
    }

    #[test]
    fn pkcs12_digest_info_ms_bug_parsing() {
        let decoded: Pkcs12DigestInfo = picky_asn1_der::from_bytes(PKCS12_DIGEST_INFO_MS_BUG).unwrap();

        expect![[r#"
            Pkcs12DigestInfo {
                digest_algorithm: Sha1,
                digest: OctetString(0x9B9F5586D85A3861A22D402FE789669C64027681),
            }
        "#]]
        .assert_debug_eq(&decoded);

        // We are not checking here for exact re-encoding, because we always encode NULL parameters
        // for `DigestInfo`
    }
}
