use std::str::FromStr;

use sequoia_openpgp as openpgp;
use openpgp::crypto::mpi::PublicKey;
use openpgp::packet;

/// Public-key algorithms.
#[non_exhaustive]
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum PublicKeyAlgorithm {
    /// RSA
    ///
    /// RSA is defined by [RFC 4880] and [RFC 9580].
    ///
    ///   [RFC 4880]: https://www.rfc-editor.org/rfc/rfc4880.html#section-9.1
    ///   [RFC 9580]: https://www.rfc-editor.org/rfc/rfc9580.html#name-public-key-algorithms
    RSA,

    /// Cv25519.
    ///
    /// Cv25519 is defined by RFC 6637.
    ///
    /// Note: [Cv25519 has been deprecated] by RFC 9580 and must only
    /// be used with v4 certificates.  For v6 certificates, prefer
    /// [`PublicKeyAlgorithm::X25519`] instead.
    ///
    ///   [RFC 6637]: https://www.rfc-editor.org/rfc/rfc6637
    ///   [Cv25519 has been deprecated]: https://www.rfc-editor.org/rfc/rfc9580.html#name-ecc-curves-for-openpgp
    Cv25519,

    /// X25519.
    ///
    /// X25519 is defined by [RFC 9580].
    ///
    /// X25519 keys can be used with v4-based certificates, but for
    /// wide compatibility it is recommended to use Cv25519.
    ///
    ///   [RFC 9580]: https://www.rfc-editor.org/rfc/rfc9580.html#name-public-key-algorithms
    X25519,

    /// X448.
    ///
    /// X448 is defined by [RFC 9580].
    ///
    ///   [RFC 9580]: https://www.rfc-editor.org/rfc/rfc9580.html#name-public-key-algorithms
    X448,

    /// EdDSA.
    ///
    /// EdDSA is defined by RFC 6637.
    ///
    /// Note: [EdDSA has been deprecated] by RFC 9580 and must only
    /// be used with v4 certificates.  For v6 certificates, prefer
    /// [`PublicKeyAlgorithm::Ed25519`] instead.
    ///
    ///   [RFC 6637]: https://www.rfc-editor.org/rfc/rfc6637
    ///   [Ed25519 has been deprecated]: https://www.rfc-editor.org/rfc/rfc9580.html#name-ecc-curves-for-openpgp
    EdDSA,

    /// Ed25519.
    ///
    /// Ed25519 is defined by [RFC 9580].
    ///
    /// Ed25519 keys can be used with v4-based certificates, but for
    /// wide compatibility it is recommended to use EdDSA keys.
    ///
    ///   [RFC 9580]: https://www.rfc-editor.org/rfc/rfc9580.html#name-public-key-algorithms
    Ed25519,

    /// Ed448.
    ///
    /// Ed448 is defined by [RFC 9580].
    ///
    ///   [RFC 9580]: https://www.rfc-editor.org/rfc/rfc9580.html#name-public-key-algorithms
    Ed448,

    /// ML-DSA-65+Ed25519.
    ///
    /// As defined by the [Post-Quantum Cryptography in OpenPGP draft
    /// specification].
    ///
    /// [Post-Quantum Cryptography in OpenPGP draft specification]: https://datatracker.ietf.org/doc/draft-ietf-openpgp-pqc/
    MLDSA65_Ed25519,

    /// ML-DSA-87+Ed448.
    ///
    /// As defined by the [Post-Quantum Cryptography in OpenPGP draft
    /// specification].
    ///
    /// [Post-Quantum Cryptography in OpenPGP draft specification]: https://datatracker.ietf.org/doc/draft-ietf-openpgp-pqc/
    MLDSA87_Ed448,

    /// SLH-DSA-128s.
    ///
    /// As defined by the [Post-Quantum Cryptography in OpenPGP draft
    /// specification].
    ///
    /// [Post-Quantum Cryptography in OpenPGP draft specification]: https://datatracker.ietf.org/doc/draft-ietf-openpgp-pqc/
    SLHDSA128s,

    /// SLH-DSA-128f.
    ///
    /// As defined by the [Post-Quantum Cryptography in OpenPGP draft
    /// specification].
    ///
    /// [Post-Quantum Cryptography in OpenPGP draft specification]: https://datatracker.ietf.org/doc/draft-ietf-openpgp-pqc/
    SLHDSA128f,

    /// SLH-DSA-256s.
    ///
    /// As defined by the [Post-Quantum Cryptography in OpenPGP draft
    /// specification].
    ///
    /// [Post-Quantum Cryptography in OpenPGP draft specification]: https://datatracker.ietf.org/doc/draft-ietf-openpgp-pqc/
    SLHDSA256s,

    /// MLKEM768+X25519.
    ///
    /// As defined by the [Post-Quantum Cryptography in OpenPGP draft
    /// specification].
    ///
    /// [Post-Quantum Cryptography in OpenPGP draft specification]: https://datatracker.ietf.org/doc/draft-ietf-openpgp-pqc/
    MLKEM768_X25519,

    /// MLKEM1024+X448.
    ///
    /// As defined by the [Post-Quantum Cryptography in OpenPGP draft
    /// specification].
    ///
    /// [Post-Quantum Cryptography in OpenPGP draft specification]: https://datatracker.ietf.org/doc/draft-ietf-openpgp-pqc/
    MLKEM1024_X448,

    // WARNING: If you add a new variant, be sure to also add it to
    // PublicKeyAlgorithm::variants!
}

