import logging

import asn1crypto.pem
import asn1crypto.x509
from oscrypto import asymmetric
from pyasn1.codec.der import encoder as der_encoder
from pyasn1.codec.der import decoder as der_decoder

from . import asn1
from .exceptions import CertificateVerificationError

logger = logging.getLogger(__name__)


class Certificate(object):
    def __init__(self, data):
        """Representation of a Certificate. It is built from an ASN.1 structure.

        :type data: asn1.pkcs7.ExtendedCertificateOrCertificate or asn1.x509.Certificate or asn1.x509.TBSCertificate
        :param data: The ASN.1 structure
        """

        self.data = data
        self._parse()

    def _parse(self):
        if isinstance(self.data, asn1.pkcs7.ExtendedCertificateOrCertificate):
            if 'extendedCertificate' in self.data:
                # TODO: Not sure if needed.
                raise NotImplementedError("Support for extendedCertificate is not implemented")

            certificate = self.data['certificate']
            self.signature_algorithm = certificate['signatureAlgorithm']
            self.signature_value = certificate['signatureValue']
            tbs_certificate = certificate['tbsCertificate']

        elif isinstance(self.data, asn1.x509.Certificate):
            certificate = self.data
            self.signature_algorithm = certificate['signatureAlgorithm']
            self.signature_value = certificate['signatureValue']
            tbs_certificate = certificate['tbsCertificate']

        else:
            tbs_certificate = self.data

        self.version = int(tbs_certificate['version']) + 1
        self.serial_number = int(tbs_certificate['serialNumber'])
        self.issuer = tbs_certificate['issuer'][0]
        self.issuer_dn = tbs_certificate['issuer'][0].to_string()
        self.valid_from = tbs_certificate['validity']['notBefore'].to_python_time()
        self.valid_to = tbs_certificate['validity']['notAfter'].to_python_time()
        self.subject = tbs_certificate['subject'][0]
        self.subject_dn = tbs_certificate['subject'][0].to_string()

        self.subject_public_algorithm = tbs_certificate['subjectPublicKeyInfo']['algorithm']
        self.subject_public_key = tbs_certificate['subjectPublicKeyInfo']['subjectPublicKey']

        self.extensions = {}
        if 'extensions' in tbs_certificate and tbs_certificate['extensions'].isValue:
            for extension in tbs_certificate['extensions']:
                self.extensions[asn1.oids.get(extension['extnID'])] = extension['extnValue']

    def __str__(self):
        return "{}(serial:{})".format(self.subject_dn, self.serial_number)

    def __eq__(self, other):
        return isinstance(other, Certificate) and \
               self.issuer == other.issuer and \
               self.serial_number == other.serial_number and \
               self.subject == other.subject and \
               self.subject_public_algorithm == other.subject_public_algorithm and \
               self.subject_public_key == other.subject_public_key

    @classmethod
    def from_der(cls, content):
        """Load the Certificate object from DER-encoded data"""
        return cls(der_decoder.decode(content, asn1Spec=asn1.x509.Certificate())[0])

    @classmethod
    def from_pem(cls, content):
        """Reads a Certificate from a PEM formatted file."""
        type_name, headers, der_bytes = asn1crypto.pem.unarmor(content)
        return cls.from_der(der_bytes)

    @property
    def to_der(self):
        """Returns the DER-encoded data from this certificate."""
        return der_encoder.encode(self.data)

    @property
    def to_asn1crypto(self):
        """Retrieves the :mod:`asn1crypto` x509 Certificate object."""
        return asn1crypto.x509.Certificate.load(self.to_der)

    def verify_signature(self, signature, data, algorithm, allow_legacy=False):
        """Verifies whether the signature bytes match the data using the hashing algorithm. Supports RSA and EC keys.
        Note that not all hashing algorithms are supported.

        :param bytes signature: The signature to verify
        :param bytes data: The data that must be verified
        :type algorithm: a hashlib function
        :param algorithm: The hashing algorithm to use
        :param bool allow_legacy: If True, allows a legacy signature verification. This method is intended for the case
            where the encryptedDigest does not contain an ASN.1 structure, but a raw hash value instead. It is attempted
            automatically when verification of the RSA signature fails.

            This case is described in more detail on
            https://mta.openssl.org/pipermail/openssl-users/2015-September/002053.html
        """

        public_key = asymmetric.load_public_key(self.to_asn1crypto.public_key)
        if public_key.algorithm == 'rsa':
            verify_func = asymmetric.rsa_pkcs1v15_verify
        elif public_key.algorithm == 'dsa':
            verify_func = asymmetric.dsa_verify
        elif public_key.algorithm == 'ec':
            verify_func = asymmetric.ecdsa_verify
        else:
            raise CertificateVerificationError("Signature algorithm %s is unsupported for %s" %
                                               (public_key.algorithm, self))

        try:
            verify_func(public_key, signature, data, algorithm().name)
        except Exception as e:
            if not allow_legacy or public_key.algorithm != 'rsa':
                raise CertificateVerificationError("Invalid signature for %s: %s" % (self, e))
        else:
            return

        try:
            asymmetric.rsa_pkcs1v15_verify(public_key, signature, algorithm(data).digest(), 'raw')
        except Exception as e:
            raise CertificateVerificationError("Invalid signature for %s (legacy attempted): %s" % (self, e))

    def potential_chains(self, context):
        """Alias for :meth:`VerificationContext.potential_chains`"""

        return context.potential_chains(self)

    def verify(self, context):
        """Alias for :meth:`VerificationContext.verify`"""

        return context.verify(self)
