#![allow(dead_code)]

use std::collections::HashMap;
use std::sync::{Mutex, MutexGuard, OnceLock};

use tempfile::TempDir;

use sequoia_openpgp as openpgp;
pub use openpgp::Result;
use openpgp::Cert;
use openpgp::crypto::PublicKeyAlgorithm;
use openpgp::policy::StandardPolicy;
use openpgp::types::HashAlgorithm;
use openpgp::types::SymmetricAlgorithm;
use openpgp::packet;
use openpgp::packet::key;

use sequoia_cryptoki::{
    Key,
    KeyCapability,
    TokenSelector,
    dicewords,

    // Some cryptoki imports.
    cryptoki::session::UserType,
    cryptoki::types::AuthPin,
};

mod backend;

/// The Giant Testsuite Lock.
static GTL: Mutex<()> = Mutex::new(());

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

pub struct SqCryptoki {
    pub ctx: &'static sequoia_cryptoki::Context,

    /// Serializes all tests.
    gtl_locked: MutexGuard<'static, ()>,

    now: std::time::SystemTime,
}

static BACKEND: OnceLock<(TempDir, sequoia_cryptoki::Context, Box<dyn backend::Backend>)>
    = OnceLock::new();

impl SqCryptoki {
    fn init() -> &'static (TempDir, sequoia_cryptoki::Context, Box<dyn backend::Backend>) {
        let b = BACKEND.get_or_init(|| {
            let state = TempDir::new()
                .expect("can create a temporary directory");

            let backend = backend::init(state.path());

            // Setup the environment.
            for (key, value) in backend.env() {
                // SAFETY: Not safe.  Godspeed.
                eprintln!("Setting ENV {}={}", key, value);
                unsafe {
                    std::env::set_var(key, value);
                }
            }

            let module = match std::env::var("SEQUOIA_CRYPTOKI_MODULE") {
                Ok(module) => module,
                Err(err) => {
                    panic!(
                        "SEQUOIA_CRYPTOKI_MODULE is not set (or invalid): {}.  \
                         Must be set to a PKCS#11 to run the test suite.  \
                         WARNING: any attached tokens will be reinitialized.",
                        err);
                }
            };
            eprintln!("Using PKCS#11 module {}", module);
            let ctx = sequoia_cryptoki::ContextBuilder::new()
                .disable_config_file()
                .add_module(module)
                .build()
                .unwrap();

            (state, ctx, backend)
        });

