// SPDX-FileCopyrightText: 2023 Paul Schaub <vanitasvitae@fsfe.org>
//
// SPDX-License-Identifier: Apache-2.0

package org.pgpainless.key.info

import java.util.*
import openpgp.openPgpKeyId
import org.bouncycastle.extensions.*
import org.bouncycastle.openpgp.*
import org.pgpainless.PGPainless
import org.pgpainless.algorithm.*
import org.pgpainless.exception.KeyException.UnboundUserIdException
import org.pgpainless.key.OpenPgpFingerprint
import org.pgpainless.key.SubkeyIdentifier
import org.pgpainless.key.util.KeyRingUtils
import org.pgpainless.policy.Policy
import org.pgpainless.signature.consumer.SignaturePicker
import org.pgpainless.signature.subpackets.SignatureSubpacketsUtil
import org.pgpainless.signature.subpackets.SignatureSubpacketsUtil.Companion.getKeyExpirationTimeAsDate
import org.pgpainless.util.DateUtil
import org.slf4j.LoggerFactory

class KeyRingInfo(
    val keys: PGPKeyRing,
    val policy: Policy = PGPainless.getPolicy(),
    val referenceDate: Date = Date()
) {

    @JvmOverloads
    constructor(
        keys: PGPKeyRing,
        referenceDate: Date = Date()
    ) : this(keys, PGPainless.getPolicy(), referenceDate)

    private val signatures: Signatures = Signatures(keys, referenceDate, policy)

    /** Primary {@link PGPPublicKey}.´ */
    val publicKey: PGPPublicKey = KeyRingUtils.requirePrimaryPublicKeyFrom(keys)

    /** Primary key ID. */
    val keyId: Long = publicKey.keyID

    /** Primary key fingerprint. */
    val fingerprint: OpenPgpFingerprint = OpenPgpFingerprint.of(keys)

    /** All User-IDs (valid, expired, revoked). */
    val userIds: List<String> = KeyRingUtils.getUserIdsIgnoringInvalidUTF8(publicKey)

    /** Primary User-ID. */
    val primaryUserId = findPrimaryUserId()

    /** Revocation State. */
    val revocationState = signatures.primaryKeyRevocation.toRevocationState()
    /**
     * Return the date on which the primary key was revoked, or null if it has not yet been revoked.
     *
     * @return revocation date or null
     */
    val revocationDate: Date? =
        if (revocationState.isSoftRevocation()) revocationState.date else null

    /**
     * Primary [PGPSecretKey] of this key ring or null if the key ring is not a [PGPSecretKeyRing].
     */
    val secretKey: PGPSecretKey? =
        when (keys) {
            is PGPSecretKeyRing -> keys.secretKey!!
            else -> null
        }

    /** OpenPGP key version. */
    val version: Int = publicKey.version

    /**
     * Return all {@link PGPPublicKey PGPPublicKeys} of this key ring. The first key in the list
     * being the primary key. Note that the list is unmodifiable.
     *
     * @return list of public keys
     */
    val publicKeys: List<PGPPublicKey> = keys.publicKeys.asSequence().toList()

    /** All secret keys. If the key ring is a [PGPPublicKeyRing], then return an empty list. */
    val secretKeys: List<PGPSecretKey> =
        when (keys) {
            is PGPSecretKeyRing -> keys.secretKeys.asSequence().toList()
            else -> listOf()
        }

    /** List of valid public subkeys. */
    val validSubkeys: List<PGPPublicKey> =
        keys.publicKeys.asSequence().filter { isKeyValidlyBound(it.keyID) }.toList()

    /** List of valid user-IDs. */
    val validUserIds: List<String> = userIds.filter { isUserIdBound(it) }

    /** List of valid and expired user-IDs. */
    val validAndExpiredUserIds: List<String> =
        userIds.filter {
            val certification = signatures.userIdCertifications[it] ?: return@filter false
            val revocation = signatures.userIdRevocations[it] ?: return@filter true
            return@filter !revocation.isHardRevocation &&
                certification.creationTime > revocation.creationTime
        }

    /** List of email addresses that can be extracted from the user-IDs. */
    val emailAddresses: List<String> =
        userIds.mapNotNull {
            PATTERN_EMAIL_FROM_USERID.matcher(it).let { m1 ->
                if (m1.find()) m1.group(1)
                else
                    PATTERN_EMAIL_EXPLICIT.matcher(it).let { m2 ->
                        if (m2.find()) m2.group(1) else null
                    }
            }
        }

    /** Newest direct-key self-signature on the primary key. */
    val latestDirectKeySelfSignature: PGPSignature? = signatures.primaryKeySelfSignature

    /** Newest primary-key revocation self-signature. */
    val revocationSelfSignature: PGPSignature? = signatures.primaryKeyRevocation

    /** Public-key encryption-algorithm of the primary key. */
    val algorithm: PublicKeyAlgorithm = PublicKeyAlgorithm.requireFromId(publicKey.algorithm)

    /** Creation date of the primary key. */
    val creationDate: Date = publicKey.creationTime!!

    /** Latest date at which the key was modified (either by adding a subkey or self-signature). */
    val lastModified: Date = getMostRecentSignature()?.creationTime ?: getLatestKeyCreationDate()

    /** True, if the underlying keyring is a [PGPSecretKeyRing]. */
    val isSecretKey: Boolean = keys is PGPSecretKeyRing

    /** True, if there are no encrypted secret keys. */
    val isFullyDecrypted: Boolean =
        !isSecretKey || secretKeys.all { it.hasDummyS2K() || it.isDecrypted() }

    /** True, if there are only encrypted secret keys. */
    val isFullyEncrypted: Boolean =
        isSecretKey && secretKeys.none { !it.hasDummyS2K() && it.isDecrypted() }

    /** List of public keys, whose secret key counterparts can be used to decrypt messages. */
    val decryptionSubkeys: List<PGPPublicKey> =
        keys.publicKeys
            .asSequence()
            .filter {
                if (it.keyID != keyId) {
                    if (signatures.subkeyBindings[it.keyID] == null) {
                        LOGGER.debug("Subkey ${it.keyID.openPgpKeyId()} has no binding signature.")
                        return@filter false
                    }
                }
                if (!it.isEncryptionKey) {
                    LOGGER.debug("(Sub-?)Key ${it.keyID.openPgpKeyId()} is not encryption-capable.")
                    return@filter false
                }
                return@filter true
            }
            .toList()

    /** Expiration date of the primary key. */
    val primaryKeyExpirationDate: Date?
        get() {
            val directKeyExpirationDate: Date? =
                latestDirectKeySelfSignature?.let { getKeyExpirationTimeAsDate(it, publicKey) }
            val possiblyExpiredPrimaryUserId = getPossiblyExpiredPrimaryUserId()
            val primaryUserIdCertification =
                possiblyExpiredPrimaryUserId?.let { getLatestUserIdCertification(it) }
            val userIdExpirationDate: Date? =
                primaryUserIdCertification?.let { getKeyExpirationTimeAsDate(it, publicKey) }

            if (latestDirectKeySelfSignature == null && primaryUserIdCertification == null) {
                /*
                throw NoSuchElementException(
                    "No direct-key signature and no user-id signature found.")
                 */
                return null
            }
            if (directKeyExpirationDate != null && userIdExpirationDate == null) {
                return directKeyExpirationDate
            }
            if (directKeyExpirationDate == null) {
                return userIdExpirationDate
            }
            return if (directKeyExpirationDate < userIdExpirationDate) directKeyExpirationDate
            else userIdExpirationDate
        }

    /** List of all subkeys that can be used to sign a message. */
    val signingSubkeys: List<PGPPublicKey> =
        validSubkeys.filter { getKeyFlagsOf(it.keyID).contains(KeyFlag.SIGN_DATA) }

    /** Whether the key is usable for encryption. */
    val isUsableForEncryption: Boolean = isUsableForEncryption(EncryptionPurpose.ANY)

    /**
     * Whether the key is capable of signing messages. This field is also true, if the key contains
     * a subkey that is capable of signing messages, but where the secret key is unavailable, e.g.
     * because it was moved to a smart-card.
     *
     * To check for keys that are actually usable to sign messages, use [isUsableForSigning].
     */
    val isSigningCapable: Boolean = isKeyValidlyBound(keyId) && signingSubkeys.isNotEmpty()

    /** Whether the key is actually usable to sign messages. */
    val isUsableForSigning: Boolean =
        isSigningCapable && signingSubkeys.any { isSecretKeyAvailable(it.keyID) }

    /** [HashAlgorithm] preferences of the primary user-ID or if absent, of the primary key. */
    val preferredHashAlgorithms: Set<HashAlgorithm>
        get() =
            primaryUserId?.let { getPreferredHashAlgorithms(it) }
                ?: getPreferredHashAlgorithms(keyId)

    /**
     * [SymmetricKeyAlgorithm] preferences of the primary user-ID or if absent of the primary key.
     */
    val preferredSymmetricKeyAlgorithms: Set<SymmetricKeyAlgorithm>
        get() =
            primaryUserId?.let { getPreferredSymmetricKeyAlgorithms(it) }
                ?: getPreferredSymmetricKeyAlgorithms(keyId)

    /** [CompressionAlgorithm] preferences of the primary user-ID or if absent, the primary key. */
    val preferredCompressionAlgorithms: Set<CompressionAlgorithm>
        get() =
            primaryUserId?.let { getPreferredCompressionAlgorithms(it) }
                ?: getPreferredCompressionAlgorithms(keyId)

    /**
     * Return the expiration date of the subkey with the provided fingerprint.
     *
     * @param fingerprint subkey fingerprint
     * @return expiration date or null
     */
    fun getSubkeyExpirationDate(fingerprint: OpenPgpFingerprint): Date? {
        return getSubkeyExpirationDate(fingerprint.keyId)
    }

    /**
     * Return the expiration date of the subkey with the provided keyId.
     *
     * @param keyId subkey keyId
     * @return expiration date
     */
    fun getSubkeyExpirationDate(keyId: Long): Date? {
        if (publicKey.keyID == keyId) return primaryKeyExpirationDate
        val subkey =
            getPublicKey(keyId)
                ?: throw NoSuchElementException(
                    "No subkey with key-ID ${keyId.openPgpKeyId()} found.")
        val bindingSig =
            getCurrentSubkeyBindingSignature(keyId)
                ?: throw AssertionError("Subkey has no valid binding signature.")
        return bindingSig.getKeyExpirationDate(subkey.creationTime)
    }

    /**
     * Return the date after which the key can no longer be used to perform the given use-case,
     * caused by expiration.
     *
     * @return expiration date for the given use-case
     */
    fun getExpirationDateForUse(use: KeyFlag): Date? {
        require(use != KeyFlag.SPLIT && use != KeyFlag.SHARED) {
            "SPLIT and SHARED are not uses, but properties."
        }

        val primaryKeyExpiration = primaryKeyExpirationDate
        val keysWithFlag: List<PGPPublicKey> = getKeysWithKeyFlag(use)
        if (keysWithFlag.isEmpty())
            throw NoSuchElementException("No key with the required key flag found.")

        var nonExpiring = false
        val latestSubkeyExpiration =
            keysWithFlag
                .map { key ->
                    getSubkeyExpirationDate(key.keyID).also { if (it == null) nonExpiring = true }
                }
                .filterNotNull()
                .maxByOrNull { it }

        if (nonExpiring) return primaryKeyExpiration
        return if (primaryKeyExpiration == null) latestSubkeyExpiration
        else if (latestSubkeyExpiration == null) primaryKeyExpiration
        else minOf(primaryKeyExpiration, latestSubkeyExpiration)
    }

    /**
     * Return true, if the given user-ID is hard-revoked.
     *
     * @return true, if the given user-ID is hard-revoked.
     */
    fun isHardRevoked(userId: CharSequence): Boolean {
        return signatures.userIdRevocations[userId]?.isHardRevocation ?: false
    }

    /**
     * Return a list of all keys which carry the provided key flag in their signature.
     *
     * @param flag flag
     * @return keys with flag
     */
    fun getKeysWithKeyFlag(flag: KeyFlag): List<PGPPublicKey> =
        publicKeys.filter { getKeyFlagsOf(it.keyID).contains(flag) }

    /**
     * Return a list of all subkeys which can be used to encrypt a message for the given user-ID.
     *
     * @return encryption subkeys
     */
    fun getEncryptionSubkeys(
        userId: CharSequence?,
        purpose: EncryptionPurpose
    ): List<PGPPublicKey> {
        if (userId != null && !isUserIdValid(userId)) {
            throw UnboundUserIdException(
                OpenPgpFingerprint.of(keys),
                userId.toString(),
                getLatestUserIdCertification(userId),
                getUserIdRevocation(userId))
        }
        return getEncryptionSubkeys(purpose)
    }

    /**
     * Return a list of all subkeys which can be used to encrypt a message, given the purpose.
     *
     * @return subkeys which can be used for encryption
     */
    fun getEncryptionSubkeys(purpose: EncryptionPurpose): List<PGPPublicKey> {
        primaryKeyExpirationDate?.let {
            if (it < referenceDate) {
                LOGGER.debug(
                    "Certificate is expired: Primary key is expired on ${DateUtil.formatUTCDate(it)}")
                return listOf()
            }
        }

        return keys.publicKeys
            .asSequence()
            .filter {
                if (!isKeyValidlyBound(it.keyID)) {
                    LOGGER.debug("(Sub?)-Key ${it.keyID.openPgpKeyId()} is not validly bound.")
                    return@filter false
                }

                getSubkeyExpirationDate(it.keyID)?.let { exp ->
                    if (exp < referenceDate) {
                        LOGGER.debug(
                            "(Sub?)-Key ${it.keyID.openPgpKeyId()} is expired on ${DateUtil.formatUTCDate(exp)}.")
                        return@filter false
                    }
                }

                if (!it.isEncryptionKey) {
                    LOGGER.debug(
                        "(Sub?)-Key ${it.keyID.openPgpKeyId()} algorithm is not capable of encryption.")
                    return@filter false
                }

                val keyFlags = getKeyFlagsOf(it.keyID)
                when (purpose) {
                    EncryptionPurpose.COMMUNICATIONS ->
                        return@filter keyFlags.contains(KeyFlag.ENCRYPT_COMMS)
                    EncryptionPurpose.STORAGE ->
                        return@filter keyFlags.contains(KeyFlag.ENCRYPT_STORAGE)
                    EncryptionPurpose.ANY ->
                        return@filter keyFlags.contains(KeyFlag.ENCRYPT_COMMS) ||
                            keyFlags.contains(KeyFlag.ENCRYPT_STORAGE)
                }
            }
            .toList()
    }

    /**
     * Return, whether the key is usable for encryption, given the purpose.
     *
     * @return true, if the key can be used to encrypt a message according to the
     *   encryption-purpose.
     */
    fun isUsableForEncryption(purpose: EncryptionPurpose): Boolean {
        return isKeyValidlyBound(keyId) && getEncryptionSubkeys(purpose).isNotEmpty()
    }

    /**
     * Return the primary user-ID, even if it is possibly expired.
     *
     * @return possibly expired primary user-ID
     */
    fun getPossiblyExpiredPrimaryUserId(): String? =
        primaryUserId
            ?: userIds
                .mapNotNull { userId -> getLatestUserIdCertification(userId)?.let { userId to it } }
                .sortedByDescending { it.second.creationTime }
                .maxByOrNull { it.second.hashedSubPackets.isPrimaryUserID }
                ?.first

    /** Return the most-recently created self-signature on the key. */
    private fun getMostRecentSignature(): PGPSignature? =
        setOfNotNull(latestDirectKeySelfSignature, revocationSelfSignature)
            .asSequence()
            .plus(signatures.userIdCertifications.values)
            .plus(signatures.userIdRevocations.values)
            .plus(signatures.subkeyBindings.values)
            .plus(signatures.subkeyRevocations.values)
            .maxByOrNull { creationDate }
    /**
     * Return the creation time of the latest added subkey.
     *
     * @return latest key creation time
     */
    fun getLatestKeyCreationDate(): Date =
        validSubkeys.maxByOrNull { creationDate }?.creationTime
            ?: throw AssertionError("Apparently there is no validly bound key in this key ring.")

    /**
     * Return the latest certification self-signature for the given user-ID.
     *
     * @return latest self-certification for the given user-ID.
     */
    fun getLatestUserIdCertification(userId: CharSequence): PGPSignature? =
        signatures.userIdCertifications[userId]

    /**
     * Return the latest revocation self-signature for the given user-ID
     *
     * @return latest user-ID revocation for the given user-ID
     */
    fun getUserIdRevocation(userId: CharSequence): PGPSignature? =
        signatures.userIdRevocations[userId]

    /**
     * Return the current binding signature for the subkey with the given key-ID.
     *
     * @return current subkey binding signature
     */
    fun getCurrentSubkeyBindingSignature(keyId: Long): PGPSignature? =
        signatures.subkeyBindings[keyId]

    /**
     * Return the current revocation signature for the subkey with the given key-ID.
     *
     * @return current subkey revocation signature
     */
    fun getSubkeyRevocationSignature(keyId: Long): PGPSignature? =
        signatures.subkeyRevocations[keyId]

    /**
     * Return a list of {@link KeyFlag KeyFlags} that apply to the subkey with the provided key id.
     *
     * @param keyId key-id
     * @return list of key flags
     */
    fun getKeyFlagsOf(keyId: Long): List<KeyFlag> =
        if (keyId == publicKey.keyID) {
            latestDirectKeySelfSignature?.let { sig ->
                SignatureSubpacketsUtil.parseKeyFlags(sig)?.let { flags ->
                    return flags
                }
            }

            primaryUserId?.let {
                SignatureSubpacketsUtil.parseKeyFlags(getLatestUserIdCertification(it))?.let { flags
                    ->
                    return flags
                }
            }
            listOf()
        } else {
            getCurrentSubkeyBindingSignature(keyId)?.let {
                SignatureSubpacketsUtil.parseKeyFlags(it)?.let { flags ->
                    return flags
                }
            }
            listOf()
        }

    /**
     * Return a list of {@link KeyFlag KeyFlags} that apply to the given user-id.
     *
     * @param userId user-id
     * @return key flags
     */
    fun getKeyFlagsOf(userId: CharSequence): List<KeyFlag> =
        if (!isUserIdValid(userId)) {
            listOf()
        } else {
            getLatestUserIdCertification(userId)?.let {
                SignatureSubpacketsUtil.parseKeyFlags(it) ?: listOf()
            }
                ?: throw AssertionError(
                    "While user-id '$userId' was reported as valid, there appears to be no certification for it.")
        }

    /**
     * Return the public key with the given key id from the provided key ring.
     *
     * @param keyId key id
     * @return public key or null
     */
    fun getPublicKey(keyId: Long): PGPPublicKey? = keys.getPublicKey(keyId)

    /**
     * Return the secret key with the given key id.
     *
     * @param keyId key id
     * @return secret key or null
     */
    fun getSecretKey(keyId: Long): PGPSecretKey? =
        when (keys) {
            is PGPSecretKeyRing -> keys.getSecretKey(keyId)
            else -> null
        }

    /**
     * Return true, if the secret-key with the given key-ID is available (i.e. not moved to a
     * smart-card).
     *
     * @return availability of the secret key
     */
    fun isSecretKeyAvailable(keyId: Long): Boolean {
        return getSecretKey(keyId)?.let {
            return if (it.s2K == null) true // Unencrypted key
            else it.s2K.type !in 100..110 // Secret key on smart-card
        }
            ?: false // Missing secret key
    }

    /**
     * Return the public key with the given fingerprint.
     *
     * @param fingerprint fingerprint
     * @return public key or null
     */
    fun getPublicKey(fingerprint: OpenPgpFingerprint): PGPPublicKey? =
        keys.getPublicKey(fingerprint.keyId)

    /**
     * Return the secret key with the given fingerprint.
     *
     * @param fingerprint fingerprint
     * @return secret key or null
     */
    fun getSecretKey(fingerprint: OpenPgpFingerprint): PGPSecretKey? =
        when (keys) {
            is PGPSecretKeyRing -> keys.getSecretKey(fingerprint.keyId)
            else -> null
        }

    /**
     * Return the public key matching the given [SubkeyIdentifier].
     *
     * @return public key
     * @throws IllegalArgumentException if the identifier's primary key does not match the primary
     *   key of the key.
     */
    fun getPublicKey(identifier: SubkeyIdentifier): PGPPublicKey? {
        require(identifier.primaryKeyId == publicKey.keyID) { "Mismatching primary key ID." }
        return getPublicKey(identifier.subkeyId)
    }

    /**
     * Return the secret key matching the given [SubkeyIdentifier].
     *
     * @return secret key
     * @throws IllegalArgumentException if the identifier's primary key does not match the primary
     *   key of the key.
     */
    fun getSecretKey(identifier: SubkeyIdentifier): PGPSecretKey? =
        when (keys) {
            is PGPSecretKeyRing -> {
                require(identifier.primaryKeyId == publicKey.keyID) {
                    "Mismatching primary key ID."
                }
                keys.getSecretKey(identifier.subkeyId)
            }
            else -> null
        }

    /**
     * Return true if the public key with the given key id is bound to the key ring properly.
     *
     * @param keyId key id
     * @return true if key is bound validly
     */
    fun isKeyValidlyBound(keyId: Long): Boolean {
        val publicKey = keys.getPublicKey(keyId) ?: return false

        // Primary key -> Check Primary Key Revocation
        if (publicKey.keyID == this.publicKey.keyID) {
            return if (signatures.primaryKeyRevocation != null &&
                signatures.primaryKeyRevocation.isHardRevocation) {
                false
            } else signatures.primaryKeyRevocation == null
        }

        // Else Subkey -> Check Subkey Revocation
        val binding = signatures.subkeyBindings[keyId]
        val revocation = signatures.subkeyRevocations[keyId]

        // No valid binding
        if (binding == null || binding.isExpired(referenceDate)) {
            return false
        }

        // Revocation
        return if (revocation != null) {
            if (revocation.isHardRevocation) {
                // Subkey is hard revoked
                false
            } else {
                // Key is soft-revoked, not yet re-bound
                (revocation.isExpired(referenceDate) ||
                    !revocation.creationTime.after(binding.creationTime))
            }
        } else true
    }

    /**
     * Return the current primary user-id of the key ring.
     *
     * <p>
     * Note: If no user-id is marked as primary key using a {@link PrimaryUserID} packet, this
     * method returns the first user-id on the key, otherwise null.
     *
     * @return primary user-id or null
     */
    private fun findPrimaryUserId(): String? {
        if (userIds.isEmpty()) {
            return null
        }

        return signatures.userIdCertifications
            .filter { (_, certification) -> certification.hashedSubPackets.isPrimaryUserID }
            .entries
            .maxByOrNull { (_, certification) -> certification.creationTime }
            ?.key
            ?: signatures.userIdCertifications.keys.firstOrNull()
    }

    /** Return true, if the primary user-ID, as well as the given user-ID are valid and bound. */
    fun isUserIdValid(userId: CharSequence) =
        if (primaryUserId == null) {
            false
        } else {
            isUserIdBound(primaryUserId) &&
                (if (userId == primaryUserId) true else isUserIdBound(userId))
        }

    /** Return true, if the given user-ID is validly bound. */
    fun isUserIdBound(userId: CharSequence) =
        signatures.userIdCertifications[userId]?.let { sig ->
            if (sig.isExpired(referenceDate)) {
                // certification expired
                return false
            }
            if (sig.hashedSubPackets.isPrimaryUserID) {
                getKeyExpirationTimeAsDate(sig, publicKey)?.let { expirationDate ->
                    // key expired?
                    if (expirationDate < referenceDate) return false
                }
            }
            signatures.userIdRevocations[userId]?.let { rev ->
                if (rev.isHardRevocation) {
                    return false // hard revoked -> invalid
                }
                sig.creationTime > rev.creationTime // re-certification after soft revocation?
            }
                ?: true // certification, but no revocation
        }
            ?: false // no certification

    /** [HashAlgorithm] preferences of the given user-ID. */
    fun getPreferredHashAlgorithms(userId: CharSequence): Set<HashAlgorithm> {
        return getKeyAccessor(userId, keyId).preferredHashAlgorithms
    }

    /** [HashAlgorithm] preferences of the given key. */
    fun getPreferredHashAlgorithms(keyId: Long): Set<HashAlgorithm> {
        return KeyAccessor.SubKey(this, SubkeyIdentifier(keys, keyId)).preferredHashAlgorithms
    }

    /** [SymmetricKeyAlgorithm] preferences of the given user-ID. */
    fun getPreferredSymmetricKeyAlgorithms(userId: CharSequence): Set<SymmetricKeyAlgorithm> {
        return getKeyAccessor(userId, keyId).preferredSymmetricKeyAlgorithms
    }

    /** [SymmetricKeyAlgorithm] preferences of the given key. */
    fun getPreferredSymmetricKeyAlgorithms(keyId: Long): Set<SymmetricKeyAlgorithm> {
        return KeyAccessor.SubKey(this, SubkeyIdentifier(keys, keyId))
            .preferredSymmetricKeyAlgorithms
    }

    /** [CompressionAlgorithm] preferences of the given user-ID. */
    fun getPreferredCompressionAlgorithms(userId: CharSequence): Set<CompressionAlgorithm> {
        return getKeyAccessor(userId, keyId).preferredCompressionAlgorithms
    }

    /** [CompressionAlgorithm] preferences of the given key. */
    fun getPreferredCompressionAlgorithms(keyId: Long): Set<CompressionAlgorithm> {
        return KeyAccessor.SubKey(this, SubkeyIdentifier(keys, keyId))
            .preferredCompressionAlgorithms
    }

    val isUsableForThirdPartyCertification: Boolean =
        isKeyValidlyBound(keyId) && getKeyFlagsOf(keyId).contains(KeyFlag.CERTIFY_OTHER)

    private fun getKeyAccessor(userId: CharSequence?, keyId: Long): KeyAccessor {
        if (getPublicKey(keyId) == null) {
            throw NoSuchElementException(
                "No subkey with key-id ${keyId.openPgpKeyId()} found on this key.")
        }
        if (userId != null && !userIds.contains(userId)) {
            throw NoSuchElementException("No user-id '$userId' found on this key.")
        }
        return if (userId != null) {
            KeyAccessor.ViaUserId(this, SubkeyIdentifier(keys, keyId), userId)
        } else {
            KeyAccessor.ViaKeyId(this, SubkeyIdentifier(keys, keyId))
        }
    }

    companion object {

        /** Evaluate the key for the given signature. */
        @JvmStatic
        fun evaluateForSignature(keys: PGPKeyRing, signature: PGPSignature) =
            KeyRingInfo(keys, signature.creationTime!!)

        private val PATTERN_EMAIL_FROM_USERID =
            "<([a-zA-Z0-9_!#$%&'*+/=?`{|}~^.-]+@[a-zA-Z0-9.-]+)>".toPattern()
        private val PATTERN_EMAIL_EXPLICIT =
            "^([a-zA-Z0-9_!#$%&'*+/=?`{|}~^.-]+@[a-zA-Z0-9.-]+)$".toPattern()

        @JvmStatic private val LOGGER = LoggerFactory.getLogger(KeyRingInfo::class.java)
    }

    private class Signatures(val keys: PGPKeyRing, val referenceDate: Date, val policy: Policy) {
        val primaryKeyRevocation: PGPSignature? =
            SignaturePicker.pickCurrentRevocationSelfSignature(keys, policy, referenceDate)
        val primaryKeySelfSignature: PGPSignature? =
            SignaturePicker.pickLatestDirectKeySignature(keys, policy, referenceDate)
        val userIdRevocations = mutableMapOf<String, PGPSignature>()
        val userIdCertifications = mutableMapOf<String, PGPSignature>()
        val subkeyRevocations = mutableMapOf<Long, PGPSignature>()
        val subkeyBindings = mutableMapOf<Long, PGPSignature>()

        init {
            KeyRingUtils.getUserIdsIgnoringInvalidUTF8(keys.publicKey).forEach { userId ->
                SignaturePicker.pickCurrentUserIdRevocationSignature(
                        keys, userId, policy, referenceDate)
                    ?.let { userIdRevocations[userId] = it }
                SignaturePicker.pickLatestUserIdCertificationSignature(
                        keys, userId, policy, referenceDate)
                    ?.let { userIdCertifications[userId] = it }
            }
            keys.publicKeys.asSequence().drop(1).forEach { subkey ->
                SignaturePicker.pickCurrentSubkeyBindingRevocationSignature(
                        keys, subkey, policy, referenceDate)
                    ?.let { subkeyRevocations[subkey.keyID] = it }
                SignaturePicker.pickLatestSubkeyBindingSignature(
                        keys, subkey, policy, referenceDate)
                    ?.let { subkeyBindings[subkey.keyID] = it }
            }
        }
    }
}
