import abc
import enum
from dataclasses import dataclass
from datetime import datetime
from typing import Optional

from asn1crypto import keys, x509

from .name_trees import process_general_subtrees
from .policy_decl import PKIXValidationParams


class TrustedServiceType(enum.Enum):
    UNSPECIFIED = 0
    """
    Unspecified.

    If a trust manager designates a trust anchor with this
    service type, it will be considered trusted for any purpose.
    """

    UNSUPPORTED = 1
    """
    Unsupported.

    If a trust manager designates a trust anchor with this
    service type, it will not be considered trusted for any
    purpose other than identifying itself.
    """

    CERTIFICATE_AUTHORITY = 2
    """
    Certificate authority (CA).

    Only trust anchors with this designation can appear
    in a PKIX validation path as the issuer of
    another certificate.
    """

    TIME_STAMPING_AUTHORITY = 3
    """
    Time stamping authority (TSA).
    """


@dataclass(frozen=True)
class TrustQualifiers:
    """
    .. versionadded 0.20.0

    Parameters that allow a trust root to be qualified.
    """

    standard_parameters: Optional['PKIXValidationParams'] = None
    """
    Standard validation parameters that will apply when initialising
    the PKIX validation process.
    """

    max_path_length: Optional[int] = None
    """
    Maximal allowed path length for this trust root, excluding self-issued
    intermediate CA certificates. If ``None``, any path length will be accepted.
    """

    max_aa_path_length: Optional[int] = None
    """
    Maximal allowed path length for this trust root for the purposes of
    AAControls. If ``None``, any path length will be accepted.
    """

    valid_from: Optional[datetime] = None
    """
    Lower bound of the trust anchor's validity period, if any.
    """

    valid_until: Optional[datetime] = None
    """
    Upper bound of the trust anchor's validity period, if any.
    """

    trusted_service_type: TrustedServiceType = TrustedServiceType.UNSPECIFIED
    """
    Indicates the service provided by the trust root.
    """


class Authority(abc.ABC):
    """
    .. versionadded:: 0.20.0

    Abstract authority, i.e. a named key.
    """

    @property
    def name(self) -> x509.Name:
        """
        The authority's name.
        """
        raise NotImplementedError

    @property
    def public_key(self) -> keys.PublicKeyInfo:
        """
        The authority's public key.
        """
        raise NotImplementedError

    @property
    def hashable(self):
        """
        A hashable unique identifier of the authority, used in ``__eq__``
        and ``__hash__``.
        """
        raise NotImplementedError

    def __hash__(self):
        return hash(self.hashable)

    def __eq__(self, other):
        if not isinstance(other, Authority):
            return False

        return self.hashable == other.hashable

    @property
    def key_id(self) -> Optional[bytes]:
        """
        Key ID as (potentially) referenced in an authorityKeyIdentifier
        extension. Only used to eliminate non-matching trust anchors,
        never to retrieve keys or to definitively identify trust anchors.
        """
        raise NotImplementedError

    def is_potential_issuer_of(self, cert: x509.Certificate) -> bool:
        """
        Function to determine whether this trust root could potentially be an
        issuer of a given certificate.
        This function is used during path building.

        :param cert:
            The certificate to evaluate.
        """
        if cert.issuer != self.name:
            return False
        if cert.authority_key_identifier and self.key_id:
            if cert.authority_key_identifier != self.key_id:
                return False
        return True


class TrustAnchor:
    """
    Abstract trust root. A trust root is an authority with trust qualifiers.
    Equality of trust roots reduces to equality of authorities.
    """

    def __init__(
        self, authority: Authority, quals: Optional[TrustQualifiers] = None
    ):
        self._authority = authority
        self._quals = quals

    @property
    def authority(self) -> Authority:
        return self._authority

    @property
    def trust_qualifiers(self) -> TrustQualifiers:
        """
        Qualifiers for the trust root.
        """
        return self._quals or TrustQualifiers()

    def __eq__(self, other):
        return (
            isinstance(other, TrustAnchor)
            and other._authority == self._authority
        )

    def __hash__(self):
        return hash(self._authority)


