use std::collections::HashMap;
use std::collections::HashSet;
use std::ops::Deref;
use std::path::Path;
use std::sync::Arc;
use std::sync::Weak;

use futures::lock::Mutex;

use anyhow::Context;

use sequoia_openpgp as openpgp;
use openpgp::Cert;
use openpgp::Fingerprint;
use openpgp::Result;
use openpgp::crypto::Password;
use openpgp::crypto::SessionKey;
use openpgp::crypto::mpi;
use openpgp::packet;
use openpgp::types::HashAlgorithm;
use openpgp::types::PublicKeyAlgorithm;

pub use sequoia_gpg_agent;
use sequoia_gpg_agent as gpg_agent;

use gpg_agent::sequoia_ipc as ipc;
use ipc::Keygrip;

use sequoia_keystore_backend as backend;
use backend::DeviceHandle;
use backend::Error;
use backend::ImportStatus;
use backend::PasswordSource;
use backend::Protection;
use backend::utils::Directory;

mod certd;

#[derive(Clone)]
pub struct Backend {
    inner: Arc<Mutex<BackendInternal>>,
}

struct BackendInternal {
    home: Directory,

    // gpg-agent doesn't store the OpenPGP key data structures nor
    // does it have all the information required to reconstruct them
    // (in particular, it's missing the key's creation time).
    // Instead, we the so-called keygrip which is more or less a hash
    // over the public key material.  But, to use a key, we need the
    // OpenPGP public key.  We look for it opportunistically in the
    // user's default cert-d.  (For ephemeral backends, we use a
    // cert-d in the ephemeral directory.)  Since looking up a key by
    // keygrip means doing a full scan, we cache the results.
    certd: Arc<certd::CertD>,

    // One device per agent.
    devices: Vec<Device>,
}

/// A Device exposes the (usable) keys managed by a particular
/// gpg-agent.
///
/// A key is usable if we have the OpenPGP data structure.  If we
/// don't have the OpenPGP key, we ignore the key.
#[derive(Clone)]
pub struct Device {
    // $GNUPGHOME (escaped).
    id: String,
    inner: Arc<Mutex<DeviceInternal>>
}

impl Device {
    /// Creates a new `WeakDevice` from this `Device`.
    fn downgrade(&self) -> WeakDevice {
        WeakDevice {
            id: self.id.clone(),
            inner: Arc::downgrade(&self.inner),
        }
    }
}

/// A `Device`, but with a weak reference to the data.
///
/// Before you use this, you need to upgrade this to a `Device`.
#[derive(Clone)]
pub struct WeakDevice {
    id: String,
    inner: Weak<Mutex<DeviceInternal>>,
}

impl WeakDevice {
    /// Upgrades the `WeakDevice` to a `Device`, if possible.
    fn upgrade(&self) -> Option<Device> {
        Some(Device {
            id: self.id.clone(),
            inner: self.inner.upgrade()?,
        })
    }
}

struct DeviceInternal {
    // $GNUPGHOME (escaped).
    id: String,

    agent: Arc<Mutex<gpg_agent::Agent>>,

    // See the documentation for BackendInternal.certd.
    certd: Arc<certd::CertD>,

    // A map from id (fingerprint) to key.
    keys: HashMap<Fingerprint, Key>,
}

#[derive(Clone)]
pub struct Key {
    // The fingerprint is also the id.
    fpr: Fingerprint,
    inner: Arc<Mutex<KeyInternal>>,
}

/// A secret key.
struct KeyInternal {
    device: WeakDevice,

    keygrip: Keygrip,
    fingerprint: Fingerprint,
    public_key: packet::Key<packet::key::PublicParts,
                            packet::key::UnspecifiedRole>,
    agent: Arc<Mutex<gpg_agent::Agent>>,
    /// The cached password.
    password: Option<Password>,
}

