use std::collections::BTreeMap;
use std::collections::btree_map::Entry as BTreeEntry;
use std::path::Path;
use std::path::PathBuf;
use std::sync::Arc;

use tokio::sync::Mutex;

use sequoia_openpgp as openpgp;
use openpgp::Result;

use sequoia_directories::Home;
use sequoia_directories::Component;

#[cfg(feature = "softkeys")]
use sequoia_keystore_softkeys as softkeys;
#[cfg(feature = "openpgp-card")]
use sequoia_keystore_openpgp_card as openpgp_card;
#[cfg(feature = "tpm")]
use sequoia_keystore_tpm as tpm;
#[cfg(feature = "gpg-agent")]
use sequoia_keystore_gpg_agent as gpg_agent;
use sequoia_keystore_backend::Backend;

/// The keystore servers.
///
/// We have one root for each "home" directory.
pub struct Servers {
    servers: BTreeMap<PathBuf, Arc<Mutex<Server>>>,
}

impl Servers {
    const fn new() -> Self {
        Servers {
            servers: BTreeMap::new(),
        }
    }

    /// Returns the server for the specified "home" directory.
    ///
    /// This is normally derived from an instance of
    /// `sequoia_dirs::Home`.  Specifically, its the value returned by
    /// `home.data_dir(sequoia_dirs::Component::Keystore)`.
    pub(crate) async fn get(home: &Path) -> Result<Arc<Mutex<Server>>> {
        let mut servers = servers().lock().await;
        let home = home.to_path_buf();

        match servers.servers.entry(home.clone()) {
            BTreeEntry::Occupied(oe) => Ok(Arc::clone(oe.get())),
            BTreeEntry::Vacant(ve) => {
                let server = Arc::new(Mutex::new(Server::new(home).await?));

                ve.insert(Arc::clone(&server));

                Ok(server)
            }
        }
    }
}

/// A thread-safe reference to the SERVERS singleton.
pub fn servers() -> &'static Mutex<Servers> {
    use std::sync::OnceLock;

    static SERVERS: OnceLock<Mutex<Servers>> = OnceLock::new();
    SERVERS.get_or_init(|| Mutex::new(Servers::new()))
}

/// A Server.
pub struct Server {
    pub backends: Vec<Box<dyn Backend + Send + Sync>>,
}

impl Server {
    /// Initializes the server.
    ///
    /// This may be called at most once.
    ///
    /// `home` is the home directory to use.  You should usually
    /// instantiate a `sequoia_dirs::Home` object, and use the value
    /// returned by
    /// `home.data_dir(sequoia_dirs::Component::Keystore)`.
    ///
    /// Each backend is passed a backend-specific home directory of
    /// the form `HOME/$BACKEND`.
    async fn new(home: PathBuf) -> Result<Self> {
        log::trace!("Server::new({})", home.display());

        let mut backends = Vec::new();

        // Whether this is the default keystore server, or an
        // alternative keystore server.  The `default` keystore uses
        // shared resources by default.  An alternate keystore should
        // only uses local resources by default, and shared resources
        // that the user explicitly enabled.
        let is_default_keystore = Home::default()
            .and_then(|default| {
                let default_keystore = default.data_dir(Component::Keystore);
                Home::aliases(&home, &default_keystore).ok()
            })
            .unwrap_or(false);

        let mut add = |backend: Result<Box<dyn Backend + Send + Sync>>| {
            match backend {
                Ok(backend) => backends.push(backend),
                Err(err) => {
                    #[allow(unused_mut)]
                    let mut shown = false;

                    #[cfg(feature = "gpg-agent")]
                    use gpg_agent::sequoia_gpg_agent;

                    #[cfg(feature = "gpg-agent")]
                    if let Some(sequoia_gpg_agent::Error::GnuPG(
                        sequoia_gpg_agent::gnupg::Error::GPGConfMissing))
                        = err.downcast_ref()
                    {
                        log::info!("Failed to initialize backend: {:#}", err);
                        shown = true;
                    }

                    #[cfg(feature = "gpg-agent")]
                    if let Some(sequoia_gpg_agent::Error::GnuPG(
                        sequoia_gpg_agent::gnupg::Error::ComponentMissing(_)))
                        = err.downcast_ref()
                    {
                        log::info!("Failed to initialize backend: {:#}", err);
                        shown = true;
                    }

                    #[cfg(feature = "gpg-agent")]
                    if let Some(sequoia_gpg_agent::Error::GnuPGHomeMissing(_))
                        = err.downcast_ref()
                    {
                        log::info!("Failed to initialize backend: {:#}", err);
                        shown = true;
                    }

                    if ! shown {
                        log::error!("Failed to initialize backend: {:#}", err);
                    }
                }
            }
        };

        #[cfg(feature = "softkeys")]
        add(softkeys::Backend::init(Some(home.clone().join("softkeys"))).await
            .map(|b| {
                b as Box<dyn sequoia_keystore_backend::Backend + Send + Sync>
            }));
        #[cfg(feature = "openpgp-card")]
        add(openpgp_card::Backend::init(home.clone().join("openpgp-card"),
                                        is_default_keystore).await
            .map(|b| {
                Box::new(b) as Box<dyn sequoia_keystore_backend::Backend + Send + Sync>
            }));
        #[cfg(feature = "tpm")]
        add(tpm::Backend::init(home.clone().join("tpm"),
                               is_default_keystore).await
            .map(|b| {
                Box::new(b) as Box<dyn sequoia_keystore_backend::Backend + Send + Sync>
            }));
        #[cfg(feature = "gpg-agent")]
        add(gpg_agent::Backend::init(home.join("gpg-agent"),
                                     is_default_keystore).await
            .map(|b| {
                Box::new(b) as Box<dyn sequoia_keystore_backend::Backend + Send + Sync>
            }));

        log::trace!("Server::new({}) -> {} backends",
                    home.display(), backends.len());

        Ok(Server {
            backends,
        })
    }
}

impl Server {
    pub fn iter(&self)
        -> impl Iterator<Item=&Box<dyn Backend + Send + Sync>> + ExactSizeIterator
    {
        self.backends.iter()
    }
}