impl PublicKeyAlgorithm {
    /// Returns all variants.
    pub fn variants() -> impl Iterator<Item=Self> {
	use PublicKeyAlgorithm::*;
	[
	    RSA,
	    Cv25519,
	    X25519,
	    X448,
	    EdDSA,
	    Ed25519,
	    Ed448,
	    MLDSA65_Ed25519,
	    MLDSA87_Ed448,
	    SLHDSA128s,
	    SLHDSA128f,
	    SLHDSA256s,
	    MLKEM768_X25519,
	    MLKEM1024_X448,
	].into_iter()
    }

    /// Returns whether the algorithm can be used for signing.
    pub fn for_signing(&self) -> bool {
	use PublicKeyAlgorithm::*;
	match self {
	    RSA => true,
	    Cv25519 => false,
	    X25519 => false,
	    X448 => false,
	    EdDSA => true,
	    Ed25519 => true,
	    Ed448 => true,
	    MLDSA65_Ed25519 => true,
	    MLDSA87_Ed448 => true,
	    SLHDSA128s => true,
	    SLHDSA128f => true,
	    SLHDSA256s => true,
	    MLKEM768_X25519 => false,
	    MLKEM1024_X448 => false,
	}
    }

    /// Returns whether the algorithm can be used for encryption.
    pub fn for_encryption(&self) -> bool {
	use PublicKeyAlgorithm::*;
	match self {
	    RSA => true,
	    Cv25519 => true,
	    X25519 => true,
	    X448 => true,
	    EdDSA => false,
	    Ed25519 => false,
	    Ed448 => false,
	    MLDSA65_Ed25519 => false,
	    MLDSA87_Ed448 => false,
	    SLHDSA128s => false,
	    SLHDSA128f => false,
	    SLHDSA256s => false,
	    MLKEM768_X25519 => true,
	    MLKEM1024_X448 => true,
	}
    }
}

