# Copyright 2024 The Debusine Developers
# See the AUTHORS file at the top-level directory of this distribution
#
# This file is part of Debusine. It is subject to the license terms
# in the LICENSE file found in the top-level directory of this
# distribution. No part of Debusine, including this file, may be copied,
# modified, propagated, or distributed except according to the terms
# contained in the LICENSE file.
"""Transparently add scopes to URLs."""

import re
from collections.abc import Callable
from typing import TYPE_CHECKING

import django.http
from django.conf import settings

from debusine.server.scopes import get_scope_urlconf

if TYPE_CHECKING:
    from debusine.db.models import Scope

scope_prefix_re = re.compile(r'^/([^/]+)(/|$)')


class ScopeMiddleware:
    """
    Extract the current scope from the URL prefix.

    If used, it must be sequenced before
    django.middleware.common.CommonMiddleware, since it can make use of URL
    resolution.
    """

    def __init__(
        self,
        get_response: Callable[
            [django.http.HttpRequest], django.http.HttpResponse
        ],
    ):
        """Middleware API entry point."""
        self.get_response = get_response

    def __call__(
        self, request: django.http.HttpRequest
    ) -> django.http.HttpResponse:
        """Middleware entry point."""
        from debusine.db.context import context
        from debusine.db.models.scopes import is_valid_scope_name

        scope_name: str | None = None

        if mo := scope_prefix_re.match(request.path_info):
            if mo.group(1) == "api":
                # /api/ gets special treatment, as scope can also be specified
                # in a header
                scope_name = request.headers.get("x-debusine-scope")
            elif is_valid_scope_name(mo.group(1)):
                scope_name = mo.group(1)

        if scope_name is None:
            scope_name = settings.DEBUSINE_DEFAULT_SCOPE

        context.set_scope(self.get_scope(scope_name))
        setattr(request, "urlconf", get_scope_urlconf(scope_name))

        return self.get_response(request)

    def get_scope(self, name: str) -> "Scope":
        """Set the current scope to the given named one."""
        from django.shortcuts import get_object_or_404

        from debusine.db.models import Scope

        return get_object_or_404(Scope, name=name)


class AuthorizationMiddleware:
    """
    Check user access to the current scope.

    If used, it must be sequenced after
    django.contrib.auth.middleware.AuthenticationMiddleware, since it needs the
    current user, and after
    debusine.server.middlewares.token_last_seen_at.TokenLastSeenAtMiddleware
    to validate the access of the worker token.
    """

    def __init__(
        self,
        get_response: Callable[
            [django.http.HttpRequest], django.http.HttpResponse
        ],
    ):
        """Middleware API entry point."""
        self.get_response = get_response

    def __call__(
        self, request: django.http.HttpRequest
    ) -> django.http.HttpResponse:
        """Middleware entry point."""
        from debusine.db.context import ContextConsistencyError, context

        # request.user may be a lazy object, e.g. as set up by
        # AuthenticationMiddleware.  In that case we must force it to be
        # evaluated here, as otherwise asgiref.sync.AsyncToSync may try to
        # evaluate it when restoring context and will raise a
        # SynchronousOnlyOperation exception.
        request.user.username
        try:
            context.set_user(request.user)
        except ContextConsistencyError as e:
            return django.http.HttpResponseForbidden(str(e))
        return self.get_response(request)
