use std::{collections::HashSet, fs::read, path::Path, time::SystemTime};

use log::{debug, error, info, warn};
use pgp::types::KeyDetails;
use voa_config::openpgp::{NumDataSignatures, OpenpgpSettings, VerificationMethod};

use crate::{
    Error,
    OpenpgpCert,
    OpenpgpSignature,
    OpenpgpSignatureCheck,
    SignerInfo,
    lookup::VerifierLookup,
    trust::{
        anchor::TrustAnchorModel,
        plain::PlainTrustModel,
        util::{log_cert_data, log_sig_data},
        wot::WebOfTrustModel,
    },
};

/// The trust model used for verification.
#[derive(Debug)]
enum TrustModel<'a> {
    Plain(PlainTrustModel<'a>),
    TrustAnchor(TrustAnchorModel<'a>),
    WebOfTrust(WebOfTrustModel<'a>),
}

/// Performs signature verification based on a trust model configuration and sets of verifiers.
#[derive(Debug)]
pub struct ModelBasedVerifier<'a> {
    /// Number of distinct valid data signatures that we expect for a data file to be
    /// considered valid
    num_data_signatures: NumDataSignatures,

    /// The trust model used by this verifier object
    trust_model: TrustModel<'a>,
}

impl<'a> ModelBasedVerifier<'a> {
    /// Create a new signature verifier object based on an [`OpenpgpSettings`] configuration,
    /// a set of artifact verifiers and trust anchors.
    ///
    /// For [`VerificationMethod::Plain`], the set of anchors must be empty.
    pub fn new(
        config: &'a OpenpgpSettings,
        artifact_verifiers: &'a [OpenpgpCert],
        trust_anchors: &'a [OpenpgpCert],
    ) -> Self {
        let trust_model = match config.verification_method() {
            VerificationMethod::Plain(plain) => {
                if !trust_anchors.is_empty() {
                    warn!(
                        "VerificationMethod is 'Plain', but {} trust_anchors exist",
                        trust_anchors.len()
                    );
                }

                TrustModel::Plain(PlainTrustModel::new(plain, artifact_verifiers))
            }

            VerificationMethod::TrustAnchor(anchor) => TrustModel::TrustAnchor(
                TrustAnchorModel::new(anchor, artifact_verifiers, trust_anchors),
            ),

            VerificationMethod::WebOfTrust(wot) => {
                TrustModel::WebOfTrust(WebOfTrustModel::new(wot, artifact_verifiers, trust_anchors))
            }
        };

        Self {
            num_data_signatures: config.num_data_signatures(),
            trust_model,
        }
    }

    /// Verifies one or more `signatures` for a `signed_file`.
    ///
    /// Success of the verification is decided according to the trust model configuration of this
    /// [`ModelBasedVerifier`]. Signature verifiers are obtained from the trust model.
    ///
    /// The return value is a boolean value as am immediate judgment about validity of the signature
    /// set, under the configured trust model.
    ///
    /// In addition, a `Vec` of [`OpenpgpSignatureCheck`] is returned, with more detailed
    /// information about each checked signature.
    /// This links each signature to a signer certificate that signed it, if the signature has been
    /// found valid under the current trust model.
    ///
    /// # Note
    ///
    /// Does not consider OpenPGP signatures with an unset creation time and instead emits a warning
    /// for them.
    ///
    /// # Errors
    ///
    /// Returns an error, if the `signed_file` cannot be read.
    pub fn verify_file_with_signatures(
        &'a self,
        signed_file: impl AsRef<Path>,
        signatures: &'a [OpenpgpSignature],
    ) -> Result<Vec<OpenpgpSignatureCheck<'a>>, Error> {
        let signed_file = signed_file.as_ref();

        // Verify the signatures against the file using the trust model
        let checks = match &self.trust_model {
            TrustModel::Plain(plain) => verify(plain, signatures, signed_file),
            TrustModel::TrustAnchor(anchor) => verify(anchor, signatures, signed_file),
            TrustModel::WebOfTrust(_wot) => {
                return Err(Error::UnimplementedVerificationMethod {
                    verification_method: "OpenPGP Web of Trust".to_string(),
                });
            }
        }?;

        // How many valid data signatures have we found?
        //
        // Specifically, we check the number of distinct primary fingerprints of issuing
        // certificates that have produced valid data signatures.
        //
        // Counting primary fingerprints ensures that we don't count multiple signatures from the
        // same issuing certificate as independent evidence.
        let signer_fingerprints: HashSet<_> = checks
            .iter()
            .filter_map(|check| check.signer_info())
            .map(|si| si.certificate().certificate.fingerprint())
            .collect();

        // We only consider the verification successful if the threshold of required valid data
        // signatures has been met.
        let num_required_signatures = self.num_data_signatures.get().get();
        let num_successful_signatures = signer_fingerprints.len();
        if num_successful_signatures < num_required_signatures {
            let failed_signatures = checks
                .iter()
                .filter_map(|check| {
                    if check.signer_info().is_none() {
                        Some(check.signature().clone())
                    } else {
                        None
                    }
                })
                .collect::<Vec<_>>();

            return Err(Error::FileVerificationFailed {
                path: signed_file.to_path_buf(),
                num_required_signatures,
                num_successful_signatures,
                failed_signatures,
            });
        };

        Ok(checks)
    }
}

