//! Import of OpenPGP certificates as VOA verifiers.

pub mod destructured;
pub mod error;

use std::{
    fs::File,
    io::Read,
    path::{Path, PathBuf},
    str::FromStr,
};

use error::Error;
use log::debug;
use pgp::{
    composed::{ArmorOptions, Deserializable, SignedPublicKey},
    types::KeyDetails,
};
use voa_core::{VerifierWriter, identifiers::Technology};

/// The data needed for importing an [OpenPGP certificate] to a [VOA] hierarchy.
///
/// Wraps a [`SignedPublicKey`].
///
/// [OpenPGP certificate]: https://openpgp.dev/book/certificates.html
/// [VOA]: https://uapi-group.org/specifications/specs/file_hierarchy_for_the_verification_of_os_artifacts/
#[derive(Clone, Debug)]
pub struct OpenPgpImport(SignedPublicKey);

impl OpenPgpImport {
    /// Creates an [`OpenPgpImport`] from an OpenPGP certificate file.
    ///
    /// Supports both binary and ASCII-armored data.
    ///
    /// # Errors
    ///
    /// Returns an error if
    ///
    /// - `path` is not a regular file,
    /// - or a [`SignedPublicKey`] cannot be created from `path`.
    pub fn from_file(path: impl AsRef<Path>) -> Result<Self, crate::Error> {
        let path = path.as_ref();
        debug!("Reading an OpenPGP certificate from file: {path:?}");

        if !path.is_file() {
            return Err(error::Error::PathIsNotAFile {
                path: path.to_path_buf(),
            }
            .into());
        }

        let file = File::open(path).map_err(|source| crate::Error::IoPath {
            path: path.to_path_buf(),
            context: "reading the file",
            source,
        })?;

        let (pubkey, _) = SignedPublicKey::from_reader_single(file).map_err(|source| {
            crate::Error::OpenPgpPath {
                path: path.to_path_buf(),
                context: "parsing an OpenPGP certificate from file",
                source,
            }
        })?;

        Ok(Self(pubkey))
    }

    /// Creates an [`OpenPgpImport`] from a [`Read`] implementation.
    ///
    /// Supports binary and ASCII-armored data.
    ///
    /// # Errors
    ///
    /// Returns an error if a [`SignedPublicKey`] cannot be created from the [`Read`]
    /// implementation.
    pub fn from_reader(reader: impl Read) -> Result<Self, crate::Error> {
        debug!("Reading an OpenPGP certificate from stdin");
        let (pubkey, _) = SignedPublicKey::from_reader_single(reader).map_err(|source| {
            crate::Error::OpenPgp {
                context: "reading an OpenPGP certificate from stdin",
                source,
            }
        })?;

        Ok(Self(pubkey))
    }
}

impl TryFrom<&Path> for OpenPgpImport {
    type Error = crate::Error;

    /// Creates an [`OpenPgpImport`] from a path.
    ///
    /// The path must be a regular file.
    /// Supports binary and ASCII-armored data.
    ///
    /// # Note
    ///
    /// Delegates to [`OpenPgpImport::from_file`].
    ///
    /// # Errors
    ///
    /// Returns an error if [`OpenPgpImport::from_file`] fails.
    fn try_from(value: &Path) -> Result<Self, Self::Error> {
        Self::from_file(value)
    }
}

impl FromStr for OpenPgpImport {
    type Err = crate::Error;

    /// Creates an [`OpenPgpImport`] from a string slice.
    ///
    /// # Note
    ///
    /// Delegates to [`TryFrom`] [`Path`].
    ///
    /// # Errors
    ///
    /// Returns an error if [`TryFrom`] [`Path`] fails.
    fn from_str(s: &str) -> Result<Self, Self::Err> {
        Self::try_from(Path::new(s))
    }
}

impl VerifierWriter for OpenPgpImport {
    /// Returns the verifier as bytes.
    ///
    /// OpenPGP certificates are stored ASCII-armored and this function applies armor.
    fn to_bytes(&self) -> Result<Vec<u8>, voa_core::Error> {
        self.0
            .to_armored_bytes(ArmorOptions::default())
            .map_err(|source| {
                voa_core::Error::WriteError(Box::new(crate::Error::OpenPgp {
                    context: "creating an ASCII-armored OpenPGP public key",
                    source,
                }))
            })
    }

