// SPDX-FileCopyrightText: Heiko Schaefer <heiko@schaefer.name>
// SPDX-License-Identifier: MIT OR Apache-2.0

//! Wrapper types and logic for certificates, aka OpenPGP public keys ("transferable public keys").

use std::io;
use std::ops::Add;

use chrono::{DateTime, Duration, Utc};
use pgp::cleartext::CleartextSignedMessage;
use pgp::crypto::aead::AeadAlgorithm;
use pgp::crypto::hash::HashAlgorithm;
use pgp::crypto::sym::SymmetricKeyAlgorithm;
use pgp::packet::{SubpacketData, SubpacketType, UserId};
use pgp::ser::Serialize;
use pgp::types::{Fingerprint, KeyId, PublicKeyTrait, SignedUser, SignedUserAttribute, Tag};
use pgp::{
    armor, ArmorOptions, PublicOrSecret, Signature, SignedKeyDetails, SignedPublicKey,
    SignedPublicSubKey, StandaloneSignature,
};

use crate::key::{ComponentKeyPub, SignedComponentKey, SignedComponentKeyPub};
use crate::signature::SigStack;
use crate::tsk::Tsk;
use crate::{policy, signature, Error};

/// A "certificate," also known as an "OpenPGP public key."
#[derive(Debug, Clone)]
pub struct Certificate {
    spk: SignedPublicKey,
}

impl From<SignedPublicKey> for Certificate {
    fn from(spk: SignedPublicKey) -> Self {
        Self { spk }
    }
}

impl TryFrom<&[u8]> for Certificate {
    type Error = Error;

    fn try_from(input: &[u8]) -> Result<Self, Self::Error> {
        use pgp::Deserializable;

        let (spk, _) = SignedPublicKey::from_reader_single(input)?;
        Ok(Self { spk })
    }
}

impl TryFrom<&Certificate> for Vec<u8> {
    type Error = Error;

    fn try_from(value: &Certificate) -> Result<Self, Self::Error> {
        Ok(value.spk().to_bytes()?)
    }
}

impl From<&Tsk> for Certificate {
    fn from(value: &Tsk) -> Self {
        Certificate::from(SignedPublicKey::from(value.key().clone()))
    }
}

/// pick the user id we consider primary
fn pick_primary_user_id<'a>(users: &'_ [&'a SignedUser]) -> Option<&'a SignedUser> {
    match users.len() {
        0 => None,
        1 => Some(users[0]),

        _ => {
            // FIXME: pick the user id with the most recent self-signature?
            // (ignoring revoked user ids, unless all are revoked)

            let mut best = users[0];

            let now = Utc::now(); // FIXME?

            let stack = SigStack::from_iter(&best.signatures);
            let mut best_is_revoked = stack.revoked_at(&now);
            // let mut best_latest_sig = stack.active_at(Some(&now));

            for user in users {
                let stack = SigStack::from_iter(&user.signatures);
                let this_is_revoked = stack.revoked_at(&now);
                // let this_latest_sig = stack.active_at(Some(&now));

                if best_is_revoked && !this_is_revoked {
                    // replace
                    best = user;
                    best_is_revoked = this_is_revoked;
                    // best_latest_sig = this_latest_sig;
                }
            }

            Some(best)
        }
    }
}

/// Return primary User ID:
///
/// We prefer the User ID with the most recent self-signature.
///
/// Either from among all User IDs that are marked "primary".
/// Or from among all User IDs if none are marked "primary".
///
/// (Note: we don't implement looking up the primary User ID at a reference time!)
fn primary_user_id(details: &SignedKeyDetails) -> Option<&SignedUser> {
    // Find all User IDs that are marked "primary"
    let primaries: Vec<_> = details.users.iter().filter(|u| u.is_primary()).collect();

    if !primaries.is_empty() {
        // choose between all that are marked "primary"
        pick_primary_user_id(&primaries)
    } else {
        // No User ID was marked "primary", we choose among all User IDs.
        let users: Vec<_> = details.users.iter().collect();
        pick_primary_user_id(&users)
    }
}

/// Returns the latest signature of the primary User ID.
/// None, if no primary User ID or binding signature for it are found.
pub(crate) fn primary_user_id_binding_at<'a>(
    details: &'a SignedKeyDetails,
    reference: &'a DateTime<Utc>,
) -> Option<&'a Signature> {
    primary_user_id(details)
        .and_then(|uid| SigStack::from_iter(uid.signatures.iter()).active_at(Some(reference)))
}

