"""Static Authenticator models"""

from base64 import b32encode
from os import urandom

from django.conf import settings
from django.core.validators import MaxValueValidator
from django.db import models
from django.utils.translation import gettext_lazy as _
from django.views import View
from rest_framework.serializers import BaseSerializer

from authentik.core.types import UserSettingSerializer
from authentik.flows.models import ConfigurableStage, FriendlyNamedStage, Stage
from authentik.lib.models import SerializerModel
from authentik.stages.authenticator.models import Device, ThrottlingMixin


class AuthenticatorStaticStage(ConfigurableStage, FriendlyNamedStage, Stage):
    """Setup static token based authentication for the user."""

    token_count = models.PositiveIntegerField(default=6)
    token_length = models.PositiveIntegerField(default=12, validators=[MaxValueValidator(100)])

    @property
    def serializer(self) -> type[BaseSerializer]:
        from authentik.stages.authenticator_static.api import AuthenticatorStaticStageSerializer

        return AuthenticatorStaticStageSerializer

    @property
    def view(self) -> type[View]:
        from authentik.stages.authenticator_static.stage import AuthenticatorStaticStageView

        return AuthenticatorStaticStageView

    @property
    def component(self) -> str:
        return "ak-stage-authenticator-static-form"

    def ui_user_settings(self) -> UserSettingSerializer | None:
        return UserSettingSerializer(
            data={
                "title": self.friendly_name or str(self._meta.verbose_name),
                "component": "ak-user-settings-authenticator-static",
            }
        )

    def __str__(self) -> str:
        return f"Static Authenticator Setup Stage {self.name}"

    class Meta:
        verbose_name = _("Static Authenticator Setup Stage")
        verbose_name_plural = _("Static Authenticator Setup Stages")


class StaticDevice(SerializerModel, ThrottlingMixin, Device):
    """
    A static :class:`~authentik.stages.authenticator.models.Device` simply consists of random
    tokens shared by the database and the user.

    These are frequently used as emergency tokens in case a user's normal
    device is lost or unavailable. They can be consumed in any order; each
    token will be removed from the database as soon as it is used.

    This model has no fields of its own, but serves as a container for
    :class:`StaticToken` objects.

    .. attribute:: token_set

        The RelatedManager for our tokens.

    """

    @property
    def serializer(self) -> type[BaseSerializer]:
        from authentik.stages.authenticator_static.api import StaticDeviceSerializer

        return StaticDeviceSerializer

    def get_throttle_factor(self):
        return getattr(settings, "OTP_STATIC_THROTTLE_FACTOR", 1)

    def verify_token(self, token):
        verify_allowed, _ = self.verify_is_allowed()
        if verify_allowed:
            match = self.token_set.filter(token=token).first()
            if match is not None:
                match.delete()
                self.throttle_reset()
            else:
                self.throttle_increment()
        else:
            match = None

        return match is not None

    class Meta(Device.Meta):
        verbose_name = _("Static Device")
        verbose_name_plural = _("Static Devices")


class StaticToken(models.Model):
    """
    A single token belonging to a :class:`StaticDevice`.

    .. attribute:: device

        *ForeignKey*: A foreign key to :class:`StaticDevice`.

    .. attribute:: token

        *CharField*: A random string up to 100 characters.
    """

    device = models.ForeignKey(StaticDevice, related_name="token_set", on_delete=models.CASCADE)
    token = models.CharField(max_length=100, db_index=True)

    class Meta:
        verbose_name = _("Static Token")
        verbose_name_plural = _("Static Tokens")

    def __str__(self) -> str:
        return "Static Token"

    @staticmethod
    def random_token():
        """
        Returns a new random string that can be used as a static token.

        :rtype: bytes

        """
        return b32encode(urandom(5)).decode("utf-8").lower()