    /// Returns the file name of the verifier.
    ///
    /// The file name consists of the [OpenPGP fingerprint] and the string representation of the
    /// [`Technology`] as suffix.
    ///
    /// [OpenPGP fingerprint]: https://openpgp.dev/book/certificates.html#fingerprint
    fn file_name(&self) -> PathBuf {
        let fingerprint = self.0.fingerprint();
        let technology = self.technology();

        PathBuf::from(format!("{fingerprint}.{technology}"))
    }

    /// Returns the technology of the verifier ([`Technology::Openpgp`]).
    fn technology(&self) -> Technology {
        Technology::Openpgp
    }
}

#[cfg(test)]
mod tests {
    use pgp::{
        composed::{EncryptionCaps, KeyType, SecretKeyParamsBuilder, SubkeyParamsBuilder},
        ser::Serialize,
    };
    use rand::thread_rng;
    use rstest::{fixture, rstest};
    use simplelog::{ColorChoice, Config, LevelFilter, TermLogger, TerminalMode};
    use tempfile::{NamedTempFile, tempdir};
    use testresult::TestResult;

    use super::*;

    /// The configuration for tests.
    struct TestConfig {
        /// Whether to ASCII-armor the input file(s).
        pub armor: bool,
    }

    /// Init logger
    fn init_logger() {
        if TermLogger::init(
            LevelFilter::Trace,
            Config::default(),
            TerminalMode::Stderr,
            ColorChoice::Auto,
        )
        .is_err()
        {
            debug!("Not initializing another logger, as one is initialized already.");
        }
    }

    /// Creates an OpenPGP certificate ([`SignedPublicKey`]).
    #[fixture]
    fn openpgp_cert() -> TestResult<SignedPublicKey> {
        let mut signkey = SubkeyParamsBuilder::default();
        signkey
            .key_type(KeyType::Ed25519Legacy)
            .can_sign(true)
            .can_encrypt(EncryptionCaps::None)
            .can_authenticate(false);
        let mut key_params = SecretKeyParamsBuilder::default();
        key_params
            .key_type(KeyType::Ed25519Legacy)
            .can_certify(true)
            .can_sign(false)
            .can_encrypt(EncryptionCaps::None)
            .primary_user_id("John Doe <jdoe@example.org>".to_string())
            .subkeys(vec![signkey.build()?]);

        let secret_key_params = key_params.build()?;
        let secret_key = secret_key_params.generate(thread_rng())?;
        let pubkey = SignedPublicKey::from(secret_key);
        Ok(pubkey)
    }

    /// Ensures that [`OpenPgpImport`] can be created from OpenPGP certificate.
    ///
    /// The OpenPGP certificate input may be
    /// Further ensures that the export of the verifier with [`OpenPgpImport::write_to_hierarchy`]
    /// works.
    #[rstest]
    #[case::single_binary(TestConfig{armor: false})]
    #[case::single_ascii_armor(TestConfig{armor: true})]
    fn write_to_hierarchy_succeeds(
        openpgp_cert: TestResult<SignedPublicKey>,
        #[case] config: TestConfig,
    ) -> TestResult {
        init_logger();

        let input_pubkey = openpgp_cert?;
        let mut temp_file = NamedTempFile::new()?;
        if config.armor {
            input_pubkey.to_armored_writer(&mut temp_file, ArmorOptions::default())?;
        } else {
            input_pubkey.to_writer(&mut temp_file)?;
        }
        let path = temp_file.path();

        let import = OpenPgpImport::try_from(path)?;

        let temp_dir = tempdir()?;
        let output_dir = temp_dir.path();
        import.write_to_hierarchy(
            output_dir,
            "os".parse()?,
            "purpose".parse()?,
            Some("context".parse()?),
        )?;

        let output_file = output_dir
            .join("os")
            .join("purpose")
            .join("context")
            .join(import.technology().to_string())
            .join(import.file_name());

        let (output_pubkey, _) = SignedPublicKey::from_armor_file(&output_file)?;

        assert_eq!(input_pubkey, output_pubkey);

        Ok(())
    }

    /// Ensures that [`OpenPgpImport::new`] fails if the input path is not a file or a directory.
    #[test]
    #[cfg(target_os = "linux")]
    fn try_from_path_fails_on_path_not_a_file() -> TestResult {
        match OpenPgpImport::try_from(PathBuf::from("/dev/null").as_path()) {
            Ok(verifier) => {
                panic!("Should have failed, but succeeded to create a verifier: {verifier:?}");
            }
            Err(error) => match error {
                crate::Error::Import(error::Error::PathIsNotAFile { .. }) => {}
                error => {
                    panic!("Did not return the correct error, got: {error}");
                }
            },
        }
        Ok(())
    }
}