impl Backend {
    /// Initializes a gpg-agent backend.
    ///
    /// `home` is the directory where the backend will look for its
    /// configuration, e.g., `$HOME/.sq/keystore/gpg-agent`.
    ///
    /// If `default` is true, this backend uses the default GnuPG home
    /// directory, i.e., `$HOME/.gnupg`, by default.
    pub async fn init<P: AsRef<Path>>(home: P, default: bool)
        -> Result<Self>
    {
        log::trace!("Backend::init");

        let home = Directory::from(home.as_ref());

        let certd = Arc::new(certd::CertD::new()?);

        let agents: &[(&str, Option<&Path>)] = if default {
            &[
                ("default", None),
            ][..]
        } else {
            &[][..]
        };

        Ok(Self::init_internal(home, &agents[..], certd).await?)
    }

    /// Initializes an ephemeral gpg-agent backend.
    ///
    /// This is primarily useful for testing.
    pub async fn init_ephemeral() -> Result<Self> {
        log::trace!("Backend::init_ephemeral");

        let home = Directory::ephemeral()?;

        let agents: &[(String, Option<&Path>)] = &[
            (home.display().to_string(), Some(home.deref())),
        ][..];

        let certd_directory = home.to_path_buf().join("pgp.cert.d");
        std::fs::create_dir_all(&certd_directory)
            .with_context(|| {
                format!("Creating {}", certd_directory.display())
            })?;

        let certd = Arc::new(certd::CertD::open(certd_directory)?);

        Ok(Self::init_internal(home.clone(), &agents[..], certd).await?)
    }

    async fn init_internal<S>(home: Directory,
                              agents: &[ (S, Option<&Path>) ],
                              certd: Arc<certd::CertD>)
        -> Result<Self>
        where S: AsRef<str>
    {
        log::trace!("Backend::init_internal");
        log::info!("sequoia-keystore-gpg-agent's home directory is: {}",
                   home.display());

        let mut devices = Vec::with_capacity(agents.len());
        for (id, path) in agents {
            let id = id.as_ref();
            let agent = if let Some(path) = path {
                gpg_agent::Agent::connect_to(path).await?
            } else {
                gpg_agent::Agent::connect_to_default().await?
            };

            let mut device = Device {
                id: id.to_string(),
                inner: Arc::new(Mutex::new(DeviceInternal {
                    id: id.to_string(),
                    agent: Arc::new(Mutex::new(agent)),
                    certd: Arc::clone(&certd),
                    keys: HashMap::new(),
                }))
            };

            if let Err(err) = device.scan().await {
                log::info!("Failed to connect to gpg-agent for {}: {}",
                           id.to_string(), err);
            }
            devices.push(device);
        }

        Ok(Backend {
            inner: Arc::new(Mutex::new(BackendInternal {
                home,
                certd,
                devices,
            }))
        })
    }
}

#[async_trait::async_trait]
impl backend::Backend for Backend {
    fn id(&self) -> String {
        "gpg-agent".into()
    }

    async fn scan(&mut self) -> Result<()> {
        log::trace!("Backend::scan");

        let mut backend = self.inner.lock().await;
        for device in backend.devices.iter_mut() {
            if let Err(err) = device.scan().await {
                log::info!("While scanning {}: {}",
                           device.id(), err);
            }
        }

        Ok(())
    }

