"""Certbot client crypto utility functions.

.. todo:: Make the transition to use PSS rather than PKCS1_v1_5 when the server
    is capable of handling the signatures.

"""
import hashlib
import logging
import re
from typing import List
from typing import Set
import warnings

from cryptography import x509
from cryptography.exceptions import InvalidSignature
from cryptography.exceptions import UnsupportedAlgorithm
from cryptography.hazmat.backends import default_backend
from cryptography.hazmat.primitives.asymmetric import ec
from cryptography.hazmat.primitives.asymmetric.ec import ECDSA
from cryptography.hazmat.primitives.asymmetric.ec import EllipticCurvePublicKey
from cryptography.hazmat.primitives.asymmetric.padding import PKCS1v15
from cryptography.hazmat.primitives.asymmetric.rsa import RSAPublicKey
from cryptography.hazmat.primitives.serialization import Encoding
from cryptography.hazmat.primitives.serialization import NoEncryption
from cryptography.hazmat.primitives.serialization import PrivateFormat
from OpenSSL import crypto
from OpenSSL import SSL
import pyrfc3339
import zope.component

from acme import crypto_util as acme_crypto_util
from certbot import errors
from certbot import interfaces
from certbot import util
from certbot.compat import os

logger = logging.getLogger(__name__)


# High level functions

def generate_key(key_size: int, key_dir: str, key_type: str = "rsa",
                 elliptic_curve: str = "secp256r1", keyname: str = "key-certbot.pem",
                 strict_permissions: bool = True) -> util.Key:
    """Initializes and saves a privkey.

    Inits key and saves it in PEM format on the filesystem.

    .. note:: keyname is the attempted filename, it may be different if a file
        already exists at the path.

    :param int key_size: key size in bits if key size is rsa.
    :param str key_dir: Key save directory.
    :param str key_type: Key Type [rsa, ecdsa]
    :param str elliptic_curve: Name of the elliptic curve if key type is ecdsa.
    :param str keyname: Filename of key
    :param bool strict_permissions: If true and key_dir exists, an exception is raised if
        the directory doesn't have 0700 permissions or isn't owned by the current user.

    :returns: Key
    :rtype: :class:`certbot.util.Key`

    :raises ValueError: If unable to generate the key given key_size.

    """
    try:
        key_pem = make_key(
            bits=key_size, elliptic_curve=elliptic_curve or "secp256r1", key_type=key_type,
        )
    except ValueError as err:
        logger.debug("", exc_info=True)
        logger.error("Encountered error while making key: %s", str(err))
        raise err

    # Save file
    util.make_or_verify_dir(key_dir, 0o700, strict_permissions)
    key_f, key_path = util.unique_file(
        os.path.join(key_dir, keyname), 0o600, "wb")
    with key_f:
        key_f.write(key_pem)
    if key_type == 'rsa':
        logger.debug("Generating RSA key (%d bits): %s", key_size, key_path)
    else:
        logger.debug("Generating ECDSA key (%d bits): %s", key_size, key_path)

    return util.Key(key_path, key_pem)


# TODO: Remove this call once zope dependencies are removed from Certbot.
def init_save_key(key_size, key_dir, key_type="rsa", elliptic_curve="secp256r1",
                  keyname="key-certbot.pem"):
    """Initializes and saves a privkey.

    Inits key and saves it in PEM format on the filesystem.

    .. note:: keyname is the attempted filename, it may be different if a file
        already exists at the path.

    .. deprecated:: 1.16.0
       Use :func:`generate_key` instead.

    :param int key_size: key size in bits if key size is rsa.
    :param str key_dir: Key save directory.
    :param str key_type: Key Type [rsa, ecdsa]
    :param str elliptic_curve: Name of the elliptic curve if key type is ecdsa.
    :param str keyname: Filename of key

    :returns: Key
    :rtype: :class:`certbot.util.Key`

    :raises ValueError: If unable to generate the key given key_size.

    """
    warnings.warn("certbot.crypto_util.init_save_key is deprecated, please use "
                  "certbot.crypto_util.generate_key instead.", DeprecationWarning)

    config = zope.component.getUtility(interfaces.IConfig)

    return generate_key(key_size, key_dir, key_type=key_type, elliptic_curve=elliptic_curve,
                        keyname=keyname, strict_permissions=config.strict_permissions)


def generate_csr(privkey: util.Key, names: Set[str], path: str,
                 must_staple: bool = False, strict_permissions: bool = True) -> util.CSR:
    """Initialize a CSR with the given private key.

    :param privkey: Key to include in the CSR
    :type privkey: :class:`certbot.util.Key`
    :param set names: `str` names to include in the CSR
    :param str path: Certificate save directory.
    :param bool must_staple: If true, include the TLS Feature extension "OCSP Must Staple"
    :param bool strict_permissions: If true and path exists, an exception is raised if
        the directory doesn't have 0755 permissions or isn't owned by the current user.

    :returns: CSR
    :rtype: :class:`certbot.util.CSR`

    """
    csr_pem = acme_crypto_util.make_csr(
        privkey.pem, names, must_staple=must_staple)

    # Save CSR
    util.make_or_verify_dir(path, 0o755, strict_permissions)
    csr_f, csr_filename = util.unique_file(
        os.path.join(path, "csr-certbot.pem"), 0o644, "wb")
    with csr_f:
        csr_f.write(csr_pem)
    logger.debug("Creating CSR: %s", csr_filename)

    return util.CSR(csr_filename, csr_pem, "pem")


# TODO: Remove this call once zope dependencies are removed from Certbot.
def init_save_csr(privkey, names, path):
    """Initialize a CSR with the given private key.

    .. deprecated:: 1.16.0
       Use :func:`generate_csr` instead.

    :param privkey: Key to include in the CSR
    :type privkey: :class:`certbot.util.Key`

    :param set names: `str` names to include in the CSR

    :param str path: Certificate save directory.

    :returns: CSR
    :rtype: :class:`certbot.util.CSR`

    """
    warnings.warn("certbot.crypto_util.init_save_csr is deprecated, please use "
                  "certbot.crypto_util.generate_csr instead.", DeprecationWarning)

    config = zope.component.getUtility(interfaces.IConfig)

    return generate_csr(privkey, names, path, must_staple=config.must_staple,
                        strict_permissions=config.strict_permissions)


# WARNING: the csr and private key file are possible attack vectors for TOCTOU
# We should either...
# A. Do more checks to verify that the CSR is trusted/valid
# B. Audit the parsing code for vulnerabilities

def valid_csr(csr):
    """Validate CSR.

    Check if `csr` is a valid CSR for the given domains.

    :param str csr: CSR in PEM.

    :returns: Validity of CSR.
    :rtype: bool

    """
    try:
        req = crypto.load_certificate_request(
            crypto.FILETYPE_PEM, csr)
        return req.verify(req.get_pubkey())