impl Certificate {
    /// The fingerprint of this certificate (i.e. the fingerprint of its primary key)
    pub fn fingerprint(&self) -> Fingerprint {
        self.spk.primary_key.fingerprint()
    }

    /// Load a set of `Certificate`s from a source.
    ///
    /// The source data may be armored or binary.
    pub fn load<R: io::Read>(source: &mut R) -> Result<Vec<Certificate>, Error> {
        let mut certs = vec![];

        let (parsed, _headers) = pgp::composed::signed_key::from_reader_many(source)?;

        for res in parsed {
            match res {
                Ok(pos) => {
                    let cert = match pos {
                        PublicOrSecret::Public(spk) => spk.into(),
                        PublicOrSecret::Secret(_ssk) => {
                            return Err(Error::Message(
                                "Expected Certificate(s), got TSK".to_string(),
                            ));
                        }
                    };

                    certs.push(cert);
                }
                Err(_) => eprintln!("Bad data {:?}", res),
            }
        }

        if certs.is_empty() {
            Err(Error::Message("No certificates found".to_string()))
        } else {
            Ok(certs)
        }
    }

    /// Save this Certificate to a writer
    pub fn save(&self, armored: bool, sink: &mut dyn io::Write) -> Result<(), Error> {
        Self::save_all([self], armored, sink)
    }

    /// Save a set of Certificates to a writer
    pub fn save_all<'a>(
        certs: impl IntoIterator<Item = &'a Self>,
        armored: bool,
        mut sink: &mut dyn io::Write,
    ) -> Result<(), Error> {
        if armored {
            let spks: Vec<_> = certs.into_iter().map(|c| c.spk()).collect();

            let opts = ArmorOptions::default();

            armor::write(
                &spks,
                armor::BlockType::PublicKey,
                &mut sink,
                opts.headers,
                opts.include_checksum,
            )?;
        } else {
            for c in certs {
                c.spk().to_writer(&mut sink)?;
            }
        }

        Ok(())
    }

    pub(crate) fn spk(&self) -> &SignedPublicKey {
        &self.spk
    }

    fn component_keys(&self) -> Vec<SignedComponentKey> {
        let x = SignedComponentKeyPub::Primary((
            self.spk.clone(),
            self.spk.details.direct_signatures.clone(),
        ));
        let mut v = vec![SignedComponentKey::Pub(x)];

        self.spk.public_subkeys.iter().for_each(|spsk| {
            let dks = self.spk.details.direct_signatures.clone();

            let x = SignedComponentKeyPub::Subkey((spsk.clone(), dks));
            v.push(SignedComponentKey::Pub(x));
        });

        v
    }

    /// Get list of all decryption-capable component keys.
    ///
    /// This fn is intended to signal potential use *decryption*, not encryption!
    /// It is *very* lenient in what it lists. It only checks for key flags, but doesn't check
    /// for validity in any way (e.g. it doesn't check for a correct subkey binding signature).
    pub fn decryption_capable_component_keys(
        &self,
    ) -> impl Iterator<Item = SignedComponentKey> + '_ {
        let now: DateTime<Utc> = chrono::offset::Utc::now();

        // FIXME: filter out unknown notations

        self.component_keys()
            .into_iter()
            .filter(move |sck| sck.is_encryption_capable(&now))
    }
}

/// Verify data signatures
#[derive(Debug)]
pub struct SignatureVerifier {
    ckey: ComponentKeyPub,
    primary_creation: DateTime<Utc>,
}

impl SignatureVerifier {
    pub(crate) fn new(ckey: ComponentKeyPub, primary_creation: DateTime<Utc>) -> Self {
        Self {
            ckey,
            primary_creation,
        }
    }

    /// Verify a data signature
    ///
    /// Rejects the signature if its creation predates either the verifier or its primary
    pub fn verify(&self, signature: &Signature, data: &[u8]) -> Result<(), Error> {
        let Some(created) = signature.created() else {
            // this should not happen
            unimplemented!("FIXME");
        };

        // reject if signature timestamp is before ckey or primary creation time
        if created < &self.primary_creation || created < self.ckey.created_at() {
            return Err(Error::Message(
                "Data signature predates key creation".to_string(),
            ));
        }

        self.ckey.verify(signature, data)
    }