    async fn list<'a>(&'a self)
        -> Box<dyn Iterator<Item=Box<dyn backend::DeviceHandle + Send + Sync + 'a>>
               + Send + Sync + 'a>
    {
        log::trace!("Backend::list");

        let backend = self.inner.lock().await;

        Box::new(
            backend.devices.iter()
                .map(|device| {
                    Box::new(device.clone())
                        as Box<dyn backend::DeviceHandle + Send + Sync>
                })
                .collect::<Vec<_>>()
                .into_iter())
    }

    async fn find_device<'a>(&self, id: &str)
        -> Result<Box<dyn backend::DeviceHandle + Send + Sync + 'a>>
    {
        log::trace!("Backend::find_device");

        let backend = self.inner.lock().await;

        for device in backend.devices.iter() {
            if device.id == id {
                return Ok(Box::new(device.clone())
                          as Box<dyn backend::DeviceHandle + Send + Sync>);
            }
        }

        Err(Error::NotFound(id.into()).into())
    }

    async fn find_key<'a>(&self, id: &str)
        -> Result<Box<dyn backend::KeyHandle + Send + Sync + 'a>>
    {
        log::trace!("Backend::find_key");

        let backend = self.inner.lock().await;

        // The first time through we look for the key without
        // scanning.  If we don't find it, then we rescan.
        for scan in [false, true] {
            for device in backend.devices.iter() {
                let device_ref = device;
                let mut device = device.inner.lock().await;

                if scan {
                    log::trace!("Rescanning {}", device.id);
                    if let Err(err) = device.scan(device_ref).await {
                        log::info!("Failed to connect to gpg-agent for {}: {}",
                                   device.id, err);
                    }
                }

                for (key_id, key) in device.keys.iter() {
                    if &key_id.to_string() == id {
                        return Ok(Box::new(key.clone())
                                  as Box<dyn backend::KeyHandle + Send + Sync>);
                    }
                }
            }
        }

        Err(Error::NotFound(id.into()).into())
    }

    async fn import<'a>(&self, _cert: Cert)
        -> Result<Vec<(ImportStatus,
                       Box<dyn backend::KeyHandle + Send + Sync + 'a>)>>
    {
        log::trace!("Backend::import");

        Err(Error::ExternalImportRequired(Some(
            "To import a key into a gpg-agent, use something like: \
             gpg --import key.pgp".into())).into())
    }
}

impl Device {
    async fn scan(&mut self) -> Result<()> {
        let mut device = self.inner.lock().await;
        device.scan(&self).await
    }
}

impl DeviceInternal {
    /// Scans.
    ///
    /// This interface is a bit awkward so that it can be called while
    /// the device lock is held.
    async fn scan(&mut self, device_ref: &Device)
        -> Result<()>
    {
        log::trace!("DeviceInternal::scan");

        let mut agent = self.agent.lock().await;

        // List the keys available on the agent.
        let keyinfos = match agent.list_keys().await {
            Ok(keyinfos) => keyinfos,
            Err(err) => {
                log::debug!("Listing keys on the agent for {}: {}",
                            self.id, err);
                return Ok(());
            }
        };

        log::trace!("Agent has {} keys: {}",
                    keyinfos.len(),
                    keyinfos
                        .iter()
                        .map(|keyinfo| {
                            keyinfo.keygrip().to_string()
                        })
                        .collect::<Vec<String>>()
                        .join(", "));

        let agent_keygrips: HashSet<&Keygrip>
            = keyinfos.iter().map(|i| i.keygrip()).collect();

        // List our keys.
        let mut our_keys: Vec<(Keygrip, Fingerprint)>
            = Vec::with_capacity(self.keys.len());
        for key in self.keys.values() {
            let key = key.inner.lock().await;
            our_keys.push((key.keygrip.clone(), key.fingerprint.clone()));
        }
        let our_keygrips: HashSet<&Keygrip>
            = HashSet::from_iter(our_keys.iter().map(|(keygrip, _)| keygrip));

        // Add any new keys.
        let new_keygrips = agent_keygrips.difference(&our_keygrips);
        match self.certd.find(HashSet::from_iter(new_keygrips.cloned())).await {
            Ok(certs) => {
                for (keygrip, public_key) in certs.into_iter() {
                    let fingerprint = public_key.fingerprint();
                    self.keys.insert(
                        fingerprint.clone(),
                        Key {
                            fpr: fingerprint.clone(),
                            inner: Arc::new(Mutex::new(KeyInternal {
                                device: device_ref.downgrade(),
                                keygrip,
                                fingerprint,
                                public_key,
                                agent: Arc::clone(&self.agent),
                                password: None,
                            })),
                        });
                }
            }
            Err(err) => {
                log::debug!("Listing cert-d: {}", err);
            }
        }

        // If some keys were removed on the agent, then remove them
        // locally.
        let removed_keygrips = our_keygrips.difference(&agent_keygrips);
        let mut our_keys_map: Option<HashMap<&Keygrip, &Fingerprint>> = None;
        for removed_keygrip in removed_keygrips {
            let our_keys_map = our_keys_map.get_or_insert_with(|| {
                HashMap::from_iter(
                    our_keys.iter().map(|(keygrip, fpr)| (keygrip, fpr)))
            });

            let fpr = our_keys_map.get(removed_keygrip).expect("have it");
            self.keys.remove(*fpr);
        }

        Ok(())
    }
}