impl From<PublicKeyAlgorithm> for openpgp::crypto::PublicKeyAlgorithm {
    /// Note: this is a lossy transformation.
    ///
    /// `EdDSA` and `Cv25519` map onto to `EdDSA` and `ECDH`,
    /// respectively, which are actually group of algorithms.
    fn from(pk: PublicKeyAlgorithm) -> openpgp::crypto::PublicKeyAlgorithm {
	use PublicKeyAlgorithm::*;
	match pk {
	    RSA => openpgp::crypto::PublicKeyAlgorithm::RSAEncryptSign,
	    Cv25519 => openpgp::crypto::PublicKeyAlgorithm::ECDH,
	    X25519 => openpgp::crypto::PublicKeyAlgorithm::X25519,
	    X448 => openpgp::crypto::PublicKeyAlgorithm::X448,
	    EdDSA => openpgp::crypto::PublicKeyAlgorithm::EdDSA,
	    Ed25519 => openpgp::crypto::PublicKeyAlgorithm::Ed25519,
	    Ed448 => openpgp::crypto::PublicKeyAlgorithm::Ed448,
	    MLDSA65_Ed25519 => openpgp::crypto::PublicKeyAlgorithm::MLDSA65_Ed25519,
	    MLDSA87_Ed448 => openpgp::crypto::PublicKeyAlgorithm::MLDSA87_Ed448,
	    SLHDSA128s => openpgp::crypto::PublicKeyAlgorithm::SLHDSA128s,
	    SLHDSA128f => openpgp::crypto::PublicKeyAlgorithm::SLHDSA128f,
	    SLHDSA256s => openpgp::crypto::PublicKeyAlgorithm::SLHDSA256s,
	    MLKEM768_X25519 => openpgp::crypto::PublicKeyAlgorithm::MLKEM768_X25519,
	    MLKEM1024_X448 => openpgp::crypto::PublicKeyAlgorithm::MLKEM1024_X448,
	}
    }
}

impl<P, R> TryFrom<&packet::Key<P, R>> for PublicKeyAlgorithm
where
    P: packet::key::KeyParts,
    R: packet::key::KeyRole,
{
    type Error = anyhow::Error;

    fn try_from(key: &packet::Key<P, R>) -> std::result::Result<Self, Self::Error> {
	#[allow(deprecated)]
        match key.mpis() {
	    PublicKey::RSA { .. } => {
                Ok(PublicKeyAlgorithm::RSA)
            }

	    PublicKey::ECDH { curve, .. } => {
		match curve {
		    openpgp::crypto::Curve::Cv25519 => {
			Ok(PublicKeyAlgorithm::Cv25519)
		    }
		    curve => {
			Err(anyhow::anyhow!("Unsupported algorithm: {} with {}",
					    key.pk_algo(), curve))
		    }
		}
            }

	    PublicKey::EdDSA { curve, .. } => {
		match curve {
		    openpgp::crypto::Curve::Ed25519 => {
			Ok(PublicKeyAlgorithm::Ed25519)
		    }
		    curve => {
			Err(anyhow::anyhow!("Unsupported algorithm: {} with {}",
					    key.pk_algo(), curve))
		    }
		}
            }

	    PublicKey::X25519 { .. } => {
                Ok(PublicKeyAlgorithm::X25519)
            }
	    PublicKey::X448 { .. } => {
                Ok(PublicKeyAlgorithm::X448)
            }
	    PublicKey::Ed25519 { .. } => {
                Ok(PublicKeyAlgorithm::Ed25519)
            }
	    PublicKey::Ed448 { .. } => {
                Ok(PublicKeyAlgorithm::Ed448)
            }
            PublicKey::MLDSA65_Ed25519 { .. } => {
                Ok(PublicKeyAlgorithm::MLDSA65_Ed25519)
            }
            PublicKey::MLDSA87_Ed448 { .. } => {
                Ok(PublicKeyAlgorithm::MLDSA87_Ed448)
            }
            PublicKey::SLHDSA128s { .. } => {
                Ok(PublicKeyAlgorithm::SLHDSA128s)
            }
            PublicKey::SLHDSA128f { .. } => {
                Ok(PublicKeyAlgorithm::SLHDSA128f)
            }
            PublicKey::SLHDSA256s { .. } => {
                Ok(PublicKeyAlgorithm::SLHDSA256s)
            }
            PublicKey::MLKEM768_X25519 { .. } => {
                Ok(PublicKeyAlgorithm::MLKEM768_X25519)
            }
            PublicKey::MLKEM1024_X448 { .. } => {
                Ok(PublicKeyAlgorithm::MLKEM1024_X448)
            }
            _ => {
                Err(anyhow::anyhow!("Unsupported algorithm: {}", key.pk_algo()))
            }
        }
    }
}