    /// Verify a cleartext data signature
    pub fn verify_csf(&self, csf: &CleartextSignedMessage) -> Result<StandaloneSignature, Error> {
        // FIXME: reject if signature timestamp is before ckey or primary creation time

        // FIXME: the csf can contain many signatures, how does that fit with this interface?

        match &self.ckey {
            ComponentKeyPub::Primary(pk) => Ok(csf.verify(pk)?),
            ComponentKeyPub::Subkey(psk) => Ok(csf.verify(psk)?),
        }
        .cloned()
    }

    /// Return the underlying component public key for this verifier
    pub fn as_componentkey(&self) -> &ComponentKeyPub {
        &self.ckey
    }
}

/// A layer on top of [Certificate] that filters out any signatures that we don't consider valid.
///
/// A `Checked` certificate only contains Signatures that have been verified to be cryptographically
/// correct.
/// See <https://openpgp.dev/book/verification.html#when-are-signatures-valid>
///
/// This is a precondition for signatures being considered fully valid, but by itself insufficient.
///
/// Note: This function currently removes all third-party signatures
/// (because they can't be cryptographically checked in the context of looking at an individual
/// certificate).
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct Checked {
    original: SignedPublicKey,
    cspk: SignedPublicKey,
    primary_fingerprint: Fingerprint, // cached value for efficient third-party fp checks
    primary_key_id: KeyId,            // cached value for efficient third-party fp checks
}

impl Checked {
    /// Produce a `Checked` view onto a `Certificate`.
    ///
    /// This operation checks all self-signatures for cryptographic validity, as well as
    /// conformance to policy.
    ///
    /// Third party signatures are not currently handled.
    pub fn new(cert: &Certificate) -> Self {
        // Make a copy of all parts of the SignedPublicKey and drop all signatures that:
        // - are not cryptographically correct
        // - don't pass our policy checks (`signature_acceptable`)

        // TODO: store bad self-sigs and third-party sigs in a separate place

        // TODO: go look for any (valid/correct) hard revocations for the full cert,
        // and encode the result in a simple "cert is hard revoked" flag:
        //
        // -> any user id binding that claims to be primary
        // -> or direct signatures on the primary

        // TODO: Which parts of validity checking can/should be moved to [SigStack]?

        let spk = &cert.spk;

        // primary
        let primary = spk.primary_key.clone();
        let primary_created = primary.created_at();

        // details
        let revocation_signatures: Vec<_> = spk
            .details
            .revocation_signatures
            .iter()
            .filter(|s| signature::signature_acceptable(s)) // FIXME: could this filter out any important revocations?
            .filter(|s| signature::not_older_than(s, primary_created))
            .filter(|s| s.verify_key(&spk.primary_key).is_ok())
            .cloned()
            .collect();

        let direct_signatures: Vec<_> = spk
            .details
            .direct_signatures
            .iter()
            .filter(|s| signature::signature_acceptable(s))
            .filter(|s| signature::not_older_than(s, primary_created))
            .filter(|s| s.verify_key(&spk.primary_key).is_ok())
            .cloned()
            .collect();

        let users: Vec<SignedUser> = spk
            .details
            .users
            .iter()
            .map(|su| {
                let id = su.id.clone();
                let signatures = su
                    .signatures
                    .iter()
                    .filter(|s| signature::signature_acceptable(s))
                    .filter(|s| signature::not_older_than(s, primary_created))
                    .filter(|s| s.verify_certification(&primary, Tag::UserId, &id).is_ok())
                    .cloned()
                    .collect();

                SignedUser { id, signatures }
            })
            .collect();

        let user_attributes: Vec<SignedUserAttribute> = spk
            .details
            .user_attributes
            .iter()
            .map(|sua| {
                let attr = sua.attr.clone();
                let signatures = sua
                    .signatures
                    .iter()
                    .filter(|s| signature::signature_acceptable(s))
                    .filter(|s| signature::not_older_than(s, primary_created))
                    .filter(|s| {
                        s.verify_certification(&primary, Tag::UserAttribute, &attr)
                            .is_ok()
                    })
                    .cloned()
                    .collect();

                SignedUserAttribute { attr, signatures }
            })
            .collect();

        let details = SignedKeyDetails::new(
            revocation_signatures,
            direct_signatures,
            users,
            user_attributes,
        );

        // subkeys
        let subkeys = spk
            .public_subkeys
            .iter()
            .map(|sk| {
                let key = sk.key.clone();
                let signatures = sk
                    .signatures
                    .iter()
                    .filter(|s| signature::signature_acceptable(s))
                    .filter(|s| signature::not_older_than(s, primary_created))
                    .filter(|s| {
                        if let Err(e) = s.verify_key_binding(&spk.primary_key, &key) {
                            log::warn!("Ignoring bad key binding signature {:#?}: {:?}", s, e);
                            return false;
                        }

                        // If there is an embedded back signature, we reject the entire signature if the back signature is cryptographically invalid
                        for embedded in s
                            .config
                            .hashed_subpackets
                            .iter()
                            .filter(|sp| sp.typ() == SubpacketType::EmbeddedSignature)
                        {
                            match &embedded.data {
                                SubpacketData::EmbeddedSignature(backsig) => {
                                    if !signature::signature_acceptable(backsig) {
                                        log::warn!(
                                            "Ignoring signature because of unacceptable embedded signature {:#?}",
                                            backsig
                                        );

                                        return false;
                                    }

                                    // FIXME: reject if signature creation time is before subkey creation time

                                    if let Err(e) =
                                        backsig.verify_backwards_key_binding(
                                            &key,
                                            &spk.primary_key,
                                        )
                                    {
                                        log::warn!(
                                            "Ignoring signature because embedded signature doesn't verify as correct {:#?}: {:?}",
                                            backsig, e
                                        );

                                        return false;
                                    }
                                }
                                _ => unreachable!("no other subpacket types should come up, here"),
                            }
                        }

                        true
                    })
                    .cloned()
                    .collect();
                SignedPublicSubKey { key, signatures }
            })
            .collect();

        // combine
        let cspk = SignedPublicKey::new(primary, details, subkeys);

        log::debug!("Checked cert: {:#?}", cspk);

        Self {
            original: spk.clone(),
            cspk,
            primary_key_id: spk.primary_key.key_id(),
            primary_fingerprint: spk.primary_key.fingerprint(),
        }
    }

