import warnings
import typing as t
from functools import cached_property
from cryptography.hazmat.primitives.asymmetric.rsa import (
    generate_private_key,
    RSAPublicKey,
    RSAPrivateKey,
    RSAPrivateNumbers,
    RSAPublicNumbers,
    rsa_recover_prime_factors,
    rsa_crt_dmp1,
    rsa_crt_dmq1,
    rsa_crt_iqmp,
)
from ..registry import KeyParameter
from ..errors import SecurityWarning, KeyParameterError
from .._rfc7517.models import AsymmetricKey
from .._rfc7517.pem import CryptographyBinding
from .._rfc7517.types import KeyParameters, AnyKey
from ..util import int_to_base64, base64_to_int


RSADictKey = t.TypedDict(
    "RSADictKey",
    {
        "n": str,
        "e": str,
        "d": str,
        "p": str,
        "q": str,
        "dp": str,
        "dq": str,
        "qi": str,
    },
    total=False,
)


class RSABinding(CryptographyBinding):
    key_type = "RSA"
    ssh_type = b"ssh-rsa"
    _cryptography_key_types = (RSAPrivateKey, RSAPublicKey)

    @staticmethod
    def generate_private_key(size: int) -> RSAPrivateKey:
        return generate_private_key(public_exponent=65537, key_size=size)

    @classmethod
    def import_private_key(cls, obj: RSADictKey) -> RSAPrivateKey:
        if "oth" in obj:  # pragma: no cover
            # https://tools.ietf.org/html/rfc7518#section-6.3.2.7
            raise ValueError('"oth" is not supported yet')

        public_numbers = RSAPublicNumbers(base64_to_int(obj["e"]), base64_to_int(obj["n"]))

        if has_all_prime_factors(obj):
            numbers = RSAPrivateNumbers(
                d=base64_to_int(obj["d"]),
                p=base64_to_int(obj["p"]),
                q=base64_to_int(obj["q"]),
                dmp1=base64_to_int(obj["dp"]),
                dmq1=base64_to_int(obj["dq"]),
                iqmp=base64_to_int(obj["qi"]),
                public_numbers=public_numbers,
            )
        else:
            d = base64_to_int(obj["d"])
            p, q = rsa_recover_prime_factors(public_numbers.n, d, public_numbers.e)
            numbers = RSAPrivateNumbers(
                d=d,
                p=p,
                q=q,
                dmp1=rsa_crt_dmp1(d, p),
                dmq1=rsa_crt_dmq1(d, q),
                iqmp=rsa_crt_iqmp(p, q),
                public_numbers=public_numbers,
            )

        return numbers.private_key()

    @classmethod
    def export_private_key(cls, key: RSAPrivateKey) -> RSADictKey:
        numbers = key.private_numbers()
        return {
            "n": int_to_base64(numbers.public_numbers.n),
            "e": int_to_base64(numbers.public_numbers.e),
            "d": int_to_base64(numbers.d),
            "p": int_to_base64(numbers.p),
            "q": int_to_base64(numbers.q),
            "dp": int_to_base64(numbers.dmp1),
            "dq": int_to_base64(numbers.dmq1),
            "qi": int_to_base64(numbers.iqmp),
        }

    @classmethod
    def import_public_key(cls, obj: RSADictKey) -> RSAPublicKey:
        numbers = RSAPublicNumbers(base64_to_int(obj["e"]), base64_to_int(obj["n"]))
        return numbers.public_key()

    @classmethod
    def export_public_key(cls, key: RSAPublicKey) -> dict[str, str]:
        numbers = key.public_numbers()
        return {"n": int_to_base64(numbers.n), "e": int_to_base64(numbers.e)}


class RSAKey(AsymmetricKey[RSAPrivateKey, RSAPublicKey]):
    key_type = "RSA"
    #: Registry definition for RSA Key
    #: https://www.rfc-editor.org/rfc/rfc7518#section-6.3
    value_registry = {
        "n": KeyParameter("Modulus", "str", private=False, required=True),
        "e": KeyParameter("Exponent", "str", private=False, required=True),
        "d": KeyParameter("Private Exponent", "str", private=True, required=False),
        "p": KeyParameter("First Prime Factor", "str", private=True, required=False),
        "q": KeyParameter("Second Prime Factor", "str", private=True, required=False),
        "dp": KeyParameter("First Factor CRT Exponent", "str", private=True, required=False),
        "dq": KeyParameter("Second Factor CRT Exponent", "str", private=True, required=False),
        "qi": KeyParameter("First CRT Coefficient", "str", private=True, required=False),
        "oth": KeyParameter("Other Primes Info", "none", private=True, required=False),
    }
    binding = RSABinding

    @property
    def is_private(self) -> bool:
        return isinstance(self.raw_value, RSAPrivateKey)

    @cached_property
    def public_key(self) -> RSAPublicKey:
        if isinstance(self.raw_value, RSAPrivateKey):
            return self.raw_value.public_key()
        return self.raw_value

    @property
    def private_key(self) -> RSAPrivateKey | None:
        if isinstance(self.raw_value, RSAPrivateKey):
            return self.raw_value
        return None

    @classmethod
    def import_key(
        cls: t.Any,
        value: AnyKey | RSAPrivateKey | RSAPublicKey,
        parameters: KeyParameters | None = None,
        password: t.Any = None,
    ) -> "RSAKey":
        key: RSAKey
        if isinstance(value, (RSAPrivateKey, RSAPublicKey)):
            key = cls(value, value, parameters)
        else:
            key = super(RSAKey, cls).import_key(value, parameters, password)
        if key.raw_value.key_size < 2048:
            # https://csrc.nist.gov/publications/detail/sp/800-131a/rev-2/final
            warnings.warn("Key size should be >= 2048 bits", SecurityWarning)
        return key

    @classmethod
    def generate_key(
        cls: type["RSAKey"],
        key_size: int | None = 2048,
        parameters: KeyParameters | None = None,
        private: bool = True,
        auto_kid: bool = False,
    ) -> "RSAKey":
        """Generate a ``RSAKey`` with the given bit size (not bytes).

        :param key_size: size in bit
        :param parameters: extra parameter in JWK
        :param private: generate a private key or public key
        :param auto_kid: add ``kid`` automatically
        """
        if key_size is None:
            key_size = 2048

        if key_size % 8 != 0:
            raise ValueError("A bit size must be a multiple of 8")

        if key_size < 2048:
            # https://csrc.nist.gov/publications/detail/sp/800-131a/rev-2/final
            warnings.warn("Key size should be >= 2048 bits", SecurityWarning)

        raw_key = cls.binding.generate_private_key(key_size)
        if private:
            key = cls(raw_key, raw_key, parameters)
        else:
            pub_key = raw_key.public_key()
            key = cls(pub_key, pub_key, parameters)
        if auto_kid:
            key.ensure_kid()
        return key


def has_all_prime_factors(obj: RSADictKey) -> bool:
    props = ["p", "q", "dp", "dq", "qi"]
    props_found = [prop in obj for prop in props]
    if all(props_found):
        return True

    if any(props_found):
        raise KeyParameterError("RSA key must include all parameters if any are present besides d")

    return False
