import secrets
from enum import StrEnum

from django.utils.functional import SimpleLazyObject, empty
from django.utils.html import format_html

# Template context key for the CSP nonce.
CONTEXT_KEY = "csp_nonce"


class CSP(StrEnum):
    """
    Content Security Policy constants for directive values and special tokens.

    These constants represent:
    1. Standard quoted string values from the CSP spec (e.g., 'self',
       'unsafe-inline')
    2. Special placeholder tokens (NONCE) that get replaced by the middleware

    Using this enum instead of raw strings provides better type checking,
    autocompletion, and protection against common mistakes like:

    - Typos (e.g., 'noone' instead of 'none')
    - Missing quotes (e.g., ["self"] instead of ["'self'"])
    - Inconsistent quote styles (e.g., ["'self'", "\"unsafe-inline\""])

    Example usage in Django settings:

        SECURE_CSP = {
            "default-src": [CSP.NONE],
            "script-src": [CSP.SELF, CSP.NONCE],
        }

    """

    # HTTP Headers.
    HEADER_ENFORCE = "Content-Security-Policy"
    HEADER_REPORT_ONLY = "Content-Security-Policy-Report-Only"

    # Standard CSP directive values.
    NONE = "'none'"
    REPORT_SAMPLE = "'report-sample'"
    SELF = "'self'"
    STRICT_DYNAMIC = "'strict-dynamic'"
    UNSAFE_EVAL = "'unsafe-eval'"
    UNSAFE_HASHES = "'unsafe-hashes'"
    UNSAFE_INLINE = "'unsafe-inline'"
    WASM_UNSAFE_EVAL = "'wasm-unsafe-eval'"

    # Special placeholder that gets replaced by the middleware.
    # The value itself is arbitrary and should not be mistaken for a real
    # nonce.
    NONCE = "<CSP_NONCE_SENTINEL>"


class LazyNonce(SimpleLazyObject):
    """
    Lazily generates a cryptographically secure nonce string, for use in CSP
    headers.

    The nonce is only generated when first accessed (e.g., via string
    interpolation or inside a template).

    The nonce will evaluate as `True` if it has been generated, and `False` if
    it has not. This is useful for third-party Django libraries that want to
    support CSP without requiring it.

    Example Django template usage with context processors enabled:

        <script {% csp_nonce_attr %}></script>

    ``{% csp_nonce_attr %}`` will only render the nonce attribute if the nonce
    has been evaluated (i.e. accessed) elsewhere in the request/response cycle.

    """

    def __init__(self):
        super().__init__(generate_nonce)

    def __bool__(self):
        return self._wrapped is not empty


def nonce_attr(context, media=None):
    nonce = context.get(CONTEXT_KEY)
    if media:
        return media.render(attrs={"nonce": nonce} if nonce is not None else None)
    if nonce is None:
        return ""
    return format_html('nonce="{}"', nonce)


def generate_nonce():
    return secrets.token_urlsafe(16)


def build_policy(config, nonce=None):
    policy = []

    for directive, values in config.items():
        if values in (None, False):
            continue

        if values is True:
            rendered_value = ""
        else:
            if isinstance(values, set):
                # Sort values for consistency, preventing cache invalidation
                # between requests and ensuring reliable browser caching.
                values = sorted(values)
            elif not isinstance(values, list | tuple):
                values = [values]

            # Replace the nonce sentinel with the actual nonce values, if the
            # sentinel is found and a nonce is provided. Otherwise, remove it.
            if (has_sentinel := CSP.NONCE in values) and nonce:
                values = [f"'nonce-{nonce}'" if v == CSP.NONCE else v for v in values]
            elif has_sentinel:
                values = [v for v in values if v != CSP.NONCE]

            if not values:
                continue

            rendered_value = " ".join(values)

        policy.append(f"{directive} {rendered_value}".rstrip())

    return "; ".join(policy)
