# -*- coding: utf-8 -*-

# Copyright: (c) 2016-2017, Yanis Guenane <yanis+ansible@guenane.org>
# Copyright: (c) 2017, Markus Teufelberger <mteufelberger+ansible@mgit.at>
# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt)

from __future__ import absolute_import, division, print_function
__metaclass__ = type


import os

from distutils.version import LooseVersion
from random import randrange

from ansible.module_utils._text import to_bytes

from ansible_collections.community.crypto.plugins.module_utils.crypto.basic import (
    OpenSSLBadPassphraseError,
)

from ansible_collections.community.crypto.plugins.module_utils.crypto.support import (
    load_privatekey,
    load_certificate,
    get_relative_time_option,
    select_message_digest,
)

from ansible_collections.community.crypto.plugins.module_utils.crypto.cryptography_support import (
    cryptography_key_needs_digest_for_signing,
    cryptography_serial_number_of_cert,
)

from ansible_collections.community.crypto.plugins.module_utils.crypto.module_backends.certificate import (
    CRYPTOGRAPHY_VERSION,
    CertificateError,
    CertificateBackend,
    CertificateProvider,
)

try:
    from OpenSSL import crypto
except ImportError:
    pass

try:
    import cryptography
    from cryptography import x509
    from cryptography.hazmat.backends import default_backend
    from cryptography.hazmat.primitives.serialization import Encoding
except ImportError:
    pass


