//! Helper functions for VOA trust model business logic

use std::{collections::HashSet, ops::Add, str::FromStr, time::SystemTime};

use log::{info, warn};
use pgp::{
    packet::{RevocationCode, Signature, SignatureType, UserId},
    types::{SignedUser, Timestamp},
};
use voa_config::openpgp::{DomainName, OpenpgpFingerprint};

use crate::{OpenpgpCert, OpenpgpSignature};

/// Checks whether an [`OpenpgpCert`] matches a set of [`OpenpgpFingerprint`]s.
///
/// Returns `true` if `cert` is an accepted [`OpenpgpCert`], based on the fingerprint filter
/// configuration in `fingerprints`.
///
/// If `fingerprints` is empty, any cert is accepted.
///
/// Otherwise, only certs with a matching fingerprint are accepted.
pub(crate) fn fingerprint_matches(
    cert: &OpenpgpCert,
    fingerprints: &HashSet<OpenpgpFingerprint>,
) -> bool {
    // if we have no filter list, then we keep all certs
    if fingerprints.is_empty() {
        return true;
    };

    if let Ok(fp) = cert.fingerprint() {
        if fingerprints.contains(&fp) {
            true
        } else {
            info!(
                "Ignoring certificate {:?} because it doesn't match the fingerprint_matches set",
                fp
            );
            false
        }
    } else {
        warn!(
            "Skipping certificate with invalid fingerprint {:?}",
            cert.certificate.fingerprint()
        );
        false
    }
}

/// Returns the domain name in an OpenPGP User ID string, if one is found.
///
/// For a `userid` of the form "Foo <bar@baz.org> Bleep", we consider
/// `bar@baz.org` the email part, and `baz.org` the domainname.
///
/// If there are additional `<` or `>` characters, this returns `None`.
/// If there is more than one `@` character in the email part, this returns `None`.
///
/// # Note
///
/// This is a very naive implementation and will be replaced by a proper type in the future.
fn extract_domain_from_user_id(userid: &str) -> Option<&str> {
    // If `userid` only contains one occurrence of "<" and ">" respectively,
    // then `maybe_email` will contain the substring between those two characters.
    let (_, part) = userid.split_once('<')?;
    let (maybe_email, rest) = part.split_once('>')?;

    // If there are any additional occurrences of "<" or ">", we reject `userid` as malformed
    if rest.contains('<') || rest.contains('>') {
        return None;
    }

    // If there is exactly one "@" in `maybe_email`, then we split the domain part of it into
    // `maybe_domain`
    let (_local, maybe_domain) = maybe_email.split_once('@')?;

    // If `maybe_domain` contains additional occurrences of "@", then we reject `userid` as invalid
    if maybe_domain.contains('@') {
        return None;
    }

    // Return the domain substring of `userid`
    Some(maybe_domain)
}

/// Checks whether an OpenPGP User ID matches a domain name.
///
/// Returns `true` if `userid` has a domain part, and it matches `identity_domain_name`, `false`
/// otherwise.
fn user_id_matches_domain(userid: &str, identity_domain_name: &DomainName) -> bool {
    extract_domain_from_user_id(userid)
        .map(|domain| Some(identity_domain_name) == DomainName::from_str(domain).ok().as_ref())
        .unwrap_or(false)
}

/// Checks whether an OpenPGP User ID matches any domain name in a set of domain names.
///
/// Returns `true` if `user_id` matches any of the `identity_domain_names`, `false` otherwise.
pub(crate) fn user_id_matches_any(
    user_id: &str,
    identity_domain_names: &HashSet<DomainName>,
) -> bool {
    identity_domain_names
        .iter()
        .any(|dn| user_id_matches_domain(user_id, dn))
}

