//! Detached OpenPGP signatures for use with VOA verifiers.

use std::path::{Path, PathBuf};

use pgp::{
    composed::{Deserializable, DetachedSignature},
    types::Timestamp,
};

use crate::{Error, OpenpgpCert};

/// A detached OpenPGP signature for use in VOA verification.
#[derive(Clone, Debug, Eq, PartialEq)]
pub struct OpenpgpSignature {
    pub(crate) detached: DetachedSignature,

    /// The file that this signature was loaded from
    pub(crate) source: Option<PathBuf>,
}

impl OpenpgpSignature {
    /// Creates an [`OpenpgpSignature`] from the file in `path`.
    ///
    /// # Errors
    ///
    /// Returns an error:
    /// - If `path` can't be read
    /// - If an [`OpenpgpSignature`] cannot be parsed from the content of the file
    pub fn from_file<P: AsRef<Path>>(path: P) -> Result<Self, Error> {
        let path = path.as_ref();

        let data = std::fs::read(path).map_err(|source| Error::IoPath {
            path: path.into(),
            source,
            context: "foo",
        })?;

        let mut sig = Self::from_slice(data.as_slice())?;
        sig.source = Some(path.to_path_buf());

        Ok(sig)
    }

    /// Creates an [`OpenpgpSignature`] from `data`.
    ///
    /// # Errors
    ///
    /// Returns an error, if an [`OpenpgpSignature`] cannot be parsed from `data`.
    pub(crate) fn from_slice(data: &[u8]) -> Result<Self, Error> {
        let detached = DetachedSignature::from_bytes(data).map_err(|source| Error::OpenPgp {
            context: "while parsing detached signature",
            source,
        })?;

        Ok(Self {
            detached,
            source: None,
        })
    }

    /// Returns the creation time.
    ///
    /// Considering the creation time of a detached OpenPGP signature prevents attacks where a
    /// signature claims to be very old, and thereby is allowed to use a weak cryptographic
    /// mechanism.
    ///
    /// # Note
    ///
    /// Users of [`OpenpgpSignature`] are advised to discard signatures, if their creation time is
    /// implausible, e.g. if a signature claims to be older than the data, that it claims to be
    /// over.
    ///
    /// # Errors
    ///
    /// Returns an error if no creation time is found.
    pub fn creation_time(&self) -> Option<Timestamp> {
        self.detached.signature.created()
    }

    /// The source file that this signature was loaded from, if known
    pub fn source(&self) -> Option<&Path> {
        self.source.as_deref()
    }
}

/// The OpenPGP certificate that issued an OpenPGP signature, and the fingerprint of the
/// component key (within the `certificate`) that created the signature.
///
/// Holds a reference to an [`OpenpgpCert`] and an OpenPGP fingerprint string.
#[derive(Debug)]
pub struct SignerInfo<'a> {
    certificate: &'a OpenpgpCert,
    component_fingerprint: String,
}

impl<'a> SignerInfo<'a> {
    /// Creates a new [`SignerInfo`] from an [`OpenpgpCert`] and a fingerprint.
    ///
    /// This struct must only be constructed once the cryptographic validity of the target
    /// signature has been successfully verified with this signer.
    pub(crate) fn new(
        certificate: &'a OpenpgpCert,
        component_fingerprint: String,
    ) -> SignerInfo<'a> {
        Self {
            certificate,
            component_fingerprint,
        }
    }

    /// Returns a reference to the OpenPGP certificate.
    pub fn certificate(&self) -> &OpenpgpCert {
        self.certificate
    }

    /// Returns a reference to the OpenPGP fingerprint of the component key that issued the
    /// signature.
    pub fn component_fingerprint(&self) -> &str {
        &self.component_fingerprint
    }
}

/// The representation of an OpenPGP signature check.
///
/// Holds a reference to an [`OpenpgpSignature`] and optionally a [`SignerInfo`].
#[derive(Debug)]
pub struct OpenpgpSignatureCheck<'a> {
    signature: &'a OpenpgpSignature,
    signer_info: Option<SignerInfo<'a>>,
}

impl<'a> OpenpgpSignatureCheck<'a> {
    /// Creates a new [`OpenpgpSignatureCheck`] from an [`OpenpgpSignature`] and an optional
    /// [`SignerInfo`].
    ///
    /// `signer_info` is `Some` if the signature has been cryptographically validated with that
    /// signer, and `None` if verification could not determine a valid signer.
    pub(crate) fn new(
        signature: &'a OpenpgpSignature,
        signer_info: Option<SignerInfo<'a>>,
    ) -> OpenpgpSignatureCheck<'a> {
        Self {
            signature,
            signer_info,
        }
    }

    /// Returns a reference to the signature.
    pub fn signature(&self) -> &OpenpgpSignature {
        self.signature
    }

    /// Returns a reference to the signer information.
    pub fn signer_info(&self) -> Option<&SignerInfo<'a>> {
        self.signer_info.as_ref()
    }
}