class OwnCACertificateBackendCryptography(CertificateBackend):
    def __init__(self, module):
        super(OwnCACertificateBackendCryptography, self).__init__(module, 'cryptography')

        self.create_subject_key_identifier = module.params['ownca_create_subject_key_identifier']
        self.create_authority_key_identifier = module.params['ownca_create_authority_key_identifier']
        self.notBefore = get_relative_time_option(module.params['ownca_not_before'], 'ownca_not_before', backend=self.backend)
        self.notAfter = get_relative_time_option(module.params['ownca_not_after'], 'ownca_not_after', backend=self.backend)
        self.digest = select_message_digest(module.params['ownca_digest'])
        self.version = module.params['ownca_version']
        self.serial_number = x509.random_serial_number()
        self.ca_cert_path = module.params['ownca_path']
        self.ca_cert_content = module.params['ownca_content']
        if self.ca_cert_content is not None:
            self.ca_cert_content = self.ca_cert_content.encode('utf-8')
        self.ca_privatekey_path = module.params['ownca_privatekey_path']
        self.ca_privatekey_content = module.params['ownca_privatekey_content']
        if self.ca_privatekey_content is not None:
            self.ca_privatekey_content = self.ca_privatekey_content.encode('utf-8')
        self.ca_privatekey_passphrase = module.params['ownca_privatekey_passphrase']

        if self.csr_content is None and self.csr_path is None:
            raise CertificateError(
                'csr_path or csr_content is required for ownca provider'
            )
        if self.csr_content is None and not os.path.exists(self.csr_path):
            raise CertificateError(
                'The certificate signing request file {0} does not exist'.format(self.csr_path)
            )
        if self.ca_cert_content is None and not os.path.exists(self.ca_cert_path):
            raise CertificateError(
                'The CA certificate file {0} does not exist'.format(self.ca_cert_path)
            )
        if self.ca_privatekey_content is None and not os.path.exists(self.ca_privatekey_path):
            raise CertificateError(
                'The CA private key file {0} does not exist'.format(self.ca_privatekey_path)
            )

        self._ensure_csr_loaded()
        self.ca_cert = load_certificate(
            path=self.ca_cert_path,
            content=self.ca_cert_content,
            backend=self.backend
        )
        try:
            self.ca_private_key = load_privatekey(
                path=self.ca_privatekey_path,
                content=self.ca_privatekey_content,
                passphrase=self.ca_privatekey_passphrase,
                backend=self.backend
            )
        except OpenSSLBadPassphraseError as exc:
            module.fail_json(msg=str(exc))

        if cryptography_key_needs_digest_for_signing(self.ca_private_key):
            if self.digest is None:
                raise CertificateError(
                    'The digest %s is not supported with the cryptography backend' % module.params['ownca_digest']
                )
        else:
            self.digest = None

    def generate_certificate(self):
        """(Re-)Generate certificate."""
        cert_builder = x509.CertificateBuilder()
        cert_builder = cert_builder.subject_name(self.csr.subject)
        cert_builder = cert_builder.issuer_name(self.ca_cert.subject)
        cert_builder = cert_builder.serial_number(self.serial_number)
        cert_builder = cert_builder.not_valid_before(self.notBefore)
        cert_builder = cert_builder.not_valid_after(self.notAfter)
        cert_builder = cert_builder.public_key(self.csr.public_key())
        has_ski = False
        for extension in self.csr.extensions:
            if isinstance(extension.value, x509.SubjectKeyIdentifier):
                if self.create_subject_key_identifier == 'always_create':
                    continue
                has_ski = True
            if self.create_authority_key_identifier and isinstance(extension.value, x509.AuthorityKeyIdentifier):
                continue
            cert_builder = cert_builder.add_extension(extension.value, critical=extension.critical)
        if not has_ski and self.create_subject_key_identifier != 'never_create':
            cert_builder = cert_builder.add_extension(
                x509.SubjectKeyIdentifier.from_public_key(self.csr.public_key()),
                critical=False
            )
        if self.create_authority_key_identifier:
            try:
                ext = self.ca_cert.extensions.get_extension_for_class(x509.SubjectKeyIdentifier)
                cert_builder = cert_builder.add_extension(
                    x509.AuthorityKeyIdentifier.from_issuer_subject_key_identifier(ext.value)
                    if CRYPTOGRAPHY_VERSION >= LooseVersion('2.7') else
                    x509.AuthorityKeyIdentifier.from_issuer_subject_key_identifier(ext),
                    critical=False
                )
            except cryptography.x509.ExtensionNotFound:
                cert_builder = cert_builder.add_extension(
                    x509.AuthorityKeyIdentifier.from_issuer_public_key(self.ca_cert.public_key()),
                    critical=False
                )

        try:
            certificate = cert_builder.sign(
                private_key=self.ca_private_key, algorithm=self.digest,
                backend=default_backend()
            )
        except TypeError as e:
            if str(e) == 'Algorithm must be a registered hash algorithm.' and self.digest is None:
                self.module.fail_json(msg='Signing with Ed25519 and Ed448 keys requires cryptography 2.8 or newer.')
            raise

        self.cert = certificate

    def get_certificate_data(self):
        """Return bytes for self.cert."""
        return self.cert.public_bytes(Encoding.PEM)

    def needs_regeneration(self):
        if super(OwnCACertificateBackendCryptography, self).needs_regeneration():
            return True

        # Check AuthorityKeyIdentifier
        if self.create_authority_key_identifier:
            try:
                ext = self.ca_cert.extensions.get_extension_for_class(x509.SubjectKeyIdentifier)
                expected_ext = (
                    x509.AuthorityKeyIdentifier.from_issuer_subject_key_identifier(ext.value)
                    if CRYPTOGRAPHY_VERSION >= LooseVersion('2.7') else
                    x509.AuthorityKeyIdentifier.from_issuer_subject_key_identifier(ext)
                )
            except cryptography.x509.ExtensionNotFound:
                expected_ext = x509.AuthorityKeyIdentifier.from_issuer_public_key(self.ca_cert.public_key())

            self._ensure_existing_certificate_loaded()
            try:
                ext = self.existing_certificate.extensions.get_extension_for_class(x509.AuthorityKeyIdentifier)
                if ext.value != expected_ext:
                    return True
            except cryptography.x509.ExtensionNotFound as dummy:
                return True

        return False

    def dump(self, include_certificate):
        result = super(OwnCACertificateBackendCryptography, self).dump(include_certificate)
        result.update({
            'ca_cert': self.ca_cert_path,
            'ca_privatekey': self.ca_privatekey_path,
        })

        if self.module.check_mode:
            result.update({
                'notBefore': self.notBefore.strftime("%Y%m%d%H%M%SZ"),
                'notAfter': self.notAfter.strftime("%Y%m%d%H%M%SZ"),
                'serial_number': self.serial_number,
            })
        else:
            if self.cert is None:
                self.cert = self.existing_certificate
            result.update({
                'notBefore': self.cert.not_valid_before.strftime("%Y%m%d%H%M%SZ"),
                'notAfter': self.cert.not_valid_after.strftime("%Y%m%d%H%M%SZ"),
                'serial_number': cryptography_serial_number_of_cert(self.cert),
            })

        return result


def generate_serial_number():
    """Generate a serial number for a certificate"""
    while True:
        result = randrange(0, 1 << 160)
        if result >= 1000:
            return result