    pub fn primary_key(&self) -> SignedComponentKeyPub {
        let mut sigs: Vec<_> = self.cspk.details.revocation_signatures.clone();
        sigs.append(&mut self.cspk.details.direct_signatures.clone());

        SignedComponentKeyPub::Primary((self.cspk.clone(), sigs))
    }

    /// An iterator over all subkeys of this Certificate
    pub fn subkeys(&self) -> impl Iterator<Item = SignedComponentKeyPub> + '_ {
        // note: used in rpgpie-cert-store
        self.cspk.public_subkeys.iter().map(|spsk| {
            SignedComponentKeyPub::Subkey((
                spsk.clone(),
                self.cspk.details.direct_signatures.clone(),
            ))
        })
    }

    /// Return the primary User ID
    pub fn primary_user_id(&self) -> Option<&SignedUser> {
        primary_user_id(&self.cspk.details)
    }

    /// List all User IDs
    pub fn user_ids(&self) -> &[SignedUser] {
        &self.cspk.details.users
    }

    /// Was this signature potentially issued by a third party?
    fn maybe_third_party(&self, s: &Signature) -> bool {
        let mut maybe_third_party = false;

        let issuers = s.issuer();
        if !issuers.is_empty() && issuers.into_iter().any(|x| x != &self.primary_key_id) {
            maybe_third_party = true
        };

        let issuer_fps = s.issuer_fingerprint();
        if !issuer_fps.is_empty()
            && issuer_fps
                .into_iter()
                .any(|x| x != &self.primary_fingerprint)
        {
            maybe_third_party = true
        };

        maybe_third_party
    }

    /// Get all third-party certifications for all User IDs
    pub fn user_id_third_party_certifications(&self) -> Vec<(UserId, Vec<&Signature>)> {
        let mut res = vec![];

        for su in &self.original.details.users {
            let id = su.id.clone();
            let sigs = su
                .signatures
                .iter()
                .filter(|s| self.maybe_third_party(s))
                .collect();

            res.push((id, sigs));
        }

        res
    }

    /// List all User Attributes
    pub fn user_attributes(&self) -> &[SignedUserAttribute] {
        &self.cspk.details.user_attributes
    }

    /// List all valid component keys that are encryption capable.
    ///
    /// The resulting set of [ComponentKeyPub] can be used to encrypt data.
    pub fn valid_encryption_capable_component_keys(&self) -> Vec<ComponentKeyPub> {
        // FIXME: First drop all bad signatures (bad algorithms)

        let now: DateTime<Utc> = chrono::offset::Utc::now();

        match self.primary_valid_at(&now) {
            // If the primary key is invalid now, there are no valid component keys
            Err(_) | Ok(false) => return vec![],

            Ok(true) => {}
        }

        // Filter based on component key validity
        self.encryption_capable_component_keys(&now)
            .filter(|sckp| sckp.is_component_subkey_valid_at(&now))
            .filter(|sckp| sckp.valid_encryption_algo())
            .filter(|sckp| policy::acceptable_pk_algorithm(sckp.public_params(), &now))
            .map(Into::into)
            .collect()
    }

    /// Get a set of [SignatureVerifier] objects, one for each valid component key that is
    /// appropriate to use for data signatures.
    ///
    /// This includes a check that the binding signature must have an embedded back signature
    /// (we have a `Checked`, so we can assume that if a back signature exists at all, it is acceptable).
    pub fn valid_signing_capable_component_keys_at(
        &self,
        reference: &DateTime<Utc>,
    ) -> Vec<SignatureVerifier> {
        log::debug!("valid_signing_capable_component_keys_at {:?}", reference);

        // We assume all bad signatures (bad algorithms, missing backsig, ..) have been dropped already,
        // because this is a `Checked`

        match self.primary_valid_at(reference) {
            // If the primary key is invalid, there are no valid component keys
            Err(_) | Ok(false) => {
                log::debug!(" primary invalid");
                return vec![];
            }

            Ok(true) => {
                log::debug!(" primary is valid");
            }
        }

        // Filter based on component key validity
        let keys = self
            .signing_capable_component_keys(reference)
            .filter(|sckp| sckp.is_component_subkey_valid_at(reference))
            .filter(|sckp| sckp.has_valid_backsig_at(reference))
            .filter(|sckp| policy::acceptable_pk_algorithm(sckp.public_params(), reference))
            .map(ComponentKeyPub::from)
            .map(|ckey| SignatureVerifier::new(ckey, *self.cspk.primary_key.created_at()))
            .collect();

        log::debug!(" -> {:#?}", keys);

        keys
    }

    /// Get a set of [ComponentKeyPub] objects, one for each valid component key that is
    /// appropriate to use for authentication.
    pub fn valid_authentication_capable_component_keys(
        &self,
        reference: &DateTime<Utc>,
    ) -> Vec<ComponentKeyPub> {
        // FIXME: First drop all bad signatures (bad algorithms)

        match self.primary_valid_at(reference) {
            // If the primary key is invalid, there are no valid component keys
            Err(_) | Ok(false) => return vec![],

            Ok(true) => {}
        }

        // Filter based on component key validity
        self.authentication_capable_component_keys(reference)
            .filter(|sckp| sckp.is_component_subkey_valid_at(reference))
            .filter(|sckp| sckp.valid_encryption_algo())
            .filter(|sckp| policy::acceptable_pk_algorithm(sckp.public_params(), reference))
            .map(Into::into)
            .collect()
    }

    fn component_keys(&self) -> impl Iterator<Item = SignedComponentKeyPub> + '_ {
        // Collect all signatures that are bound directly to the primary

        let pri = vec![self.primary_key()];

        pri.into_iter().chain(self.subkeys())
    }

    /// Get list of all encryption capable component keys
    fn encryption_capable_component_keys<'a>(
        &'a self,
        reference: &'a DateTime<Utc>,
    ) -> impl Iterator<Item = SignedComponentKeyPub> + 'a {
        self.component_keys().filter(move |ckp| {
            SignedComponentKey::Pub(ckp.clone()).is_encryption_capable(reference)
        })
    }

    /// Get list of all component keys with "signing" key flag.
    fn signing_capable_component_keys<'a>(
        &'a self,
        reference: &'a DateTime<Utc>,
    ) -> impl Iterator<Item = SignedComponentKeyPub> + 'a {
        self.component_keys()
            .filter(move |ckp| SignedComponentKey::Pub(ckp.clone()).is_signing_capable(reference))
    }

    /// Get list of all authentication capable component keys
    fn authentication_capable_component_keys<'a>(
        &'a self,
        reference: &'a DateTime<Utc>,
    ) -> impl Iterator<Item = SignedComponentKeyPub> + 'a {
        self.component_keys().filter(move |ckp| {
            SignedComponentKey::Pub(ckp.clone()).is_authentication_capable(reference)
        })
    }

    /// Get latest binding signature for the primary user id.
    fn primary_user_id_binding<'a>(
        &'a self,
        reference: &'a DateTime<Utc>,
    ) -> Option<&'a Signature> {
        primary_user_id_binding_at(&self.cspk.details, reference)
    }

    fn dks(&self, reference: &'_ DateTime<Utc>) -> Option<&Signature> {
        // combine direct_signatures+revocation_signatures
        SigStack::from_iter(
            self.cspk
                .details
                .direct_signatures
                .iter()
                .chain(self.cspk.details.revocation_signatures.iter()),
        )
        .active_at(Some(reference))
    }

    pub fn direct_third_party_certifications(&self) -> Vec<&Signature> {
        self.original
            .details
            .direct_signatures
            .iter()
            .chain(&self.original.details.revocation_signatures)
            .filter(|s| self.maybe_third_party(s))
            .collect()
    }

    /// Get the creation time of the certificate (the creation time of the primary key)
    pub fn primary_creation_time(&self) -> &DateTime<Utc> {
        self.cspk.primary_key.created_at()
    }

    // Return expiration time as a duration, in seconds
    fn primary_expiration_time(&self, reference: &DateTime<Utc>) -> Result<Option<u32>, Error> {
        // Pick the more defensive expiration value, if both are set.
        // - None: no expiration
        // - 0: no expiration
        // - any other value: expiration in "seconds after creation"
        fn shorter(s1: Option<u32>, s2: Option<u32>) -> Option<u32> {
            match (s1, s2) {
                (None, None) => None,
                (Some(s), None) | (None, Some(s)) => Some(s),

                // "0" means indefinite -> return the other value, which might be shorter
                (Some(0), Some(s)) | (Some(s), Some(0)) => Some(s),

                (Some(s1), Some(s2)) => {
                    // Both are not zero -> return the smaller number
                    Some(u32::min(s1, s2))
                }
            }
        }

        let pri_binding = self.primary_user_id_binding(reference);
        let dks = self.dks(reference);

        let exp = match (dks, pri_binding) {
            (None, None) => {
                // found neither direct key binding, nor primary user id binding signature
                return Err(Error::NoPrimaryBinding);
            }
            (Some(s), None) | (None, Some(s)) => {
                crate::util::duration_to_seconds(s.key_expiration_time())
            }

            (Some(s1), Some(s2)) => shorter(
                crate::util::duration_to_seconds(s1.key_expiration_time()),
                crate::util::duration_to_seconds(s2.key_expiration_time()),
            ),
        };

        Ok(exp)
    }

    /// Is the primary key (and thus the certificate as a whole) valid at `reference` time?
    ///
    /// The primary is considered invalid if it is expired, revoked, or has no acceptable binding
    /// self-signature.
    pub fn primary_valid_at(&self, reference: &DateTime<Utc>) -> Result<bool, Error> {
        let creation = self.primary_creation_time();
        let expiration = self.primary_expiration_time(reference)?;

        // creation time is in the future, relative to the reference time
        if creation > reference {
            return Ok(false);
        }

        // If there is an expiration time, and it is not the value 0, the key is invalid if "creation+expiration" is prior to the reference time
        if let Some(expiration) = expiration {
            if expiration != 0 {
                let expires = self.cspk.primary_key.created_at().add(
                    #[allow(clippy::expect_used)]
                    Duration::try_seconds(expiration as i64).expect("should never fail for an u32"),
                );

                if expires < *reference {
                    log::debug!(" primary key expires {:?}", expires);

                    return Ok(false);
                }
            }
        }

        // If the primary is using an insufficiently strong public key algorithm (at the reference time), we reject its use
        if !policy::acceptable_pk_algorithm(self.cspk.primary_key.public_params(), reference) {
            return Ok(false);
        }

        // Check that a valid primary user id binding or dks exist at the reference time.
        // (This includes checking that the binding signatures use an acceptable public key algorithm.)
        if !Self::primary_has_valid_binding_at(self, reference, creation) {
            log::debug!(" no valid primary key binding");

            return Ok(false);
        }

        // the primary is revoked at the reference time
        if self.revoked_at(reference) {
            return Ok(false);
        }

        Ok(true)
    }

    /// Check for either a valid dks at reference, or a valid primary key binding at reference.
    fn primary_has_valid_binding_at(
        &self,
        reference: &DateTime<Utc>,
        creation: &DateTime<Utc>,
    ) -> bool {
        // Does a valid dks exist, at "reference"?
        let dks_stack: SigStack = SigStack::from_iter(self.cspk.details.direct_signatures.iter());
        if dks_stack.has_valid_binding_at(reference, creation) {
            return true;
        }

        // Is the primary user id binding valid, at "reference"?
        if let Some(user) = primary_user_id(&self.cspk.details) {
            let stack = SigStack::from_iter(user.signatures.iter());
            if stack.has_valid_binding_at(reference, creation) {
                return true;
            }
        }

        false
    }

    /// Is the full certificate revoked at `reference` time?
    ///
    /// This takes into account the semantics of hard and soft revocation.
    pub fn revoked_at(&self, reference: &DateTime<Utc>) -> bool {
        log::debug!("revoked_at {:?}", reference);

        let stack = SigStack::from_iter(
            self.cspk
                .details
                .revocation_signatures
                .iter()
                .chain(self.cspk.details.direct_signatures.iter()),
        );

        if stack.revoked_at(reference) {
            log::debug!(" revoked via a direct revocation");
            return true;
        }

        // is there a valid direct key binding at `reference`?
        let valid_dks = stack.has_valid_binding_at(reference, self.cspk.primary_key.created_at());

        if !valid_dks {
            // Consider if the primary user id is revoked
            //
            // (Note: this approach considers the *current* primary User ID.
            // Alternatively, the primary User ID at `reference` could be found and checked.)
            if let Some(primary_uid) = primary_user_id(&self.cspk.details) {
                let stack = SigStack::from_iter(primary_uid.signatures.iter());

                if stack.revoked_at(reference) {
                    log::debug!(" revoked via primary User ID");
                    return true;
                }
            }
        }

        false
    }

    /// The preferred `SymmetricKeyAlgorithm` setting for this certificate
    pub fn preferred_symmetric_key_algo<'a>(
        &'a self,
        reference: &'a DateTime<Utc>,
    ) -> Option<&'a [SymmetricKeyAlgorithm]> {
        let prim_bind = self.primary_user_id_binding(reference);
        if let Some(prim_bind) = prim_bind {
            return Some(prim_bind.preferred_symmetric_algs());
        }

        // FIXME: handle dks

        None
    }

    /// The preferred `AeadAlgorithm` setting for this certificate
    pub fn preferred_aead_algo<'a>(
        &'a self,
        reference: &'a DateTime<Utc>,
    ) -> Option<&'a [(SymmetricKeyAlgorithm, AeadAlgorithm)]> {
        let prim_bind = self.primary_user_id_binding(reference);
        if let Some(prim_bind) = prim_bind {
            return Some(prim_bind.preferred_aead_algs());
        }

        // FIXME: handle dks

        None
    }

    /// The preferred `HashAlgorithm` setting for this certificate
    pub fn preferred_hash_algorithms<'a>(
        &'a self,
        reference: &'a DateTime<Utc>,
    ) -> Option<&'a [HashAlgorithm]> {
        let prim_bind = self.primary_user_id_binding(reference);
        if let Some(prim_bind) = prim_bind {
            return Some(prim_bind.preferred_hash_algs());
        }

        // FIXME: handle dks

        None
    }

    /// The `Features` setting of this Certificate
    pub fn features<'a>(&'a self, reference: &'a DateTime<Utc>) -> Option<u8> {
        let prim_bind = self.primary_user_id_binding(reference);
        if let Some(prim_bind) = prim_bind {
            let feat = prim_bind.features();
            if feat.len() == 1 {
                return Some(feat[0]);
            }
        }

        // get dks at reference time
        if let Some(dks) = self.dks(reference) {
            let feat = dks.features();
            if feat.len() == 1 {
                return Some(feat[0]);
            }
        }

        None
    }

    /// The fingerprint of this Certificate
    pub fn fingerprint(&self) -> Fingerprint {
        // FIXME: return reference, don't clone
        self.primary_fingerprint.clone()
    }

    /// The key id of this Certificate
    pub fn key_id(&self) -> KeyId {
        // FIXME: return reference, don't clone
        self.primary_key_id.clone()
    }
}

