import dataclasses
import enum
from dataclasses import dataclass, field
from datetime import datetime
from typing import Generator, List, Optional

from asn1crypto import x509
from pyhanko.sign.diff_analysis import DEFAULT_DIFF_POLICY, DiffPolicy
from pyhanko.sign.validation import KeyUsageConstraints
from pyhanko.sign.validation.utils import CMSAlgorithmUsagePolicy

from pyhanko_certvalidator.context import (
    CertValidationPolicySpec,
    ValidationDataHandlers,
)
from pyhanko_certvalidator.context import (
    bootstrap_validation_data_handlers as _certvalidator_bootstrap_handlers,
)
from pyhanko_certvalidator.fetchers import FetcherBackend
from pyhanko_certvalidator.fetchers.requests_fetchers import (
    RequestsFetcherBackend,
)
from pyhanko_certvalidator.ltv.poe import (
    KnownPOE,
    POEManager,
    POEType,
    ValidationObject,
    ValidationObjectType,
    digest_for_poe,
)
from pyhanko_certvalidator.ltv.types import ValidationTimingInfo
from pyhanko_certvalidator.policy_decl import NonRevokedStatusAssertion
from pyhanko_certvalidator.revinfo.archival import CRLContainer, OCSPContainer

__all__ = [
    'SignatureValidationSpec',
    'PdfSignatureValidationSpec',
    'RevinfoOnlineFetchingRule',
    'LocalKnowledge',
    'RevocationInfoGatheringSpec',
    'CMSAlgorithmUsagePolicy',
    'bootstrap_validation_data_handlers',
]


class RevinfoOnlineFetchingRule(enum.Enum):
    ALWAYS_FETCH = enum.auto()
    """
    Always permit fetching revocation information from online sources.
    """

    NO_HISTORICAL_FETCH = enum.auto()
    """
    Only attempt to fetch revocation information when performing validation
    of a signature at the current time, and use the local cache in past
    validation evaluation.
    """
    # TODO document precisely what this means, esp. for the legacy API

    LOCAL_ONLY = enum.auto()
    """
    Only use locally cached revocation information.
    """


@dataclass(frozen=True)
class RevocationInfoGatheringSpec:
    online_fetching_rule: RevinfoOnlineFetchingRule = (
        RevinfoOnlineFetchingRule.NO_HISTORICAL_FETCH
    )
    fetcher_backend: FetcherBackend = field(
        default_factory=RequestsFetcherBackend
    )


@dataclass(frozen=True)
class LocalKnowledge:
    known_ocsps: List[OCSPContainer] = field(default_factory=list)
    known_crls: List[CRLContainer] = field(default_factory=list)
    known_certs: List[x509.Certificate] = field(default_factory=list)
    known_poes: List[KnownPOE] = field(default_factory=list)
    nonrevoked_assertions: List[NonRevokedStatusAssertion] = field(
        default_factory=list
    )

    def add_to_poe_manager(self, poe_manager: POEManager):
        for poe in self.known_poes:
            poe_manager.register_known_poe(poe)

    def assert_existence_known_at(
        self, dt: datetime
    ) -> Generator[KnownPOE, None, None]:
        for poe in self.known_poes:
            yield dataclasses.replace(
                poe,
                poe_time=min(poe.poe_time, dt),
                poe_type=POEType.PROVIDED,
            )
        for crl in self.known_crls:
            yield KnownPOE(
                digest=digest_for_poe(crl.crl_data.dump()),
                poe_time=dt,
                poe_type=POEType.PROVIDED,
                validation_object=ValidationObject(
                    ValidationObjectType.CRL, crl
                ),
            )
        for ocsp in self.known_ocsps:
            yield KnownPOE(
                digest=digest_for_poe(ocsp.ocsp_response_data.dump()),
                poe_time=dt,
                poe_type=POEType.PROVIDED,
                validation_object=ValidationObject(
                    ValidationObjectType.OCSP_RESPONSE, ocsp
                ),
            )
        for cert in self.known_certs:
            yield KnownPOE(
                digest=digest_for_poe(cert.dump()),
                poe_time=dt,
                poe_type=POEType.PROVIDED,
                validation_object=ValidationObject(
                    ValidationObjectType.CERTIFICATE, cert
                ),
            )


@dataclass(frozen=True)
class SignatureValidationSpec:
    cert_validation_policy: CertValidationPolicySpec
    revinfo_gathering_policy: RevocationInfoGatheringSpec = (
        RevocationInfoGatheringSpec()
    )
    ts_cert_validation_policy: Optional[CertValidationPolicySpec] = None
    ac_validation_policy: Optional[CertValidationPolicySpec] = None
    local_knowledge: LocalKnowledge = LocalKnowledge()
    key_usage_settings: KeyUsageConstraints = KeyUsageConstraints()
    signature_algorithm_policy: Optional[CMSAlgorithmUsagePolicy] = None


@dataclass(frozen=True)
class PdfSignatureValidationSpec:
    signature_validation_spec: SignatureValidationSpec
    diff_policy: Optional[DiffPolicy] = DEFAULT_DIFF_POLICY


def _backend_if_necessary(
    hist: bool, rule: RevinfoOnlineFetchingRule, backend: FetcherBackend
) -> Optional[FetcherBackend]:
    if rule == RevinfoOnlineFetchingRule.LOCAL_ONLY:
        return None
    elif rule == RevinfoOnlineFetchingRule.NO_HISTORICAL_FETCH and hist:
        return None
    else:
        return backend


def bootstrap_validation_data_handlers(
    spec: SignatureValidationSpec,
    timing_info: Optional[ValidationTimingInfo] = None,
    is_historical: Optional[bool] = None,
    poe_manager_override: Optional[POEManager] = None,
) -> ValidationDataHandlers:
    if is_historical is None:
        hist = (
            timing_info.point_in_time_validation
            if timing_info is not None
            else False
        )
    else:
        hist = is_historical
    revinfo_policy = spec.revinfo_gathering_policy

    fetcher_backend = _backend_if_necessary(
        hist=hist,
        rule=revinfo_policy.online_fetching_rule,
        backend=revinfo_policy.fetcher_backend,
    )

    knowledge = spec.local_knowledge
    handlers = _certvalidator_bootstrap_handlers(
        fetchers=fetcher_backend,
        crls=knowledge.known_crls,
        ocsps=knowledge.known_ocsps,
        certs=knowledge.known_certs,
        poe_manager=poe_manager_override,
        nonrevoked_assertions=knowledge.nonrevoked_assertions,
    )
    return handlers
