from typing import Iterable, Optional

from asn1crypto import x509

from .context import ValidationContext
from .errors import InvalidCertificateError, PathBuildingError, ValidationError
from .path import ValidationPath
from .policy_decl import PKIXValidationParams
from .util import CancelableAsyncIterator
from .validate import async_validate_path, validate_tls_hostname, validate_usage
from .version import __version__, __version_info__

__all__ = [
    'CertificateValidator',
    'PKIXValidationParams',
    'ValidationContext',
    '__version__',
    '__version_info__',
    'find_valid_path',
]


async def find_valid_path(
    certificate: x509.Certificate,
    paths: CancelableAsyncIterator[ValidationPath],
    validation_context: ValidationContext,
    pkix_validation_params: Optional[PKIXValidationParams] = None,
):
    exceptions = []
    try:
        async for candidate_path in paths:
            try:
                await async_validate_path(
                    validation_context, candidate_path, pkix_validation_params
                )
                return candidate_path
            except ValidationError as e:
                exceptions.append(e)
    except PathBuildingError:
        if certificate.self_signed in {'yes', 'maybe'}:
            raise InvalidCertificateError(
                f'The X.509 certificate provided is self-signed - '
                f'"{certificate.subject.human_friendly}"'
            )
        raise
    finally:
        await paths.cancel()

    if len(exceptions) == 1:
        raise exceptions[0]

    non_signature_exception = None
    for exception in exceptions:
        if 'signature' not in str(exception):
            non_signature_exception = exception

    if non_signature_exception:
        raise non_signature_exception

    raise exceptions[0]


class CertificateValidator:
    # A pyhanko_certvalidator.path.ValidationPath object - only set once validated
    _path = None

    def __init__(
        self,
        end_entity_cert: x509.Certificate,
        intermediate_certs: Optional[Iterable[x509.Certificate]] = None,
        validation_context: Optional[ValidationContext] = None,
        pkix_params: Optional[PKIXValidationParams] = None,
    ):
        """
        :param end_entity_cert:
            An asn1crypto.x509.Certificate object X.509 end-entity
            certificate to validate

        :param intermediate_certs:
            None or a list of asn1crypto.x509.Certificate
            Used in constructing certificate paths for validation.

        :param validation_context:
            A pyhanko_certvalidator.context.ValidationContext() object that
            controls generic validation options and tracks revocation data.

            The same validation context will also be used in the validation
            of relevant certificates found in OCSP responses and/or CRLs.

        :param pkix_params:
            A pyhanko_certvalidator.context.PKIXValidationParams() object that
            controls advanced PKIX validation parameters used to validate
            the end-entity certificate. These can be used to constrain
            policy processing and names.

            Ancillary validation of CRLs and OCSP responses ignore these
            settings.
        """

        if validation_context is None:
            validation_context = ValidationContext()

        if intermediate_certs is not None:
            certificate_registry = validation_context.certificate_registry
            for intermediate_cert in intermediate_certs:
                certificate_registry.register(intermediate_cert)

        self._context: ValidationContext = validation_context
        self._certificate: x509.Certificate = end_entity_cert
        self._params: Optional[PKIXValidationParams] = pkix_params

    async def async_validate_path(self) -> ValidationPath:
        """
        Builds possible certificate paths and validates them until a valid one
        is found, or all fail.

        :raises:
            pyhanko_certvalidator.errors.PathBuildingError - when an error occurs building the path
            pyhanko_certvalidator.errors.PathValidationError - when an error occurs validating the path
            pyhanko_certvalidator.errors.RevokedError - when the certificate or another certificate in its path has been revoked
        """

        if self._path is not None:
            return self._path

        certificate = self._certificate

        paths = self._context.path_builder.async_build_paths_lazy(certificate)
        self._path = candidate_path = await find_valid_path(
            certificate,
            paths,
            validation_context=self._context,
            pkix_validation_params=self._params,
        )
        return candidate_path

    async def async_validate_usage(
        self, key_usage, extended_key_usage=None, extended_optional=False
    ):
        """
        Validates the certificate path and that the certificate is valid for
        the key usage and extended key usage purposes specified.

        :param key_usage:
            A set of unicode strings of the required key usage purposes. Valid
            values include:

             - "digital_signature"
             - "non_repudiation"
             - "key_encipherment"
             - "data_encipherment"
             - "key_agreement"
             - "key_cert_sign"
             - "crl_sign"
             - "encipher_only"
             - "decipher_only"

        :param extended_key_usage:
            A set of unicode strings of the required extended key usage
            purposes. These must be either dotted number OIDs, or one of the
            following extended key usage purposes:

             - "server_auth"
             - "client_auth"
             - "code_signing"
             - "email_protection"
             - "ipsec_end_system"
             - "ipsec_tunnel"
             - "ipsec_user"
             - "time_stamping"
             - "ocsp_signing"
             - "wireless_access_points"

            An example of a dotted number OID:

             - "1.3.6.1.5.5.7.3.1"

        :param extended_optional:
            A bool - if the extended_key_usage extension may be ommited and still
            considered valid

        :raises:
            pyhanko_certvalidator.errors.PathValidationError - when an error occurs validating the path
            pyhanko_certvalidator.errors.RevokedError - when the certificate or another certificate in its path has been revoked
            pyhanko_certvalidator.errors.InvalidCertificateError - when the certificate is not valid for the usages specified

        :return:
            A pyhanko_certvalidator.path.ValidationPath object of the validated
            certificate validation path
        """

        validated_path = await self.async_validate_path()
        validate_usage(
            self._context,
            self._certificate,
            key_usage,
            extended_key_usage,
            extended_optional,
        )
        return validated_path

    async def async_validate_tls(self, hostname):
        """
        Validates the certificate path, that the certificate is valid for
        the hostname provided and that the certificate is valid for the purpose
        of a TLS connection.

        :param hostname:
            A unicode string of the TLS server hostname

        :raises:
            pyhanko_certvalidator.errors.PathValidationError - when an error occurs validating the path
            pyhanko_certvalidator.errors.RevokedError - when the certificate or another certificate in its path has been revoked
            pyhanko_certvalidator.errors.InvalidCertificateError - when the certificate is not valid for TLS or the hostname

        :return:
            A pyhanko_certvalidator.path.ValidationPath object of the validated
            certificate validation path
        """

        await self.async_validate_path()
        validate_tls_hostname(self._context, self._certificate, hostname)
        return self._path