impl std::fmt::Display for PublicKeyAlgorithm {
    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
	let algo = match self {
	    PublicKeyAlgorithm::RSA => "rsa",
	    PublicKeyAlgorithm::Cv25519 => "cv25519",
	    PublicKeyAlgorithm::X25519 => "x25519",
	    PublicKeyAlgorithm::X448 => "x448",
	    PublicKeyAlgorithm::EdDSA => "eddsa",
	    PublicKeyAlgorithm::Ed25519 => "ed25519",
	    PublicKeyAlgorithm::Ed448 => "ed448",
	    PublicKeyAlgorithm::MLDSA65_Ed25519 => "mldsa65+ed25519",
	    PublicKeyAlgorithm::MLDSA87_Ed448 => "mldsa87+ed448",
	    PublicKeyAlgorithm::SLHDSA128s => "slhdsa128s",
	    PublicKeyAlgorithm::SLHDSA128f => "slhdsa128f",
	    PublicKeyAlgorithm::SLHDSA256s => "slhdsa256s",
	    PublicKeyAlgorithm::MLKEM768_X25519 => "mlkem768+x25519",
	    PublicKeyAlgorithm::MLKEM1024_X448 => "mlkem1024+x448",
	};

	write!(f, "{}", algo)
    }
}

impl FromStr for PublicKeyAlgorithm {
    type Err = anyhow::Error;

    fn from_str(algo: &str) -> Result<Self, Self::Err> {
	match algo {
	    "rsa" => Ok(PublicKeyAlgorithm::RSA),
	    "cv25519" => Ok(PublicKeyAlgorithm::Cv25519),
	    "x25519" => Ok(PublicKeyAlgorithm::X25519),
	    "x448" => Ok(PublicKeyAlgorithm::X448),
	    "eddsa" => Ok(PublicKeyAlgorithm::EdDSA),
	    "ed25519" => Ok(PublicKeyAlgorithm::Ed25519),
	    "ed448" => Ok(PublicKeyAlgorithm::Ed448),
	    "mldsa65+ed25519" => Ok(PublicKeyAlgorithm::MLDSA65_Ed25519),
	    "mldsa87+ed448" => Ok(PublicKeyAlgorithm::MLDSA87_Ed448),
	    "slhdsa128s" => Ok(PublicKeyAlgorithm::SLHDSA128s),
	    "slhdsa128f" => Ok(PublicKeyAlgorithm::SLHDSA128f),
	    "slhdsa256s" => Ok(PublicKeyAlgorithm::SLHDSA256s),
	    "mlkem768+x25519" => Ok(PublicKeyAlgorithm::MLKEM768_X25519),
	    "mlkem1024+x448" => Ok(PublicKeyAlgorithm::MLKEM1024_X448),
	    algo => Err(anyhow::anyhow!("Unknown algorithm {}", algo)),
	}
    }
}

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

    #[test]
    fn roundtrip() {
	for v in PublicKeyAlgorithm::variants() {
	    let s = v.to_string();
	    assert_eq!(v, s.parse().expect(&format!("can parse {}", s)));
	}
    }

    #[test]
    fn for_signing() {
	for v in PublicKeyAlgorithm::variants() {
	    assert_eq!(v.for_signing(),
		       openpgp::crypto::PublicKeyAlgorithm::from(v).for_signing());
	}
    }

    #[test]
    fn for_encryption() {
	for v in PublicKeyAlgorithm::variants() {
	    assert_eq!(v.for_encryption(),
		       openpgp::crypto::PublicKeyAlgorithm::from(v).for_encryption());
	}
    }
}