/// Ensures that `sigs` contains a certifying signature that is valid at `reference_time`,
/// for self-binding an identity to `cert`.
///
/// This assumes `sigs` contains validated self-signatures over a user id
/// (which is guaranteed if they come from a `Checked`).
///
/// The creation time of `cert` is used as a lower bound for identity validity.
pub(crate) fn identity_bound_at(
    cert: &OpenpgpCert,
    sigs: &[Signature],
    reference_time: SystemTime,
) -> bool {
    // No identity is valid before the creation time of `cert`
    if reference_time < SystemTime::from(cert.certificate.primary_creation_time()) {
        return false;
    }

    let mut any_positive_binding = false;

    for sig in sigs {
        match is_certification_revocation(sig) {
            RevokedCertification::Hard => return false,
            RevokedCertification::Soft => {
                if let Some(created) = sig.created()
                    && SystemTime::from(created) <= reference_time
                {
                    // soft revocation that is already in effect at reference_time
                    return false;
                }
            }
            RevokedCertification::None => {
                if let Some(created) = sig.created() {
                    if let Some(expires) = sig.signature_expiration_time() {
                        let expired =
                            Timestamp::from_secs(created.as_secs().add(expires.as_secs()));
                        if SystemTime::from(expired) > reference_time {
                            any_positive_binding = true;
                        }
                    } else {
                        any_positive_binding = true;
                    }
                }
            }
        }
    }

    // The identity is not revoked at reference time, and there is at least one
    // positive binding that isn't expired at reference time.
    if any_positive_binding {
        return true;
    }

    // we have found neither a revocation nor an explicit binding
    false
}

/// Checks if `sig` is a certification revocation signature, and if so, if it's "hard" or "soft".
fn is_certification_revocation(sig: &Signature) -> RevokedCertification {
    if let Some(SignatureType::CertRevocation) = sig.typ() {
        if is_soft_revocation_reason(sig.revocation_reason_code()) {
            RevokedCertification::Soft
        } else {
            RevokedCertification::Hard
        }
    } else {
        RevokedCertification::None
    }
}

/// Models the revocation status of an OpenPGP component.
///
/// - `Hard` means that the component is revoked at all points in time.
/// - `Soft` means that the component is revoked starting at one specific point in time.
/// - `None` means that the component is not revoked.
enum RevokedCertification {
    None,
    Soft,
    Hard,
}

/// Checks for explicitly "soft" reason codes.
/// In all other cases, we consider a revocation to be "hard".
fn is_soft_revocation_reason(reason: Option<&RevocationCode>) -> bool {
    matches!(
        reason,
        Some(RevocationCode::KeyRetired)
            | Some(RevocationCode::CertUserIdInvalid)
            | Some(RevocationCode::KeySuperseded)
    )
}

/// Finds the [`SignedUser`] that matches a `user_id`.
///
/// This object contains the valid (cryptographically and based on rpgpie policy) self-signatures
/// over that user id.
pub(crate) fn find_signed_user<'c>(
    cert: &'c OpenpgpCert,
    user_id: &UserId,
) -> Option<&'c SignedUser> {
    cert.certificate
        .user_ids()
        .iter()
        .find(|&su| &su.id == user_id)
}

/// Creates a representative string for logging from an [`OpenpgpSignature`].
///
/// The returned string is of the form "fingerprint (file)" where "fingerprint" is the
/// list of issuer fingerprints of the signature and "file" is the optional path of a file from
/// which the OpenPGP signature has been read.
pub(crate) fn log_sig_data(signature: &OpenpgpSignature) -> String {
    format!(
        "{}{}",
        signature
            .detached
            .signature
            .issuer_fingerprint()
            .iter()
            .map(|fingerprint| fingerprint.to_string())
            .collect::<Vec<String>>()
            .join(", "),
        if let Some(path) = signature.source() {
            format!(" ({})", path.to_string_lossy())
        } else {
            "".to_string()
        }
    )
}

/// Creates a representative string for logging from an [`OpenpgpCert`].
///
/// The returned string is of the form "fingerprint (verifier)" where "fingerprint" is the
/// primary key fingerprint of the certificate and "verifier" is a list of one or more verifier
/// locations from which the certificate has been assembled.
pub(crate) fn log_cert_data(cert: &OpenpgpCert) -> String {
    format!(
        "{} ({})",
        cert.certificate.fingerprint(),
        cert.sources
            .iter()
            .map(|verifier| verifier.canonicalized().to_string_lossy().to_string())
            .collect::<Vec<_>>()
            .join(",")
    )
}