class OwnCACertificateBackendPyOpenSSL(CertificateBackend):
    def __init__(self, module):
        super(OwnCACertificateBackendPyOpenSSL, self).__init__(module, 'pyopenssl')

        self.notBefore = get_relative_time_option(self.module.params['ownca_not_before'], 'ownca_not_before', backend=self.backend)
        self.notAfter = get_relative_time_option(self.module.params['ownca_not_after'], 'ownca_not_after', backend=self.backend)
        self.digest = self.module.params['ownca_digest']
        self.version = self.module.params['ownca_version']
        self.serial_number = generate_serial_number()
        if self.module.params['ownca_create_subject_key_identifier'] != 'create_if_not_provided':
            self.module.fail_json(msg='ownca_create_subject_key_identifier cannot be used with the pyOpenSSL backend!')
        if self.module.params['ownca_create_authority_key_identifier']:
            self.module.warn('ownca_create_authority_key_identifier is ignored by the pyOpenSSL backend!')
        self.ca_cert_path = self.module.params['ownca_path']
        self.ca_cert_content = self.module.params['ownca_content']
        if self.ca_cert_content is not None:
            self.ca_cert_content = self.ca_cert_content.encode('utf-8')
        self.ca_privatekey_path = self.module.params['ownca_privatekey_path']
        self.ca_privatekey_content = self.module.params['ownca_privatekey_content']
        if self.ca_privatekey_content is not None:
            self.ca_privatekey_content = self.ca_privatekey_content.encode('utf-8')
        self.ca_privatekey_passphrase = self.module.params['ownca_privatekey_passphrase']

        if self.csr_content is None and not os.path.exists(self.csr_path):
            raise CertificateError(
                'The certificate signing request file {0} does not exist'.format(self.csr_path)
            )
        if self.ca_cert_content is None and not os.path.exists(self.ca_cert_path):
            raise CertificateError(
                'The CA certificate file {0} does not exist'.format(self.ca_cert_path)
            )
        if self.ca_privatekey_content is None and not os.path.exists(self.ca_privatekey_path):
            raise CertificateError(
                'The CA private key file {0} does not exist'.format(self.ca_privatekey_path)
            )

        self._ensure_csr_loaded()
        self.ca_cert = load_certificate(
            path=self.ca_cert_path,
            content=self.ca_cert_content,
        )
        try:
            self.ca_privatekey = load_privatekey(
                path=self.ca_privatekey_path,
                content=self.ca_privatekey_content,
                passphrase=self.ca_privatekey_passphrase
            )
        except OpenSSLBadPassphraseError as exc:
            self.module.fail_json(msg=str(exc))

    def generate_certificate(self):
        """(Re-)Generate certificate."""
        cert = crypto.X509()
        cert.set_serial_number(self.serial_number)
        cert.set_notBefore(to_bytes(self.notBefore))
        cert.set_notAfter(to_bytes(self.notAfter))
        cert.set_subject(self.csr.get_subject())
        cert.set_issuer(self.ca_cert.get_subject())
        cert.set_version(self.version - 1)
        cert.set_pubkey(self.csr.get_pubkey())
        cert.add_extensions(self.csr.get_extensions())

        cert.sign(self.ca_privatekey, self.digest)
        self.cert = cert

    def get_certificate_data(self):
        """Return bytes for self.cert."""
        return crypto.dump_certificate(crypto.FILETYPE_PEM, self.cert)

    def dump(self, include_certificate):
        result = super(OwnCACertificateBackendPyOpenSSL, self).dump(include_certificate)
        result.update({
            'ca_cert': self.ca_cert_path,
            'ca_privatekey': self.ca_privatekey_path,
        })

        if self.module.check_mode:
            result.update({
                'notBefore': self.notBefore,
                'notAfter': self.notAfter,
                'serial_number': self.serial_number,
            })
        else:
            if self.cert is None:
                self.cert = self.existing_certificate
            result.update({
                'notBefore': self.cert.get_notBefore(),
                'notAfter': self.cert.get_notAfter(),
                'serial_number': self.cert.get_serial_number(),
            })

        return result


class OwnCACertificateProvider(CertificateProvider):
    def validate_module_args(self, module):
        if module.params['ownca_path'] is None and module.params['ownca_content'] is None:
            module.fail_json(msg='One of ownca_path and ownca_content must be specified for the ownca provider.')
        if module.params['ownca_privatekey_path'] is None and module.params['ownca_privatekey_content'] is None:
            module.fail_json(msg='One of ownca_privatekey_path and ownca_privatekey_content must be specified for the ownca provider.')

    def needs_version_two_certs(self, module):
        return module.params['ownca_version'] == 2

    def create_backend(self, module, backend):
        if backend == 'cryptography':
            return OwnCACertificateBackendCryptography(module)
        if backend == 'pyopenssl':
            return OwnCACertificateBackendPyOpenSSL(module)


def add_ownca_provider_to_argument_spec(argument_spec):
    argument_spec.argument_spec['provider']['choices'].append('ownca')
    argument_spec.argument_spec.update(dict(
        ownca_path=dict(type='path'),
        ownca_content=dict(type='str'),
        ownca_privatekey_path=dict(type='path'),
        ownca_privatekey_content=dict(type='str', no_log=True),
        ownca_privatekey_passphrase=dict(type='str', no_log=True),
        ownca_digest=dict(type='str', default='sha256'),
        ownca_version=dict(type='int', default=3),
        ownca_not_before=dict(type='str', default='+0s'),
        ownca_not_after=dict(type='str', default='+3650d'),
        ownca_create_subject_key_identifier=dict(
            type='str',
            default='create_if_not_provided',
            choices=['create_if_not_provided', 'always_create', 'never_create']
        ),
        ownca_create_authority_key_identifier=dict(type='bool', default=True),
    ))
    argument_spec.mutually_exclusive.extend([
        ['ownca_path', 'ownca_content'],
        ['ownca_privatekey_path', 'ownca_privatekey_content'],
    ])
