use std::fmt::Write;
use std::time::SystemTime;

use anyhow::Context as _;

use sequoia_openpgp as openpgp;
use openpgp::Profile;

use chrono::DateTime;
use chrono::Utc;

use crate::Context;
use crate::dicewords::random_words;
use crate::PublicKeyAlgorithm;
use crate::Result;

/// How a key is bundled on the OpenPGP side.
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum Bundling {
    /// A standalone type.
    Standalone,

    /// The post-quantum half of a hybrid key.
    PostQuantumHalf,

    /// The traditional half of a hybrid key.
    TraditionalHalf,
}

/// Key generation.
impl Context {
    /// Returns a key ID and a key label for a new key object.
    ///
    /// The key ID encodes all of the parameters needed to recreate
    /// the OpenPGP key data structure that are not otherwise stored
    /// on the token.
    ///
    /// `profile` is the key's version.
    ///
    /// `bundling` is how the key is bundled, if at all.  In OpenPGP,
    /// MLDSA and ML-KEM post-quantum keys are combined with a
    /// traditional key.  In PKCS#11, these are separate.  As such,
    /// two PKCS#11 keys are needed for a single OpenPGP post-quantum
    /// key.  This indicates whether the key is standalone (i.e.,
    /// corresponds to an OpenPGP key), or is part of a composite key,
    /// and, if so, which part.
    ///
    /// `creation_time` is the key's creation time.  If `None`, the
    /// current time is used.
    ///
    /// `key_id_extra` encodes any extra parameters that should be
    /// included in the key ID.  Currently, this is only used by v4
    /// ECDH, which include a hash algorithm and a symmetric algorithm
    /// in the public key data structure.
    ///
    /// If `key_label` is `None`, a random, unambiguous key label is
    /// selected.  Otherwise, the value is used as is.
    ///
    /// The key label (whether provided by the user or generating
    /// randomly) is added to the key ID.  This helps ensure that keys
    /// created at the same time have different key IDs.
    pub (crate) fn generate_key_id(&self,
                                   algo: PublicKeyAlgorithm,
                                   profile: Profile, bundling: Bundling,
                                   creation_time: Option<SystemTime>,
                                   key_id_extra: Option<&str>,
                                   key_label: Option<&str>)
        -> Result<(String, String)>
    {
        let creation_time: DateTime<Utc> =
            creation_time.unwrap_or_else(SystemTime::now).into();

        // The key ID without the label.
        let mut base_key_id =
            format!("pgp:v{}{}:{}",
                    match profile {
                        Profile::RFC9580 => 6,
                        Profile::RFC4880 => 4,
                        p => return Err(openpgp::Error::InvalidArgument(
                            format!("unknown profile {:?}", p)).into()),
                    },
                    match bundling {
                        Bundling::Standalone => "",
                        Bundling::PostQuantumHalf => "pq",
                        Bundling::TraditionalHalf => "t",
                    },
		    algo);
        if let Some(key_id_extra) = key_id_extra {
            base_key_id.push('-');
            base_key_id.push_str(key_id_extra);
        }
        base_key_id.push(':');
        // Encode the creation time in ISO 8601-1:2019's
        // basic format.
	write!(base_key_id, "{}", creation_time.format("%Y%m%dT%H%M%SZ"))?;

        let (key_id, key_label) = if let Some(key_label) = key_label {
            // The user provided a label.  Use that as the suffix.

            // Make sure the resulting key ID is unambiguous.
            let key_id = format!("{}:{}", base_key_id, key_label);
            if let Err(err) = self.check_no_keys_have_id(&key_id) {
                return Err(err).with_context(|| {
                    format!("Cannot create an unambiguous key ID \
                             with the specified label ({})",
                            key_label)
                });
            }

            (key_id, key_label.to_string())
        } else {
            // Generate a random label.

            loop {
                // Use a few random words.
                let key_label = random_words(3)?.join("-");
                let key_id = format!("{}:{}", base_key_id, key_label);
                // Make sure the label and key id are unique.  If not,
                // try again.
                if self.check_no_keys_have_label(&key_label).is_ok()
                    && self.check_no_keys_have_id(&key_id).is_ok()
                {
                    break (key_id, key_label);
                }
            }
        };

        Ok((key_id, key_label))
    }
}