        b
    }

    fn context() -> &'static sequoia_cryptoki::Context {
        &Self::init().1
    }

    fn backend() -> &'static Box<dyn backend::Backend> {
        &Self::init().2
    }

    /// Allocates the next free slot.
    pub fn allocate_slot(&self) -> u64 {
        Self::backend().allocate_slot()
    }

    /// Creates a new SqCryptoki context in a new, emphemeral home
    /// directory.  The clock is set to the specified time.
    pub fn at(now: std::time::SystemTime) -> Self {
        let gtl_locked = loop {
            match GTL.lock() {
                Ok(lock) => break lock,
                Err(_) => GTL.clear_poison(),
            }
        };
        let ctx = Self::context();

        let config = SqCryptoki {
            ctx,
            gtl_locked,
            now,
        };

        config
    }

    /// Creates a new SqCryptoki context in a new, emphemeral home
    /// directory.  The clock is set to the current time.
    pub fn new() -> Self {
        // The current time.
        let mut now = std::time::SystemTime::now();
        let since_epoch = now.duration_since(std::time::UNIX_EPOCH).unwrap();
        now = now - std::time::Duration::new(0, since_epoch.subsec_nanos());

        Self::at(now)
    }

    pub fn finalize(self) -> Result<()> {
        Ok(())
    }

    pub fn algo_supported(algo: &sequoia_cryptoki::PublicKeyAlgorithm) -> bool {
        let backend = Self::backend();
        backend.algo_supported(algo)
    }

    /// Returns the current time.
    pub fn now(&self) -> std::time::SystemTime {
        self.now
    }

    /// Lists the tokens.
    pub fn try_token_list(&self) -> Result<()> {
        todo!()
    }

    /// Lists the tokens.
    pub fn token_list(&self) -> () {
        self.try_token_list()
            .expect("token list should have succeeded")
    }

    /// Initialize or reinitialize a token.
    pub fn try_token_init_(&self, token: TokenSelector,
                           label: Option<&str>, so_pin: &str,
                           pin: &str)
        -> Result<String>
    {
        let token = self.ctx.select_one_token(&token)?;

        let info = token.info();

        if info.token_initialized() {
            eprintln!("Token already initialized, reinitializing.");
        } else {
            eprintln!("Token not yet initialized.  \
                       Performing initial initialization.");
        }

        let new_token_label = label
            .map(|label| label.to_string())
            .unwrap_or_else(|| {
                dicewords::random_words(2).unwrap().join("-")
            });

        // Initialize (or reinitialize) the token.  This deletes all
        // non-indestructible objects!  If the token is uninitialized,
        // then this sets the SO PIN to the specified PIN.  If the token
        // is being reinitialized, the SO PIN must match the SO's PIN.
        // Afterwards, the user PIN is unset.
        let so_pin = AuthPin::new(so_pin.into());
        token.backend().context().init_token(
            token.slot().clone(), &so_pin, &new_token_label)?;

        let user_pin = AuthPin::new(pin.into());

        // Initialize the user PIN.
        let session = token.backend().context()
            .open_rw_session(token.slot().clone())?;
        session.login(UserType::So, Some(&so_pin))?;
        session.init_pin(&user_pin)?;
        drop(session);

        Ok(new_token_label)
    }

    /// Initialize a token.
    ///
    /// Returns the token's label.
    pub fn try_token_init(&self, slot: u64, label: Option<&str>, so_pin: &str,
                          pin: &str)
        -> Result<String>
    {
        let token = TokenSelector {
            backend: None,
            slot: Some(slot),
            token_label: None,
            token_serial_number: None,
        };

        self.try_token_init_(token, label, so_pin, pin)
    }

    /// Lists the tokens.
    ///
    /// Returns the token's label.
    pub fn token_init(&self, slot: u64, label: Option<&str>, so_pin: &str,
                      pin: &str)
        -> String
    {
        self.try_token_init(slot, label, so_pin, pin)
            .expect("token init should have succeeded")
    }

    /// Reinitializes a token.
    ///
    /// Returns the token's label.
    pub fn try_token_reinit(&self, token_label: &str,
                            so_pin: &str, pin: &str)
        -> Result<()>
    {
        let token = TokenSelector {
            backend: None,
            slot: None,
            token_label: Some(token_label.into()),
            token_serial_number: None,
        };

        self.try_token_init_(token, Some(token_label), so_pin, pin)?;
        Ok(())
    }

    /// Reinitializes a token.
    ///
    /// Returns the token's label.
    pub fn token_reinit(&self, token_label: &str,
                        so_pin: &str, pin: &str)
    {
        self.try_token_reinit(token_label, so_pin, pin)
            .expect("token reinit should have succeeded");
    }

    /// Generate a key.
    fn try_key_generate_intern(&self,
                               slot: u64,
                               key_label: Option<&str>,
                               algo: PublicKeyAlgorithm,
                               for_signing: bool,
                               profile: openpgp::Profile,
                               pin: &str)
                               -> Result<Key>
    {
        let token = self.ctx.select_one_token(&TokenSelector {
            backend: None,
            slot: Some(slot),
            token_label: None,
            token_serial_number: None,
        })?;

        if ! token.info().token_initialized() {
            return Err(anyhow::anyhow!(
                "Token not initialized.  Try running `sq-cryptoki token init` \
                 first."));
        }

        let session = token.open_rw_user_session(pin)?;

        match (algo, for_signing) {
            (PublicKeyAlgorithm::EdDSA, _) => {
                assert_eq!(for_signing, algo.for_signing());
		self.ctx.generate_eddsa(&session, token, None, key_label)
	    }
            (PublicKeyAlgorithm::Ed25519, _) => {
                assert_eq!(for_signing, algo.for_signing());
                self.ctx.generate_ed25519(&session, token, profile,
                                          None, key_label)
            }
            (PublicKeyAlgorithm::Ed448, _) => {
                assert_eq!(for_signing, algo.for_signing());
                self.ctx.generate_ed448(&session, token, profile,
                                        None, key_label)
            }
            (PublicKeyAlgorithm::MLDSA65_Ed25519, _) => {
                assert_eq!(for_signing, algo.for_signing());
                self.ctx.generate_mldsa65_ed25519(&session, token, profile,
                                                  None, key_label)
            }
            (PublicKeyAlgorithm::MLDSA87_Ed448, _) => {
                assert_eq!(for_signing, algo.for_signing());
                self.ctx.generate_mldsa87_ed448(&session, token, profile,
                                                None, key_label)
            }
            (PublicKeyAlgorithm::SLHDSA128s, _) => {
                assert!(for_signing);
                self.ctx.generate_slh_dsa_128s(&session, token, profile,
                                               None, key_label)
            }
            (PublicKeyAlgorithm::SLHDSA128f, _) => {
                assert!(for_signing);
                self.ctx.generate_slh_dsa_128f(&session, token, profile,
                                               None, key_label)
            }
            (PublicKeyAlgorithm::SLHDSA256s, _) => {
                assert!(for_signing);
                self.ctx.generate_slh_dsa_256s(&session, token, profile,
                                               None, key_label)
            }
            (PublicKeyAlgorithm::X25519, _) => {
                assert_eq!(for_signing, algo.for_signing());
                self.ctx.generate_x25519(&session, token, profile,
                                         None, key_label)
            }
            (PublicKeyAlgorithm::X448, _) => {
                assert_eq!(for_signing, algo.for_signing());
                self.ctx.generate_x448(&session, token, profile,
                                       None, key_label)
            }
            (PublicKeyAlgorithm::RSAEncryptSign, _) => {
                if for_signing {
                    self.ctx.generate_rsa_for_signing(&session, token, profile,
                                                      3072, None, key_label)
                } else {
                    self.ctx.generate_rsa_for_encryption(&session, token, profile,
                                                         3072, None, key_label)
                }
            }
            (PublicKeyAlgorithm::MLKEM768_X25519, _) => {
                assert!(! for_signing);
                self.ctx.generate_mlkem768_x25519(&session, token, profile,
                                                  None, key_label)
            }
            (PublicKeyAlgorithm::MLKEM1024_X448, _) => {
                assert!(! for_signing);
                self.ctx.generate_mlkem1024_x448(&session, token, profile,
                                                 None, key_label)
            }
            _ => panic!("{} not implemented", algo),
        }
    }

    /// Generate a key.
    pub fn try_key_generate(&self,
                            slot: u64,
                            key_label: Option<&str>,
                            algo: PublicKeyAlgorithm,
                            for_signing: bool,
                            profile: openpgp::Profile,
                            pin: &str)
                            -> Result<Key>
    {
        self.try_key_generate_intern(
            slot, key_label, algo, for_signing, profile, pin)
    }

    /// Generate a key.
    pub fn key_generate(&self,
                        slot: u64,
                        key_label: Option<&str>,
                        algo: PublicKeyAlgorithm,
                        for_signing: bool,
                        profile: openpgp::Profile,
                        pin: &str)
                        -> Key
    {
        self.try_key_generate(
            slot, key_label, algo, for_signing, profile, pin)
            .expect("key generate should have succeeded")
    }

    /// Try and generate a parameterized v4 Cv25519 key.
    pub fn try_legacy_cv25519_key_generate(&self,
                                           token_label: &str,
                                           hash_algo: Option<HashAlgorithm>,
                                           sym_algo: Option<SymmetricAlgorithm>,
                                           key_label: Option<&str>,
                                           pin: &str)
                                           -> Result<Key>
    {
        let token = self.ctx.select_one_token(&TokenSelector {
            backend: None,
            slot: None,
            token_label: Some(token_label.to_string()),
            token_serial_number: None,
        })?;

        if ! token.info().token_initialized() {
            return Err(anyhow::anyhow!(
                "Token not initialized.  Try running `sq-cryptoki token init` \
                 first."));
        }

        let session = token.open_rw_user_session(pin)?;

        self.ctx.generate_legacy_cv25519(&session, token, None,
                                         hash_algo, sym_algo,
                                         key_label)
    }

    /// Generate a parameterized v4 Cv25519 key.
    pub fn legacy_cv25519_key_generate(&self,
                                       token_label: &str,
                                       hash_algo: Option<HashAlgorithm>,
                                       sym_algo: Option<SymmetricAlgorithm>,
                                       key_label: Option<&str>,
                                       pin: &str)
                                       -> Key
    {
        self.try_legacy_cv25519_key_generate(
            token_label, hash_algo, sym_algo, key_label, pin)
            .expect("key generate should have succeeded")
    }

    /// Generate a composite key.
    pub fn try_composite_key_generate(&self,
                                      slot: u64,
                                      key_label: Option<&str>,
                                      algo: PublicKeyAlgorithm,
                                      for_signing: bool,
                                      profile: openpgp::Profile,
                                      pin: &str)
                                      -> Result<Key>
    {
        self.try_key_generate_intern(
            slot, key_label, algo, for_signing, profile, pin)
    }

    /// Generate a composite key.
    pub fn composite_key_generate(&self,
                                  slot: u64,
                                  key_label: Option<&str>,
                                  algo: PublicKeyAlgorithm,
                                  for_signing: bool,
                                  profile: openpgp::Profile,
                                  pin: &str)
                                  -> Key
    {
        self.try_composite_key_generate(
            slot, key_label, algo, for_signing, profile, pin)
            .expect("key generate should have succeeded")
    }

    /// Generate a key.
    pub fn try_import(&self,
                      token_label: &str,
                      key: &packet::key::Key<key::SecretParts,
                                             key::UnspecifiedRole>,
                      key_capability: KeyCapability,
                      key_label: Option<&str>,
                      pin: &str)
                      -> Result<Key>
    {
        let token = self.ctx.select_one_token(&TokenSelector {
            backend: None,
            slot: None,
            token_label: Some(token_label.to_string()),
            token_serial_number: None,
        })?;

        if ! token.info().token_initialized() {
            return Err(anyhow::anyhow!(
                "Token not initialized.  Try running `sq-cryptoki token init` \
                 first."));
        }

        let session = token.open_rw_user_session(pin)?;

        self.ctx.import(&session, token, key_label, key, key_capability)
    }

    /// Import a key.
    pub fn import(&self,
                  token_label: &str,
                  key: &packet::key::Key<key::SecretParts,
                                         key::UnspecifiedRole>,
                  key_capability: KeyCapability,
                  key_label: Option<&str>,
                  pin: &str)
                  -> Key
    {
        self.try_import(token_label, key, key_capability, key_label, pin)
            .expect("can import")
    }

    /// Export a certificate.
    pub fn try_key_export(&self)
        -> Result<(HashMap<String, String>, Cert)>
    {
        todo!()
    }

    /// Export a certificate.
    pub fn key_export(&self)
        -> (HashMap<String, String>, Cert)
    {
        todo!()
    }

    /// Sign a message.
    pub fn try_key_sign(&self, _user_pin: &str)
        -> Result<()>
    {
        todo!()
    }

    /// Sign a message.
    pub fn key_sign(&self, _user_pin: &str)
        -> ()
    {
        todo!()
    }

    /// Decrypt a message.
    ///
    /// Returns the plaintext.
    pub fn try_key_decrypt(&self, _user_pin: &str)
        -> Result<()>
    {
        todo!()
    }

    /// Decrypt a message.
    ///
    /// Returns the plaintext.
    pub fn key_decrypt(&self, _user_pin: &str)
        -> ()
    {
        todo!()
    }
}

/// A prompter that always returns an error.
#[derive(Default)]
pub struct CancelPrompt(());

impl sequoia_cryptoki::Prompt for CancelPrompt {
    fn prompt_password(&self, _: &str) -> Result<openpgp::crypto::Password> {
        Err(anyhow::anyhow!("nope"))
    }
}