def derive_quals_from_cert(cert: x509.Certificate) -> TrustQualifiers:
    """
    Extract trust qualifiers from data and extensions of a certificate.

    .. note::
        Recall that any property of a trust root other than its name and public
        key are in principle irrelevant to the PKIX validation algorithm
        itself.
        This function is merely a helper function that allows the certificate's
        other data to be conveniently gathered to populate the default
        validation parameters for paths deriving from that trust root.

    :param cert:
        The certificate from which to extract qualifiers (usually a
        self-signed one)
    :return:
        A :class:`TrustQualifiers` object with the extracted qualifiers.
    """
    # TODO align with RFC 5937?
    ext_found = False
    permitted_subtrees = excluded_subtrees = None
    if cert.name_constraints_value is not None:
        ext_found = True
        nc_ext: x509.NameConstraints = cert.name_constraints_value
        permitted_val = nc_ext['permitted_subtrees']
        if isinstance(permitted_val, x509.GeneralSubtrees):
            permitted_subtrees = process_general_subtrees(permitted_val)
        excluded_val = nc_ext['excluded_subtrees']
        if isinstance(excluded_val, x509.GeneralSubtrees):
            excluded_subtrees = process_general_subtrees(excluded_val)

    acceptable_policies = None
    if cert.certificate_policies_value is not None:
        ext_found = True
        policies_val: x509.CertificatePolicies = cert.certificate_policies_value
        acceptable_policies = frozenset(
            [
                pol_info['policy_identifier'].dotted
                for pol_info in policies_val
                if pol_info['policy_identifier'].native != 'any_policy'
            ]
        )

    params = None
    if ext_found:
        params = PKIXValidationParams(
            user_initial_policy_set=(
                acceptable_policies or frozenset(['any_policy'])
            ),
            # For trust roots where the user asked for this derivation,
            #  let's assume that they want the policies to be enforced.
            initial_explicit_policy=acceptable_policies is not None,
            initial_permitted_subtrees=permitted_subtrees,
            initial_excluded_subtrees=excluded_subtrees,
        )

    return TrustQualifiers(
        max_path_length=cert.max_path_length,
        standard_parameters=params,
        valid_from=cert.not_valid_before,
        valid_until=cert.not_valid_after,
        trusted_service_type=(
            TrustedServiceType.CERTIFICATE_AUTHORITY
            if cert.ca
            else TrustedServiceType.UNSUPPORTED
        ),
    )


class AuthorityWithCert(Authority):
    """
    .. versionadded:: 0.20.0

    Authority provisioned as a certificate.

    :param cert:
        The certificate.
    """

    def __init__(self, cert: x509.Certificate):
        self._cert = cert

    @property
    def name(self) -> x509.Name:
        return self._cert.subject

    @property
    def public_key(self):
        return self._cert.public_key

    @property
    def hashable(self):
        cert = self._cert
        return cert.subject.hashable, cert.public_key.dump()

    @property
    def key_id(self) -> Optional[bytes]:
        return self._cert.key_identifier

    @property
    def certificate(self) -> x509.Certificate:
        return self._cert

    def is_potential_issuer_of(self, cert: x509.Certificate):
        if not super().is_potential_issuer_of(cert):
            return False
        if cert.authority_issuer_serial:
            if cert.authority_issuer_serial != self._cert.issuer_serial:
                return False
        return True


class CertTrustAnchor(TrustAnchor):
    """
    .. versionadded:: 0.20.0

    Trust anchor provisioned as a certificate.

    :param cert:
        The certificate, usually self-signed.
    :param quals:
        Explicit trust qualifiers.
    :param derive_default_quals_from_cert:
        Flag indicating to derive default trust qualifiers from the certificate
        content if explicit ones are not provided. Defaults to ``False``.
    """

    def __init__(
        self,
        cert: x509.Certificate,
        quals: Optional[TrustQualifiers] = None,
        derive_default_quals_from_cert: bool = False,
    ):
        authority = AuthorityWithCert(cert)
        self._cert = cert
        super().__init__(authority, quals)
        self._derive = derive_default_quals_from_cert

    @property
    def certificate(self) -> x509.Certificate:
        return self._cert

    @property
    def trust_qualifiers(self) -> TrustQualifiers:
        if self._quals is not None:
            return self._quals
        elif self._derive:
            self._quals = quals = derive_quals_from_cert(self._cert)
            return quals
        else:
            return TrustQualifiers()


class NamedKeyAuthority(Authority):
    """
    Authority provisioned as a named key.

    :param entity_name:
        The name of the entity that controls the private key of the trust root.
    :param public_key:
        The trust root's public key.
    """

    def __init__(self, entity_name: x509.Name, public_key: keys.PublicKeyInfo):
        self._name = entity_name
        self._public_key = public_key

    @property
    def name(self) -> x509.Name:
        return self._name

    @property
    def public_key(self):
        return self._public_key

    @property
    def key_id(self) -> Optional[bytes]:
        return None

    @property
    def hashable(self):
        return self._name.hashable, self._public_key.dump()
