"""authentik proxy models"""

import string
from collections.abc import Iterable
from random import SystemRandom
from urllib.parse import urljoin
from uuid import uuid4

from django.db import models
from django.templatetags.static import static
from django.utils.translation import gettext as _
from rest_framework.serializers import Serializer

from authentik.core.models import ExpiringModel
from authentik.crypto.models import CertificateKeyPair
from authentik.lib.models import DomainlessURLValidator, InternallyManagedMixin
from authentik.outposts.models import OutpostModel
from authentik.providers.oauth2.models import (
    ClientType,
    GrantType,
    OAuth2Provider,
    RedirectURI,
    RedirectURIMatchingMode,
    ScopeMapping,
)

SCOPE_AK_PROXY = "ak_proxy"
OUTPOST_CALLBACK_SIGNATURE = "X-authentik-auth-callback"


class ProxySession(InternallyManagedMixin, ExpiringModel):
    """Session storage for proxyv2 outposts using PostgreSQL"""

    uuid = models.UUIDField(default=uuid4, primary_key=True)
    session_key = models.TextField(unique=True, db_index=True)
    user_id = models.UUIDField(null=True, blank=True, db_index=True)

    session_data = models.JSONField(default=dict, blank=True)

    class Meta:
        verbose_name = _("Proxy Session")
        verbose_name_plural = _("Proxy Sessions")
        indexes = [
            models.Index(fields=["user_id"]),
        ]

    def __str__(self) -> str:
        return f"Session {self.session_key[:8]}..."


def get_cookie_secret():
    """Generate random 32-character string for cookie-secret"""
    return "".join(SystemRandom().choice(string.ascii_uppercase + string.digits) for _ in range(32))


def _get_callback_url(uri: str) -> list[RedirectURI]:
    return [
        RedirectURI(
            RedirectURIMatchingMode.STRICT,
            urljoin(uri, "outpost.goauthentik.io/callback") + f"?{OUTPOST_CALLBACK_SIGNATURE}=true",
        ),
        RedirectURI(RedirectURIMatchingMode.STRICT, uri + f"?{OUTPOST_CALLBACK_SIGNATURE}=true"),
    ]


class ProxyMode(models.TextChoices):
    """All modes a Proxy provider can operate in"""

    PROXY = "proxy"
    FORWARD_SINGLE = "forward_single"
    FORWARD_DOMAIN = "forward_domain"


class ProxyProvider(OutpostModel, OAuth2Provider):
    """Protect applications that don't support any of the other
    Protocols by using a Reverse-Proxy."""

    internal_host = models.TextField(
        validators=[DomainlessURLValidator(schemes=("http", "https"))],
        blank=True,
    )
    external_host = models.TextField(validators=[DomainlessURLValidator(schemes=("http", "https"))])
    internal_host_ssl_validation = models.BooleanField(
        default=True,
        help_text=_("Validate SSL Certificates of upstream servers"),
        verbose_name=_("Internal host SSL Validation"),
    )
    mode = models.TextField(
        default=ProxyMode.PROXY,
        choices=ProxyMode.choices,
        help_text=_(
            "Enable support for forwardAuth in traefik and nginx auth_request. Exclusive with "
            "internal_host."
        ),
    )

    skip_path_regex = models.TextField(
        default="",
        blank=True,
        help_text=_(
            "Regular expressions for which authentication is not required. "
            "Each new line is interpreted as a new Regular Expression."
        ),
    )

    intercept_header_auth = models.BooleanField(
        default=True,
        help_text=_(
            "When enabled, this provider will intercept the authorization header and authenticate "
            "requests based on its value."
        ),
    )
    basic_auth_enabled = models.BooleanField(
        default=False,
        verbose_name=_("Set HTTP-Basic Authentication"),
        help_text=_(
            "Set a custom HTTP-Basic Authentication header based on values from authentik."
        ),
    )
    basic_auth_user_attribute = models.TextField(
        blank=True,
        verbose_name=_("HTTP-Basic Username Key"),
        help_text=_(
            "User/Group Attribute used for the user part of the HTTP-Basic Header. "
            "If not set, the user's Email address is used."
        ),
    )
    basic_auth_password_attribute = models.TextField(
        blank=True,
        verbose_name=_("HTTP-Basic Password Key"),
        help_text=_("User/Group Attribute used for the password part of the HTTP-Basic Header."),
    )

    certificate = models.ForeignKey(
        CertificateKeyPair,
        on_delete=models.SET_NULL,
        null=True,
        blank=True,
    )

    cookie_secret = models.TextField(default=get_cookie_secret)
    cookie_domain = models.TextField(default="", blank=True)

    @property
    def component(self) -> str:
        return "ak-provider-proxy-form"

    @property
    def icon_url(self) -> str | None:
        return static("authentik/sources/proxy.svg")

    @property
    def serializer(self) -> type[Serializer]:
        from authentik.providers.proxy.api import ProxyProviderSerializer

        return ProxyProviderSerializer

    @property
    def launch_url(self) -> str | None:
        """Use external_host as launch URL"""
        return self.external_host

    def set_oauth_defaults(self):
        """Ensure all OAuth2-related settings are correct"""
        self.grant_types = [
            GrantType.AUTHORIZATION_CODE,
            GrantType.CLIENT_CREDENTIALS,
            GrantType.PASSWORD,
        ]
        self.client_type = ClientType.CONFIDENTIAL
        self.signing_key = None
        self.include_claims_in_id_token = True
        scopes = ScopeMapping.objects.filter(
            managed__in=[
                "goauthentik.io/providers/oauth2/scope-openid",
                "goauthentik.io/providers/oauth2/scope-profile",
                "goauthentik.io/providers/oauth2/scope-email",
                "goauthentik.io/providers/oauth2/scope-entitlements",
                "goauthentik.io/providers/proxy/scope-proxy",
            ]
        )
        self.property_mappings.add(*list(scopes))
        self.redirect_uris = _get_callback_url(self.external_host)

    def __str__(self):
        return f"Proxy Provider {self.name}"

    def get_required_objects(self) -> Iterable[models.Model | str | tuple[str, models.Model]]:
        required = [self]
        if self.certificate is not None:
            required.append(("authentik_crypto.view_certificatekeypair", self.certificate))
            required.append(
                ("authentik_crypto.view_certificatekeypair_certificate", self.certificate)
            )
            required.append(("authentik_crypto.view_certificatekeypair_key", self.certificate))
        return required

    class Meta:
        verbose_name = _("Proxy Provider")
        verbose_name_plural = _("Proxy Providers")
        authentik_used_by_shadows = ["authentik_providers_oauth2.oauth2provider"]
