//! VOA technology implementation for OpenPGP

use std::{
    collections::{HashMap, hash_map::Entry},
    fs::File,
};

use log::{debug, info, warn};
use rpgpie::certificate::Certificate;
use voa_core::{
    Voa,
    identifiers::{Context, Os, Purpose, Technology},
};

use crate::{FILE_SUFFIX, OpenpgpCert, cert::UncheckedOpenpgpCert};

/// An OpenPGP-specific view of a VOA hierarchy
#[derive(Debug)]
pub struct VoaOpenpgp(Voa);

impl Default for VoaOpenpgp {
    fn default() -> Self {
        Self::new()
    }
}

impl VoaOpenpgp {
    /// Initializes a new [`VoaOpenpgp`] instance.
    ///
    /// # Note
    ///
    /// Depending on the effective user ID of the calling process, [`VoaOpenpgp::lookup`] later
    /// happens either in system or in user mode.
    pub fn new() -> VoaOpenpgp {
        info!("Initializing VOA OpenPGP instance");

        Self(Voa::new())
    }

    /// Retrieves OpenPGP verifiers from available VOA hierarchies based on a query.
    ///
    /// Verifiers are returned as OpenPGP certificates.
    /// Valid verifiers are considered, if they match the VOA identifiers `os`, `purpose` and
    /// `context`. Invalid verifiers are skipped.
    ///
    /// # Note
    ///
    /// Skips OpenPGP verifiers under certain conditions and emits warning messages instead:
    ///
    /// - an OpenPGP verifier file name is not valid UTF-8,
    /// - an OpenPGP verifier file cannot be opened for reading,
    /// - an OpenPGP certificate cannot be read from an OpenPGP verifier file,
    /// - an OpenPGP verifier file contains more than one OpenPGP certificate,
    /// - or an OpenPGP verifier file name does not match the primary fingerprint of the OpenPGP
    ///   certificate contained in it.
    pub fn lookup(&self, os: Os, purpose: Purpose, context: Context) -> Vec<OpenpgpCert> {
        let verifiers = self.0.lookup(os, purpose, context, Technology::Openpgp);

        // Map of Fingerprint -> OpenPGPCert
        let mut certs: HashMap<String, UncheckedOpenpgpCert> = HashMap::new();

        for (canonicalized, verifiers) in verifiers {
            debug!("Processing VOA file {canonicalized:?}");

            // -- load cert from file --
            let Ok(mut file) = File::open(&canonicalized) else {
                warn!("Couldn't open verifier file {canonicalized:?}, skipping",);

                continue;
            };

            let Ok(mut loaded) = Certificate::load(&mut file) else {
                warn!("Failed to deserialize OpenPGP certificate {canonicalized:?}, skipping",);

                continue;
            };

            if loaded.len() != 1 {
                warn!(
                    "File {file:?} contained {} certificates, skipping",
                    loaded.len()
                );

                continue;
            }

            let certificate = loaded.remove(0);

            // -- check that filename and certificate content align --

            let Some(source_file_name) = canonicalized.file_name() else {
                // The path of the verifier is not valid UTF-8.
                warn!("The file name {canonicalized:?} is not valid UTF-8, skipping");
                continue;
            };

            let fingerprint = format!("{:02x?}", certificate.fingerprint());
            let expected_filename = format!("{fingerprint}{FILE_SUFFIX}");

            if *source_file_name != *expected_filename {
                warn!(
                    "Verifier filename {source_file_name:?} doesn't match expectation ({expected_filename}), skipping",
                );

                continue;
            }

            // -- merge if necessary, store in map --
            match certs.entry(fingerprint) {
                Entry::Vacant(ve) => {
                    // We don't have a certificate for that fingerprint yet, so we start a new entry
                    ve.insert(UncheckedOpenpgpCert {
                        certificate,
                        sources: verifiers,
                    });
                }
                Entry::Occupied(oe) => {
                    // We obtained data for a pre-existing fingerprint, merge in the new data
                    oe.into_mut().merge(certificate, verifiers);
                }
            }
        }

        // -- transform map into Vec of OpenPGPCert --
        certs.into_values().map(Into::into).collect()
    }
}
