//! Implementation of the "plain" verification method.
//!
//! This verification method only considers artifact verifiers, but no trust anchors.

use std::{collections::HashSet, time::SystemTime};

use log::{trace, warn};
use voa_config::openpgp::{DomainName, PlainMode};

use crate::{
    OpenpgpCert,
    lookup::VerifierLookup,
    trust::{
        model::ArtifactVerifierValidity,
        util::{fingerprint_matches, identity_bound_at, user_id_matches_any},
    },
};

/// Signature verification object that uses a plain artifact verifier-based trust model.
#[derive(Debug)]
pub(crate) struct PlainTrustModel<'a> {
    /// Artifact verifiers are only considered if they contain a validly self-bound identity in
    /// one of these domain names, at the reference time.
    pub(crate) identity_domain_names: &'a HashSet<DomainName>,

    /// Lookup table for the set of artifact verifiers
    pub(crate) artifact_verifier_lookup: VerifierLookup<'a>,
}

impl<'a> PlainTrustModel<'a> {
    /// Creates a new [`PlainTrustModel`].
    ///
    /// Applies OpenPGP fingerprint based filters from `config` on `artifact_verifiers`.
    /// Further filters for `artifact_verifiers` are only applied during signature verification.
    pub(crate) fn new(config: &'a PlainMode, artifact_verifiers: &'a [OpenpgpCert]) -> Self {
        // NOTE: We don't filter certificates for validity here.
        // (For data signature verification, we care about certificate validity at signature
        // creation time.)

        let fingerprints = config.fingerprint_matches();

        // Filter artifact verifiers by fingerprint
        let artifact_verifier_lookup = VerifierLookup::from_iter(
            artifact_verifiers
                .iter()
                .filter(|cert| fingerprint_matches(cert, fingerprints)),
        );

        PlainTrustModel {
            identity_domain_names: config.identity_domain_matches(),
            artifact_verifier_lookup,
        }
    }
}

impl ArtifactVerifierValidity for PlainTrustModel<'_> {
    /// Check if `cert` is valid under the "plain" trust model.
    ///
    /// If `identity_domain_names` is not empty, this checks that `cert` has a validly
    /// self-bound identity in one of the legal domain names, at `reference_time`.
    fn check(&self, cert: &OpenpgpCert, reference_time: SystemTime) -> bool {
        if self.identity_domain_names.is_empty() {
            // We don't explicitly require any particular identity
            //
            // (However, note that elsewhere, certificate validity is checked! In that context, for
            // some certificates, a valid self-certification on a user id may be required.)

            return true;
        }

        for user in cert.certificate.user_ids() {
            // Extract the user id as a UTF-8 string
            let Ok(userid) = String::from_utf8(user.id.id().to_vec()) else {
                warn!(
                    "Invalid non-UTF8 User ID {} in certificate {}",
                    String::from_utf8_lossy(user.id.id()),
                    cert.certificate.fingerprint()
                );
                continue;
            };

            // Check that the identity matches a domain in `identity_domain_names`
            if !user_id_matches_any(&userid, self.identity_domain_names) {
                trace!(
                    "Skipping User ID {}, because it doesn't match {:?}",
                    userid, self.identity_domain_names
                );
                continue;
            }

            // Check that the identity is validly self-bound at `reference_time`
            if !identity_bound_at(cert, &user.signatures, reference_time) {
                trace!(
                    "Skipping User ID {userid}, because it is not validly self-bound at {reference_time:?}",
                );
                continue;
            }

            // This identity matches `identity_domain_names`, and is validly self-bound
            trace!("Accepted User ID {userid}");
            return true;
        }

        // We found no valid identity that matches `identity_domain_names`
        trace!("No accepted User ID was found");
        false
    }

    fn lookup(&self) -> &VerifierLookup<'_> {
        &self.artifact_verifier_lookup
    }
}