impl From<&Certificate> for Checked {
    fn from(value: &Certificate) -> Self {
        Checked::new(value)
    }
}

impl From<Checked> for Certificate {
    fn from(value: Checked) -> Self {
        value.cspk.into()
    }
}

#[cfg(test)]
mod tests {
    #![allow(clippy::expect_used)]

    use chrono::{DateTime, TimeDelta, Utc};

    use crate::certificate::Checked;

    fn load_cert(filename: &str) -> Checked {
        let mut read = std::fs::File::open(filename).expect("open");
        let c = crate::certificate::Certificate::load(&mut read).expect("load");

        assert_eq!(c.len(), 1);

        Checked::from(&c[0])
    }

    fn expiration_at(c: &Checked, at: &str) -> DateTime<Utc> {
        use std::ops::Add;

        let created = c.primary_creation_time();

        let rfc3339 = DateTime::parse_from_rfc3339(at).expect("parse at");

        let validity = c
            .primary_expiration_time(&rfc3339.into())
            .expect("got expiration")
            .expect("some");

        created.add(TimeDelta::seconds(validity as i64))
    }

    #[test]
    fn test_dsa() {
        // This certificate uses a DSA primary (which is used for all self-signatures).
        //
        // Note that we consider DSA invalid from February 3, 2023 forward (see [policy]).
        //
        // Thus, self-signatures that are newer than this cutoff are ignored -
        // so the self-signature on the primary User ID from 2024-06-03 is considered invalid,
        // and the key is considered expired at 2024-06-07T19:57:03Z.
        let checked = load_cert("tests/608B00ABE1DAA3501C5FF91AE58271326F9F4937");

        // --- validity of the primary ---

        // key is not revoked
        assert!(!checked.revoked_at(
            &DateTime::parse_from_rfc3339("2025-01-01T23:59:00Z")
                .expect("parse date")
                .into()
        ));

        // valid at 2020-01-01T23:59:00Z
        assert!(checked
            .primary_valid_at(
                &DateTime::parse_from_rfc3339("2020-01-01T23:59:00Z")
                    .expect("parse date")
                    .into()
            )
            .expect("primary_valid_at"));

        let exp = expiration_at(&checked, "2020-01-01T23:59:00Z");
        assert_eq!(
            exp,
            DateTime::parse_from_rfc3339("2020-08-03T13:32:24Z").expect("parse date")
        );

        let exp = expiration_at(&checked, "2021-01-01T23:59:00Z");
        assert_eq!(
            exp,
            DateTime::parse_from_rfc3339("2022-07-06T17:06:34Z").expect("parse date")
        );

        let exp = expiration_at(&checked, "2024-01-01T23:59:00Z");
        assert_eq!(
            exp,
            DateTime::parse_from_rfc3339("2024-06-07T19:57:03Z").expect("parse date")
        );

        // not valid at 2024-07-01T23:59:00Z
        assert!(!checked
            .primary_valid_at(
                &DateTime::parse_from_rfc3339("2024-07-01T23:59:00Z")
                    .expect("parse date")
                    .into()
            )
            .expect("primary_valid_at"));

        // --- encryption subkey ---

        let enc = checked.valid_encryption_capable_component_keys();
        assert_eq!(enc.len(), 0);

        // TODO: if we had a [valid_encryption_capable_component_keys_at] fn, we could test for
        //  - historical validity of the old ElGamal encryption key, and
        //  - the switch to the RSA encryption key on 2017-08-02.

        // --- signature component key (the primary key) ---
        let sig = checked.valid_signing_capable_component_keys_at(
            &DateTime::parse_from_rfc3339("2022-07-01T23:59:00Z")
                .expect("parse date")
                .into(),
        );
        assert_eq!(sig.len(), 1);

        let sig = checked.valid_signing_capable_component_keys_at(
            &DateTime::parse_from_rfc3339("2024-07-01T23:59:00Z")
                .expect("parse date")
                .into(),
        );
        assert_eq!(sig.len(), 0);
    }
}
