import enum
import hashlib
from dataclasses import dataclass
from datetime import datetime, timezone
from typing import Any, Dict, Iterator, Optional, Union

from asn1crypto import core, x509

from pyhanko_certvalidator.revinfo.archival import CRLContainer, OCSPContainer

__all__ = [
    'KnownPOE',
    'POEManager',
    'POEType',
    'ValidationObject',
    'ValidationObjectType',
    'digest_for_poe',
]


@enum.unique
class ValidationObjectType(enum.Enum):
    """
    Types of validation objects recognised by ETSI TS 119 102-2.
    """

    CERTIFICATE = 'certificate'
    CRL = 'CRL'
    OCSP_RESPONSE = 'OCSPResponse'
    TIMESTAMP = 'timestamp'
    EVIDENCE_RECORD = 'evidencerecord'
    PUBLIC_KEY = 'publicKey'
    SIGNED_DATA = 'signedData'
    OTHER = 'other'

    def urn(self):
        return f'urn:etsi:019102:validationObject:{self.value}'


KnownObjectType = Union[bytes, CRLContainer, OCSPContainer, x509.Certificate]


def guess_validation_object_type(
    thing: object,
) -> Optional[ValidationObjectType]:
    if isinstance(thing, CRLContainer):
        return ValidationObjectType.CRL
    elif isinstance(thing, OCSPContainer):
        return ValidationObjectType.OCSP_RESPONSE
    elif isinstance(thing, x509.Certificate):
        return ValidationObjectType.CERTIFICATE
    return None


@dataclass(frozen=True)
class ValidationObject:
    """
    A validation object used in the course of a validation operation
    for which proofs of existence can potentially be gathered.
    """

    object_type: ValidationObjectType
    """
    The type of validation object.
    """

    value: Any
    """
    The actual object.

    Currently, the following types are supported explicitly.
    Others must currently be supplied as :class:`bytes`.

     - :class:`.CRLContainer`: :attr:`.ValidationObjectType.CRL`
     - :class:`.OCSPContainer`: :attr:`.ValidationObjectType.OCSP_RESPONSE`
     - :class:`x509.Certificate`: :attr:`.ValidationObjectType.CERTIFICATE`
    """


@enum.unique
class POEType(enum.Enum):
    PROVIDED = 'provided'
    VALIDATION = 'validation'
    POLICY = 'policy'

    @property
    def urn(self) -> str:
        return f'urn:etsi:019102:poetype:{self.value}'


@dataclass(frozen=True)
class KnownPOE:
    poe_type: POEType
    digest: bytes
    poe_time: datetime
    validation_object: Optional[ValidationObject] = None


def digest_for_poe(data: bytes) -> bytes:
    return hashlib.sha256(data).digest()


class POEManager:
    """
    Class to manage proof-of-existence (POE) claims.

    :param current_dt_override:
        Override the current time.
    """

    def __init__(self, current_dt_override: Optional[datetime] = None):
        self._poes: Dict[bytes, KnownPOE] = {}
        self._current_dt_override = current_dt_override

    def register(
        self,
        data: KnownObjectType,
        poe_type: POEType,
        dt: Optional[datetime] = None,
    ) -> KnownPOE:
        """
        Register a new POE claim if no POE for an earlier time is available.

        :param data:
            Data to register a POE claim for.
        :param poe_type:
            The type of POE.
        :param dt:
            The POE time to register. If ``None``, assume the current time.
        :return:
            The oldest POE datetime available.
        """
        if isinstance(data, bytes):
            b_data = data
        elif isinstance(data, core.Asn1Value):
            b_data = data.dump()
        elif isinstance(data, CRLContainer):
            b_data = data.crl_data.dump()
        elif isinstance(data, OCSPContainer):
            b_data = data.ocsp_response_data.dump()
        else:
            raise NotImplementedError
        digest = digest_for_poe(b_data)

        dt = dt or self._current_dt_override or datetime.now(timezone.utc)
        vo_type = guess_validation_object_type(data)
        vo = None
        if vo_type:
            vo = ValidationObject(object_type=vo_type, value=data)
        return self.register_known_poe(
            KnownPOE(
                poe_type=poe_type,
                digest=digest,
                poe_time=dt,
                validation_object=vo,
            )
        )

    def register_by_digest(
        self,
        digest: bytes,
        poe_type: POEType,
        dt: Optional[datetime] = None,
    ) -> KnownPOE:
        """
        Register a new POE claim if no POE for an earlier time is available.

        :param digest:
            SHA-256 digest of the data to register a POE claim for.
        :param dt:
            The POE time to register. If ``None``, assume the current time.
        :param poe_type:
            The type of POE.
        :return:
            The oldest POE datetime available.
        """
        dt = dt or self._current_dt_override or datetime.now(timezone.utc)
        return self.register_known_poe(
            KnownPOE(
                poe_type=poe_type,
                digest=digest,
                poe_time=dt,
                validation_object=None,
            )
        )

    def register_known_poe(self, known_poe: KnownPOE) -> KnownPOE:
        """
        Register a new POE claim if no POE for an earlier time is available.

        :param known_poe:
            The POE object to register.
        :return:
            The oldest POE for the given digest.
        """
        dt = known_poe.poe_time
        digest = known_poe.digest
        try:
            cur_poe = self._poes[digest]
            if cur_poe.poe_time <= dt:
                return cur_poe
        except KeyError:
            pass
        self._poes[digest] = known_poe
        return known_poe

    def __iter__(self) -> Iterator[KnownPOE]:
        """
        Iterate over the current earliest known POE for all items currently
        being managed.

        Returns an iterator with :class:`KnownPOE` objects.
        """
        return iter(self._poes.values())

    def __getitem__(self, item: KnownObjectType) -> datetime:
        """
        Return the earliest available POE for an item.

        .. note::
            This is a wrapper around :meth:`register` with `dt=None`, and hence
            will register the current time as the POE time for the given item.
            This side effect is intentional.

        :param item:
            Item to get the current POE time for.
        :return:
            A datetime object representing the earliest available POE for the
            item.
        """
        return self.register(
            item, poe_type=POEType.VALIDATION, dt=None
        ).poe_time

    def __ior__(self, other):
        """
        Combine data in another POE manager with the POEs managed by this
        instance.
        """
        if not isinstance(other, POEManager):
            raise TypeError
        for poe in iter(other):
            self.register_known_poe(poe)

    def __copy__(self):
        new_instance = POEManager(current_dt_override=self._current_dt_override)
        new_instance._poes = dict(self._poes)
        return new_instance
