# 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.

"""Middleware for updating Worker.last_seen_at."""
from collections.abc import Callable
from typing import Any

import django
from channels.db import database_sync_to_async
from django.utils import timezone

from debusine.db.models import Token


class TokenLastSeenAtMiddleware:
    """
    Save the Token in the scope and update Worker.last_seen_at.

    Token available in the request helps to avoid double lookups.
    """

    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):
        """Middleware entry point."""
        token_key = request.headers.get("token")

        if token_key is None:
            request.token = None  # type: ignore[attr-defined]
            return self.get_response(request)

        token = Token.objects.get_token_or_none(token_key)

        if token is not None:
            token.last_seen_at = timezone.now()
            token.save()

        request.token = token  # type: ignore[attr-defined]

        return self.get_response(request)


class TokenLastSeenAtMiddlewareChannels:
    """
    Save the Token in the scope and update Worker.last_seen_at.

    Token available in the scope helps to avoid double lookups.
    """

    def __init__(self, app):
        """Middleware API entry point."""
        self.app = app

    @staticmethod
    def get_token_header(headers: list[tuple[str, Any]]) -> str | None:
        """Return the token key from the headers or None."""
        for header in headers:
            if header[0].lower() == b"token":
                return header[1].decode("utf-8")

        return None

    async def __call__(self, scope: dict[str, Any], receive, send):
        """Middleware entry point."""
        # When middleware is modifying the scope, it should make a copy of the
        # scope object before mutating it.
        # See https://asgi.readthedocs.io/en/latest/specs/main.html#middleware
        scope = dict(scope)

        token_key = self.get_token_header(scope["headers"])

        if token_key is None:
            # No token was in the request, nothing needs to be done
            scope["token"] = None
            return await self.app(scope, receive, send)

        token = await database_sync_to_async(Token.objects.get_token_or_none)(
            token_key
        )

        if token is not None:
            token.last_seen_at = timezone.now()
            await database_sync_to_async(token.save)()

        scope["token"] = token

        return await self.app(scope, receive, send)
