from __future__ import annotations

from collections.abc import Awaitable, Callable

from asgiref.sync import iscoroutinefunction, markcoroutinefunction
from django.conf import settings
from django.core.exceptions import ImproperlyConfigured
from django.core.signals import setting_changed
from django.dispatch import receiver
from django.http import HttpRequest
from django.http.response import HttpResponseBase
from django.utils.functional import cached_property

_FEATURE_NAMES: set[str] = {
    # Chrome features (99)
    # https://github.com/chromium/chromium/raw/refs/heads/main/services/network/public/cpp/permissions_policy/permissions_policy_features.json5
    "accelerometer",
    "ambient-light-sensor",
    "aria-notify",
    "attribution-reporting",
    "autofill",
    "autoplay",
    "bluetooth",
    "browsing-topics",
    "camera",
    "captured-surface-control",
    "ch-device-memory",
    "ch-downlink",
    "ch-dpr",
    "ch-ect",
    "ch-prefers-color-scheme",
    "ch-prefers-reduced-motion",
    "ch-prefers-reduced-transparency",
    "ch-rtt",
    "ch-save-data",
    "ch-ua",
    "ch-ua-arch",
    "ch-ua-bitness",
    "ch-ua-form-factors",
    "ch-ua-full-version",
    "ch-ua-full-version-list",
    "ch-ua-high-entropy-values",
    "ch-ua-mobile",
    "ch-ua-model",
    "ch-ua-platform",
    "ch-ua-platform-version",
    "ch-ua-wow64",
    "ch-viewport-height",
    "ch-viewport-width",
    "ch-width",
    "clipboard-read",
    "clipboard-write",
    "compute-pressure",
    "cross-origin-isolated",
    "deferred-fetch",
    "deferred-fetch-minimal",
    "device-attributes",
    "digital-credentials-create",
    "digital-credentials-get",
    "display-capture",
    "encrypted-media",
    "execution-while-not-rendered",
    "execution-while-out-of-viewport",
    "fenced-unpartitioned-storage-read",
    "focus-without-user-activation",
    "fullscreen",
    "gamepad",
    "geolocation",
    "gyroscope",
    "hid",
    "identity-credentials-get",
    "idle-detection",
    "interest-cohort",
    "join-ad-interest-group",
    "keyboard-map",
    "language-detector",
    "language-model",
    "local-fonts",
    "local-network",
    "local-network-access",
    "loopback-network",
    "magnetometer",
    "manual-text",
    "media-playback-while-not-visible",
    "microphone",
    "midi",
    "on-device-speech-recognition",
    "otp-credentials",
    "payment",
    "picture-in-picture",
    "private-aggregation",
    "private-state-token-issuance",
    "private-state-token-redemption",
    "publickey-credentials-create",
    "publickey-credentials-get",
    "record-ad-auction-events",
    "rewriter",
    "run-ad-auction",
    "screen-wake-lock",
    "serial",
    "shared-storage",
    "shared-storage-select-url",
    "speaker-selection",
    "storage-access",
    "summarizer",
    "sync-xhr",
    "translator",
    "unload",
    "usb",
    "vertical-scroll",
    "web-app-installation",
    "web-share",
    "window-management",
    "writer",
    "xr-spatial-tracking",
    # Firefox-only features (2)
    # https://github.com/mozilla/gecko-dev/raw/refs/heads/master/dom/security/featurepolicy/FeaturePolicyUtils.cpp
    "document-domain",
    "vr",
}


class PermissionsPolicyMiddleware:
    sync_capable = True
    async_capable = True

    def __init__(
        self,
        get_response: (
            Callable[[HttpRequest], HttpResponseBase]
            | Callable[[HttpRequest], Awaitable[HttpResponseBase]]
        ),
    ) -> None:
        self.get_response = get_response
        self.async_mode = iscoroutinefunction(self.get_response)

        if self.async_mode:
            # Mark the class as async-capable, but do the actual switch
            # inside __call__ to avoid swapping out dunder methods
            markcoroutinefunction(self)

        self.permissions_policy  # noqa: B018 - Access at setup so ImproperlyConfigured can be raised
        self.permissions_policy_report_only  # noqa: B018 - Access at setup so ImproperlyConfigured can be raised
        receiver(setting_changed)(self.clear_header_value)

    def __call__(
        self, request: HttpRequest
    ) -> HttpResponseBase | Awaitable[HttpResponseBase]:
        if self.async_mode:
            return self.__acall__(request)
        response = self.get_response(request)
        assert isinstance(response, HttpResponseBase)  # type narrow

        if permissions_policy := self.permissions_policy:
            response["Permissions-Policy"] = permissions_policy
        if permissions_policy_report_only := self.permissions_policy_report_only:
            response["Permissions-Policy-Report-Only"] = permissions_policy_report_only
        return response

    async def __acall__(self, request: HttpRequest) -> HttpResponseBase:
        result = self.get_response(request)
        assert not isinstance(result, HttpResponseBase)  # type narrow
        response = await result
        if permissions_policy := self.permissions_policy:
            response["Permissions-Policy"] = permissions_policy
        if permissions_policy_report_only := self.permissions_policy_report_only:
            response["Permissions-Policy-Report-Only"] = permissions_policy_report_only
        return response

    @cached_property
    def permissions_policy(self) -> str:
        return self.compute_header_value(getattr(settings, "PERMISSIONS_POLICY", {}))

    @cached_property
    def permissions_policy_report_only(self) -> str:
        return self.compute_header_value(
            getattr(settings, "PERMISSIONS_POLICY_REPORT_ONLY", {})
        )

    def compute_header_value(
        self, setting: dict[str, str | list[str] | tuple[str]]
    ) -> str:
        pieces = []
        for feature, values in sorted(setting.items()):
            if feature not in _FEATURE_NAMES:
                raise ImproperlyConfigured(f"Unknown feature {feature}")
            if isinstance(values, str):
                values = (values,)

            item = []
            for value in values:
                if value == "none":
                    # 'none' was previously supported as a special token for
                    # Feature-Policy, now can be represented by the empty list.
                    pass
                elif value in ("self", "*"):
                    item.append(value)
                else:
                    item.append(f'"{value}"')  # noqa: B028
            pieces.append(feature + "=(" + " ".join(item) + ")")
        return ", ".join(pieces)

    def clear_header_value(self, setting: str, **kwargs: object) -> None:
        if setting == "PERMISSIONS_POLICY":
            try:
                del self.permissions_policy
            except AttributeError:
                pass
        elif setting == "PERMISSIONS_POLICY_REPORT_ONLY":
            try:
                del self.permissions_policy_report_only
            except AttributeError:
                pass
