"""Authorizing download view for user-uploaded media.

Replaces unauthenticated static serving of MEDIA_ROOT. Every request resolves the
file's owning row, derives that row's organization, and refuses callers outside
it — closing the gap where a leaked or guessed filename exposed another tenant's
HR contracts, bank statements and reimbursement receipts.

Fails closed by design. A file whose owning row cannot be found, or whose
organization cannot be derived, is denied rather than served: an unreferenced
file in MEDIA_ROOT has no owner to authorize against, so there is no safe way to
release it. That means orphaned files 404 — deliberately.

Resolution is by exact FileField value. Every model with a FileField is indexed
once at first use (33 models, 40 fields today), then each lookup is a single
indexed query per candidate field until a row matches.
"""

import posixpath
import unicodedata

from django.apps import apps
from django.db import models
from django.http import FileResponse, Http404, HttpResponseForbidden
from django.views.decorators.http import require_safe

from apps.core.tenancy import get_active_organization, organization_of


def _file_fields():
    """[(model, field_name)] for every FileField/ImageField in the project.

    Built once per process. Cheap: reads the already-loaded app registry.
    """
    global _FILE_FIELD_CACHE
    try:
        return _FILE_FIELD_CACHE
    except NameError:
        pass
    pairs = []
    for model in apps.get_models():
        for field in model._meta.get_fields():
            if isinstance(field, models.FileField):
                pairs.append((model, field.name))
    _FILE_FIELD_CACHE = pairs
    return pairs


def _normalize(path):
    """Reject traversal and normalize to the stored FileField form.

    Django stores names with forward slashes relative to MEDIA_ROOT. Anything
    that escapes that root, or is absolute, is refused before it reaches the DB.
    """
    # NFC: macOS writes decomposed filenames, so an upload made there is stored
    # decomposed while a browser may request the composed form (or vice versa).
    path = unicodedata.normalize("NFC", path).lstrip("/")
    normalized = posixpath.normpath(path)
    if normalized.startswith("..") or normalized.startswith("/") or ".." in normalized.split("/"):
        raise Http404("Invalid media path.")
    return normalized


def _find_owner(name):
    """(instance, field_name) owning the stored file `name`, or (None, None)."""
    for model, field_name in _file_fields():
        try:
            obj = model._default_manager.filter(**{field_name: name}).first()
        except Exception:
            # A model whose manager needs context (or an unmigrated table) must
            # not take down every media request.
            continue
        if obj is not None:
            return obj, field_name
    return None, None


@require_safe
def serve_media(request, path):
    """Serve MEDIA_ROOT/<path> only to a caller inside the owning organization."""
    if not request.user.is_authenticated:
        # 404 rather than 401: a 401 confirms the path exists, which is itself a
        # disclosure when the filename is the only secret.
        raise Http404("Not found.")

    name = _normalize(path)
    obj, field_name = _find_owner(name)
    if obj is None:
        raise Http404("Not found.")

    owner_org = organization_of(obj)
    if owner_org is None:
        # Cannot prove ownership -> cannot authorize. Superusers are allowed
        # through so platform staff can still retrieve un-owned files.
        if not request.user.is_superuser:
            raise Http404("Not found.")
    else:
        caller_org = get_active_organization(request)
        if owner_org.id != caller_org.id and not request.user.is_superuser:
            return HttpResponseForbidden("Outside your organization.")

    file_field = getattr(obj, field_name)
    try:
        handle = file_field.open("rb")
    except (FileNotFoundError, ValueError):
        raise Http404("Not found.")

    response = FileResponse(handle, filename=posixpath.basename(name))
    # Private: this URL is authorized per-caller, so a shared cache holding the
    # body would serve one tenant's file to the next requester.
    response["Cache-Control"] = "private, max-age=0, no-store"
    response["X-Content-Type-Options"] = "nosniff"
    return response
