import time
from functools import lru_cache
from typing import Union

import requests
from cryptography import x509
from cryptography.x509.extensions import ExtensionNotFound
from cryptography.x509.oid import ExtensionOID
from loguru import logger

import pki_tools
from pki_tools import types, exceptions


def is_revoked(
    cert: Union[x509.Certificate, types.PemCert], crl_cache_seconds: int = 3600
) -> bool:
    """
    Checks if a certificate is revoked using the CRL extensions.

    Arguments:
        cert -- The certificate to check revocation for. Can either be
        a
        [x509.Certificate](https://cryptography.io/en/latest/x509/reference/#cryptography.x509.Certificate)
        or a
        [types.PemCert](https://pki-tools.fulder.dev/pki_tools/types/#pemcert)
        string
        crl_cache_seconds -- Specifies how long the CRL should be
        cached, default is 1 hour.
    Returns:
        True if the certificate is revoked, False otherwise
    Raises:
        [exceptions.ExtensionMissing](https://pki-tools.fulder.dev/pki_tools/exceptions/#extensionmissing)
        -- When CRL extension is missing

        [exceptions.CrlFetchFailure](https://pki-tools.fulder.dev/pki_tools/exceptions/#crlfetchfailure)
        -- When the CRL could not be fetched

        [exceptions.CrlLoadError](https://pki-tools.fulder.dev/pki_tools/exceptions/#crlloaderror)
        -- If CRL could be fetched successfully but could not be loaded e.g.
        due invalid format or file

        [exceptions.Error](https://pki-tools.fulder.dev/pki_tools/exceptions/#error)
        -- If revocation check fails both with OCSP and CRL
    """
    if types._is_pem_str(cert):
        cert = pki_tools.cert_from_pem(cert)

    log = logger.bind(
        cert=pki_tools.pem_from_cert(cert),
        serial=cert.serial_number,
    )

    ext = cert.extensions
    try:
        crl_ex = ext.get_extension_for_oid(
            ExtensionOID.CRL_DISTRIBUTION_POINTS,
        )

        for dist_point in crl_ex.value:
            for full_name in dist_point.full_name:
                crl_url = full_name.value

                cache_ttl = round(time.time() / crl_cache_seconds)
                crl = _get_crl_from_url(crl_url, cache_ttl=cache_ttl)

                r = crl.get_revoked_certificate_by_serial_number(
                    cert.serial_number,
                )
                if r is not None:
                    log.bind(date=r.revocation_date).info(
                        "Certificate revoked"
                    )
                    return True
    except ExtensionNotFound:
        log.debug("CRL extension missing")
        raise exceptions.ExtensionMissing()
    return False


@lru_cache(maxsize=None)
def _get_crl_from_url(crl_url, cache_ttl=None):
    del cache_ttl

    ret = requests.get(crl_url)

    if ret.status_code != 200:
        logger.bind(
            status=ret.status_code,
            url=crl_url,
        ).error("Failed to fetch CRL from URL")
        raise exceptions.CrlFetchFailure()

    crl_data = ret.content
    return _crl_data_to_crypto(crl_data)


def _crl_data_to_crypto(crl_data):
    try:
        return x509.load_der_x509_crl(crl_data)
    except (TypeError, ValueError):
        pass

    try:
        return x509.load_pem_x509_crl(crl_data)
    except TypeError as e:
        logger.bind(crl=crl_data).error("Failed to load CRL")
        raise exceptions.CrlLoadError(e) from None
