import re
import threading
from contextlib import contextmanager
from copy import deepcopy
from datetime import datetime, time, timedelta
from html import entities

from django.conf import settings
from django.contrib.auth.tokens import PasswordResetTokenGenerator
from django.core.management import call_command
from django.template.defaulttags import CsrfTokenNode
from django.test.utils import override_settings
from django.utils.crypto import constant_time_compare
from django.utils.http import base36_to_int

from .models import CronLock, logger

ENTITY_RE = re.compile(r"&(\w+?;)")

# create search engine settings, with autoupdate turned OFF
if hasattr(settings, "WAGTAILSEARCH_BACKENDS"):
    NO_AUTOUPDATE_SEARCH = deepcopy(settings.WAGTAILSEARCH_BACKENDS)
    NO_AUTOUPDATE_SEARCH["default"]["AUTO_UPDATE"] = False
else:
    NO_AUTOUPDATE_SEARCH = {}


def disable_searchengine_updates():
    SETTINGS_MODULE = settings.SETTINGS_MODULE
    return override_settings(
        WAGTAILSEARCH_BACKENDS=NO_AUTOUPDATE_SEARCH, SETTINGS_MODULE=SETTINGS_MODULE
    )


def _unicode_for_entity_with_name(name):
    # if the entity is unknown, insert niks
    return entities.html5.get(name, " ")


def escape_single(matchobj):
    return _unicode_for_entity_with_name(matchobj.group(1))


def resolve_entities(string):
    """Resolve all html entities to their corresponding unicode character"""
    if not string:
        return ""

    return ENTITY_RE.sub(escape_single, string)


class RandomTokenGenerator(PasswordResetTokenGenerator):
    key_salt = "ocyan.plugin.oscar_checkout.checkout.utils.RandomTokenGenerator"

    def check_token(self, user, token):
        """
        Check that a password reset token is correct for a given user.
        """
        if not (user and token):
            return False
        # Parse the token
        try:
            ts_b36, _ = token.split("-")
            # Support pre-Django 3.1 generated tokens.
            legacy_token = len(ts_b36) < 4
        except ValueError:
            return False

        try:
            ts = base36_to_int(ts_b36)
        except ValueError:
            return False

        # Check that the timestamp/uid has not been tampered with
        if not constant_time_compare(
            self._make_token_with_timestamp(user, ts, self.secret), token
        ):
            # Support pre-Django 3.1 generated tokens.
            if not constant_time_compare(
                self._make_token_with_timestamp(user, ts, self.secret),
                token,
            ):
                return False

        # convert days to seconds and round to
        # midnight (server time) for pre-Django 3.1 generated tokens.
        now = self._now()
        if legacy_token:
            ts *= 24 * 60 * 60
            ts += int((now - datetime.combine(now.date(), time.min)).total_seconds())

        # Check the timestamp is within limit.
        if (self._num_seconds(now) - ts) > settings.OCYAN_RANDOM_TOKEN_TIMEOUT:
            return False

        return True


randomtokengenerator = RandomTokenGenerator()


@contextmanager
def acquire_cronlock(name, max_age=timedelta(hours=12)):
    thread_id = threading.get_ident()
    try:
        lock = CronLock.objects.lock(name, thread_id, max_age)
        yield bool(lock)
    finally:
        if lock is not None:
            lock.release()


def run_exclusive(name, *args, max_age=timedelta(hours=12), **kwargs):
    """
    Run a management command inside a database lock.

    This ensures that only 1 process can be running the command at the same
    time, even across multiple application servers.
    """
    thread_id = threading.get_ident()
    lock = CronLock.objects.lock(name, thread_id, max_age)

    if lock is not None:
        try:
            logger.debug("Running command %s", name)
            call_command(name, *args, **kwargs)
        finally:
            lock.release()


class NonceNode(CsrfTokenNode):
    "Templatetag node implementation for the nonce templatetag"

    def render(self, context):
        nonce = context.get("csrf_token")
        if nonce:
            return "%s==" % nonce

        return super().render(context)