#[async_trait::async_trait]
impl backend::DeviceHandle for Device {
    fn id(&self) -> String {
        log::trace!("Device::id");

        self.id.clone()
    }

    async fn available(&self) -> bool {
        log::trace!("Device::available");

        true
    }

    async fn configured(&self) -> bool {
        log::trace!("Device::configured");

        true
    }

    async fn registered(&self) -> bool {
        log::trace!("Device::registered");

        true
    }

    async fn lock(&mut self) -> Result<()> {
        log::trace!("Device::lock");

        // This is a noop: an agent can't be locked.
        Ok(())
    }

    async fn list<'a>(&'a self)
        -> Box<dyn Iterator<Item=Box<dyn backend::KeyHandle + Send + Sync + 'a>>
               + Send + Sync + 'a>
    {
        log::trace!("Device::list");

        let device_ref = self;
        let mut device = self.inner.lock().await;
        if let Err(err) = device.scan(device_ref).await {
            log::info!("Failed to connect to gpg-agent for {}: {}",
                       device.id, err);
        }

        let keys = device.keys.values()
            .map(|key| {
                Box::new(key.clone())
                    as Box<dyn backend::KeyHandle + Send + Sync>
            })
            .collect::<Vec<_>>();

        Box::new(keys.into_iter())
    }
}

#[async_trait::async_trait]
impl backend::KeyHandle for Key {
    fn id(&self) -> String {
        log::trace!("Key::id");

        self.fpr.to_string()
    }

    fn fingerprint(&self) -> Fingerprint {
        log::trace!("Key::fingerprint");

        self.fpr.clone()
    }

    async fn device<'a>(&self)
        -> Box<dyn backend::DeviceHandle + Send + Sync + 'a>
    {
        let key = self.inner.lock().await;
        let device = key.device.upgrade().expect("device lives longer than key");
        drop(key);

        Box::new(device)
    }

    /// Returns whether the key is available.
    ///
    /// A key managed by gpg-agent is considered to *not* be available
    /// if:
    ///
    /// - It is disabled
    /// - It is missing
    /// - It is on a smartcard, and the smartcard is not inserted.
    async fn available(&self) -> bool {
        log::trace!("Key::available");

        let key = self.inner.lock().await;
        let mut agent = key.agent.lock().await;
        let Ok(keyinfo) = agent.key_info(&key.keygrip).await else {
            // If this fails, the key was deleted.  In that case,
            // return false as we clearly can't address the key.
            return false;
        };

        // Keys that are disabled or missing are not available.
        if keyinfo.key_disabled()
            || keyinfo.keytype() == gpg_agent::keyinfo::KeyType::Missing
        {
            return false;
        }

        if keyinfo.keytype() == gpg_agent::keyinfo::KeyType::Smartcard {
            // On a smart card.  See if the smart card is inserted.
            match agent.card_info().await {
                Ok(cardinfo) => {
                    return cardinfo.keys().any(|fpr| fpr == key.fingerprint);
                }
                Err(err) => {
                    log::debug!("Getting smartcard info from gpg-agent: {}", err);
                }
            }
            false
        } else {
            // Soft key.
            true
        }
    }

    async fn locked(&self) -> Protection {
        log::trace!("Key::locked");

        let key = self.inner.lock().await;
        let mut agent = key.agent.lock().await;
        let Ok(keyinfo) = agent.key_info(&key.keygrip).await else {
            // If this fails, the key was deleted.  In that case, just
            // return Unlocked: it doesn't matter; we can't address
            // the key.
            return Protection::Unlocked;
        };
        drop(agent);
        drop(key);

        // As a first approximation, a key is locked if it has
        // protection, and the passphrase is not cached.
        //
        // A signing key may be locked and the passphrase may be
        // cached, but if `ignore-cache-for-signing` is set, then
        // `gpg` will prompt for a passphrase anyways.  Unfortunately,
        // AFAICS there is no way to determine if
        // `ignore-cache-for-signing` is set using gpg-agent's IPC
        // interface.
        if keyinfo.protection()
            == &gpg_agent::keyinfo::KeyProtection::NotProtected
        {
            Protection::Unlocked
        } else {
            // It's protected...  probably.  It could be the key is on
            // a smartcard, and the slot is unlocked.  Unfortunately,
            // we have no reliably way to find out.
            if keyinfo.passphrase_cached() {
                Protection::Unlocked
            } else {
                Protection::ExternalPassword(Some("\
                    This key is protected by GnuPG's agent.  To continue, \
                    you may need to provide your password to GnuPG's \
                    pinentry".to_string()))
            }
        }
    }