/// An interface into an OpenPGP trust model that determines if an artifact verifier is accepted, at
/// reference time.
///
/// Objects that implement this trait hold a set of verifiers, and a trust model configuration that
/// specifies which verifiers are valid in which context.
pub(crate) trait ArtifactVerifierValidity {
    /// Check if `cert` is acceptable as an artifact signer at `reference_time`, according to
    /// the trust model
    fn check(&self, cert: &OpenpgpCert, reference_time: SystemTime) -> bool;

    /// Borrow a `VerifierLookup` over the artifact verifiers in the trust model
    fn lookup(&self) -> &VerifierLookup<'_>;
}

/// Verifies the content of `signed_file` against a set of `signatures`, and returns validity
/// information for each signature.
fn verify<'b>(
    artifact_verifiers: &'b impl ArtifactVerifierValidity,
    signatures: &'b [OpenpgpSignature],
    signed_file: &Path,
) -> Result<Vec<OpenpgpSignatureCheck<'b>>, Error> {
    // We load the signed payload into memory (for now)
    let data = read(signed_file).map_err(|source| Error::IoPath {
        path: signed_file.into(),
        source,
        context: "reading a signed file",
    })?;

    let mut checks = Vec::new();

    'sigs: for signature in signatures {
        let log_sig_data = log_sig_data(signature);
        info!("Verifying OpenPGP signature ({log_sig_data}) for file {signed_file:?}");

        let Some(sig_created) = signature.creation_time() else {
            warn!("Skipping signature without creation time");
            checks.push(OpenpgpSignatureCheck::new(signature, None));
            continue;
        };

        let verifiers = artifact_verifiers
            .lookup()
            .get_matching_verifiers(signature);
        debug!("Found {} candidate verifiers", verifiers.len());

        for (verifier, cert) in &verifiers {
            if !artifact_verifiers.check(cert, sig_created.into()) {
                // We can't use this cert
                warn!(
                    "We can't use cert {} to verify, under the current policy, at {:?}",
                    cert.certificate.fingerprint(),
                    sig_created
                );

                continue;
            }

            let log_cert_data = log_cert_data(cert);
            let component_fp = verifier.as_componentkey().fingerprint().to_string();

            // Cryptographic signature check of the payload against the verifier
            if let Err(error) = verifier.verify(&signature.detached.signature, &data) {
                // This signature didn't verify as cryptographically correct.
                // However, the verifier was considered appropriate to use by
                // `VerifierLookup::get_matching_verifiers` (based on issuer subpackets in the
                // signature).
                //
                // It's very irregular that validation would fail under these circumstances,
                // so we log this at the "error" level.
                // Ideally, a human should investigate what's going on there.
                error!(
                    "Signature verification error for {log_sig_data} for file {signed_file:?} with component key {component_fp} in OpenPGP certificate {log_cert_data}: {error}"
                );

                // try next verifier, if any
                continue;
            }

            info!(
                "Successfully verified OpenPGP signature {log_sig_data} for file {signed_file:?} with component key {component_fp} in OpenPGP certificate {log_cert_data}"
            );

            checks.push(OpenpgpSignatureCheck::new(
                signature,
                Some(SignerInfo::new(cert, component_fp)),
            ));

            // We're done with this signature and can move to the next one
            continue 'sigs;
        }

        warn!(
            "Signature {log_sig_data} and file {signed_file:?} could not be verified (tried {} candidate verifiers)",
            verifiers.len()
        );
        checks.push(OpenpgpSignatureCheck::new(signature, None));
    }

    Ok(checks)
}
