use std::collections::HashMap;
use std::collections::HashSet;
use std::path::Path;

use futures::lock::Mutex;
use futures::lock::MutexGuard;

use sequoia_openpgp as openpgp;
use openpgp::cert::raw::RawCert;
use openpgp::packet::Key;
use openpgp::packet::key::PublicParts;
use openpgp::packet::key::UnspecifiedRole;
use openpgp::parse::Parse;

use openpgp_cert_d as cert_d;

use crate::Keygrip;
use crate::Result;

struct CertDCache {
    certd_tag: Option<cert_d::Tag>,
    // A cache of the keygrip to keys.
    keys: HashMap<Keygrip, Key<PublicParts, UnspecifiedRole>>,
}

pub struct CertD {
    certd: cert_d::CertD,
    cache: Mutex<CertDCache>,
}

impl CertD {
    /// Opens the default cert-d.
    pub fn new() -> Result<Self> {
        Ok(CertD {
            certd: cert_d::CertD::new()?,
            cache: Mutex::new(CertDCache {
                certd_tag: None,
                keys: HashMap::default(),
            }),
        })
    }

    /// Opens the specified cert-d.
    pub fn open<P>(path: P) -> Result<Self>
        where P: AsRef<Path>
    {
        Ok(CertD {
            certd: cert_d::CertD::with_base_dir(path.as_ref())?,
            cache: Mutex::new(CertDCache {
                certd_tag: None,
                keys: HashMap::default(),
            }),
        })
    }

    /// Returns a reference to the underlying cert-d.
    pub fn certd(&self) -> &cert_d::CertD {
        &self.certd
    }

    // Check that the in-memory cache is up to date, and rebuild it if
    // necessary.
    fn rescan<'a>(&self, mut cache: MutexGuard<'a, CertDCache>)
        -> MutexGuard<'a, CertDCache>
    {
        let certd_tag = self.certd.tag();
        if let Some(tag) = cache.certd_tag.as_ref() {
            if tag == &certd_tag {
                // Up to date.
                return cache;
            }
        }

        // Rebuild the cache.
        //
        // We should probably parallelize this, but we are only
        // dealing with raw certificates, so even for 1000s of
        // certificates this is pretty fast.
        let mut items = Vec::with_capacity(128);
        for item in self.certd.iter_files() {
            let (fingerprint, f) = match item {
                Ok(r) => r,
                Err(err) => {
                    log::debug!("Reading certd: {}", err);
                    continue;
                }
            };

            let cert = match RawCert::from_reader(f) {
                Ok(cert) => cert,
                Err(err) => {
                    log::debug!("Parsing certificate for {}: {}",
                                fingerprint, err);
                    continue;
                }
            };

            for key in cert.keys() {
                let Ok(keygrip) = Keygrip::of(key.mpis()) else {
                    continue;
                };

                items.push((keygrip.clone(), key.clone()));
            }
        }

        cache.certd_tag = Some(certd_tag);
        cache.keys = HashMap::from_iter(items.into_iter());

        cache
    }

    /// Look up certificates by their keygrips.
    pub async fn find(&self, keygrips: HashSet<&Keygrip>)
        -> Result<HashMap<Keygrip, Key<PublicParts, UnspecifiedRole>>>
    {
        let cache = self.cache.lock().await;

        // To avoid blocking the current thread, we run the rescan
        // function on a separate thread.  It signals that it is done
        // by sending the result via a one-shot channel, which we can
        // asynchronously wait on.  This avoids blocking the main
        // thread.
        let (sender, receiver) = futures::channel::oneshot::channel::<_>();

        std::thread::scope(|s| {
            s.spawn(move || {
                sender.send(self.rescan(cache))
            });
        });
        let cache = receiver.await?;

        let mut items = HashMap::default();

        for keygrip in keygrips {
            if let Some(key) = cache.keys.get(keygrip) {
                log::trace!("{} maps to {}",
                            keygrip, key.fingerprint());

                items.insert(keygrip.clone(), key.clone());
            } else {
                log::debug!("{} unusable: can't find the public key \
                             (try running: gpg --export | sq cert import)",
                            keygrip);
            }
        }

        Ok(items)
    }
}