    async fn password_source(&self) -> PasswordSource {
        PasswordSource::ExternalSideEffect
    }

    async fn decryption_capable(&self) -> bool {
        log::trace!("Key::decryption_capable");

        let key = self.inner.lock().await;
        key.public_key.pk_algo().for_encryption()
    }

    async fn signing_capable(&self) -> bool {
        log::trace!("Key::signing_capable");

        let key = self.inner.lock().await;
        key.public_key.pk_algo().for_signing()
    }

    async fn unlock(&mut self, password: Option<&Password>) -> Result<()> {
        log::trace!("Key::unlock");

        let password = if let Some(password) = password {
            // We can use the password, but only if loopback mode is
            // enabled.
            password
        } else {
            // We can only prompt as a side effect.
            return Err(Error::NoExternalPassword(
                Some("Cannot prompt user for password".into())).into());
        };
        let mut key = self.inner.lock().await;
        key.password = Some(password.clone());
        Ok(())
    }

    async fn lock(&mut self) -> Result<()> {
        log::trace!("Key::lock");

        let mut key = self.inner.lock().await;

        // Clear our local cache.
        key.password = None;

        // Clear the password on the agent.
        let mut agent = key.agent.lock().await;
        agent.forget_passphrase(&key.keygrip.to_string(), |_| ()).await?;

        Ok(())
    }

    async fn public_key(&self)
        -> packet::Key<packet::key::PublicParts,
                       packet::key::UnspecifiedRole>
    {
        log::trace!("Key::public_key");

        let key = self.inner.lock().await;
        key.public_key.clone()
    }

    async fn decrypt_ciphertext(&mut self,
                                ciphertext: &mpi::Ciphertext,
                                plaintext_len: Option<usize>)
        -> Result<SessionKey>
    {
        log::trace!("Key::decrypt_ciphertext");

        let key = self.inner.lock().await;
        let agent = key.agent.lock().await;
        let public_key = key.public_key.clone();
        let mut keypair = agent.keypair(&public_key)?;
        // Drop the lock while we do the actual operation.
        drop(agent);

        if let Some(password) = key.password.as_ref() {
            keypair = keypair.with_password(password.clone());
        }
        drop(key);

        keypair.decrypt_async(ciphertext, plaintext_len).await
    }

    async fn sign(&mut self, hash_algo: HashAlgorithm, digest: &[u8])
        -> Result<(PublicKeyAlgorithm, mpi::Signature)>
    {
        log::trace!("Key::sign");

        let key = self.inner.lock().await;
        let agent = key.agent.lock().await;
        let public_key = key.public_key.clone();
        let mut keypair = agent.keypair(&public_key)?;
        // Drop the lock while we do the actual operation.
        drop(agent);

        if let Some(password) = key.password.as_ref() {
            keypair = keypair.with_password(password.clone());
        }
        drop(key);

        keypair.sign_async(hash_algo, digest).await
            .map(|mpis| {
                (public_key.pk_algo(), mpis)
            })
    }

    async fn export(&mut self)
        -> Result<openpgp::packet::Key<
            openpgp::packet::key::SecretParts,
            openpgp::packet::key::UnspecifiedRole>>
    {
        let key = self.inner.lock().await;
        let mut agent = key.agent.lock().await;

        Ok(agent.export(key.public_key.clone()).await?)
    }

    async fn change_password(&mut self, password: Option<&Password>)
        -> Result<()>
    {
        log::trace!("KeyHandle::change_password({}, {})",
                    self.fingerprint(),
                    if let Some(password) = password {
                        if password.map(|p| p.is_empty()) {
                            "clear password"
                        } else {
                            "set password"
                        }
                    } else {
                        "ask for password"
                    });

        if password.is_some() {
            return Err(Error::NoInlinePassword(None).into());
        };

        let key = self.inner.lock().await;

        let agent = key.agent.lock().await;
        let mut keypair = agent.keypair(&key.public_key)?;

        Ok(keypair.password(true).await?)
    }

    async fn delete_secret_key_material(&mut self)
        -> Result<()>
    {
        log::trace!("KeyHandle::delete_secret_key_material");

        let key = self.inner.lock().await;

        let agent = key.agent.lock().await;
        let mut keypair = agent.keypair(&key.public_key)?;

        Ok(keypair.delete_key(false).await?)
    }
}

#[cfg(test)]
mod tests {
    use super::*;

    use std::fs::File;
    use std::io::Write;

    use anyhow::Context;

    use openpgp::KeyHandle;
    use openpgp::parse::Parse;
    use openpgp::policy::StandardPolicy;
    use openpgp::serialize::Serialize;

    const P: &StandardPolicy = &StandardPolicy::new();

    use backend::test_framework;

    use backend::Backend as _;

    fn preinit() -> bool {
        // XXX: Return false if gpg-agent is not installed.
        true
    }

    async fn init_backend() -> Backend {
        gpg_agent::trace(true);
        let backend = Backend::init_ephemeral().await
            .expect("creating an ephemeral backend");

        // Enable loopback mode.
        {
            let mut backend = backend.inner.lock().await;

            let home = backend.home.to_path_buf();
            let gpg_agent_conf = home.join("gpg-agent.conf");

            let mut f = File::options()
                .create(true).append(true)
                .open(&gpg_agent_conf)
                .with_context(|| {
                    format!("Opening {}", gpg_agent_conf.display())
                }).expect("can open gpg-agent.conf");
            writeln!(&mut f, "allow-loopback-pinentry").expect("can write");
            drop(f);

            let device = backend.devices.get_mut(0).unwrap();
            let device = device.inner.lock().await;
            let mut agent = device.agent.lock().await;
            agent.reload().await.expect("can reload");
        }

        // If we didn't supply a password, suppress pinentry.
        {
            let mut backend = backend.inner.lock().await;
            let device = backend.devices.get_mut(0).unwrap();
            let device = device.inner.lock().await;
            let mut agent = device.agent.lock().await;
            agent.set_pinentry_mode(gpg_agent::PinentryMode::Cancel);
        }

        backend
    }

    async fn import_cert(backend: &mut Backend, cert: &Cert) {
        let mut backend = backend.inner.lock().await;

        // Import the keys into the agent.
        {
            let device = backend.devices.get_mut(0).unwrap();
            let device = device.inner.lock().await;
            let mut agent = device.agent.lock().await;

            for k in cert.keys().secret() {
                agent.import(P,
                             &cert, k.key().parts_as_secret().expect("have secret"),
                             true, true).await.expect("can import");
            }
        }

        // And insert the certificate into our local certd so that we
        // can find the OpenPGP keys.
        backend.certd.certd().insert(
            &cert.fingerprint().to_string(),
            (), false,
            |(), disk| {
                let cert_;
                let cert = if let Some(disk) = disk {
                    // Merge.
                    let disk = Cert::from_bytes(disk).expect("valid cert");
                    cert_ = cert.clone().merge_public(disk).expect("can merge");
                    &cert_
                } else {
                    // New.
                    cert
                };

                let mut bytes = Vec::new();
                cert.serialize(&mut bytes).expect("can serialize to a vec");
                Ok(bytes.into())
            })
            .expect("inserted");
    }

    sequoia_keystore_backend::generate_tests!(
        preinit, false, // Tests don't need to be serialized.
        Backend, init_backend,
        import_cert,
        true, // Can import encrypted secret key material.
        None, // unlimited key sets
        None, // No default password
        true, // Can export.
        true, // Can change password.
        true // Can delete secret key material.
    );
}
