import io
import os
from datetime import timedelta

from django.core.files.base import ContentFile
from django.core.files.storage import default_storage
from django.http import HttpResponse
from django.urls import path
from django.db.models import Q
from django.conf import settings
from django.utils.http import http_date
from django.utils.timezone import now
from drf_yasg.utils import swagger_auto_schema
from PIL import Image
from rest_framework import status
from rest_framework.decorators import api_view, permission_classes, authentication_classes
from rest_framework.permissions import IsAuthenticated
from rest_framework.response import Response

from authentication.signature_auth import SignatureAuthentication
from files.models import File

THUMBNAIL_SIZES = (32, 64, 256)


def _accessible_files(request):
    # Shared by media_urls and thumbnail_urls so both endpoints always agree on who can see
    # what - a file is visible if the requester is friends-or-self with whatever currently
    # references it (an inventory item, a profile picture) or it's their own staged photo.
    return File.objects.filter(
        Q(connected_items__owner__in=request.user.friends_or_self()) |
        Q(profile_picture_users__in=request.user.friends_or_self()) |
        Q(staged_by_workflows__owner__in=request.user.user.all())
    ).distinct()


def _cache_headers(etag):
    # Content is addressed by its own hash and can never change under a given URL, so caches
    # (and the conditional-GET checks in both views below) can treat it as immutable forever.
    return {
        'ETag': etag,
        'Cache-Control': 'max-age=31536000, private, immutable',
        'Expires': http_date((now() + timedelta(days=365)).timestamp()),
    }


@swagger_auto_schema(method='GET', auto_schema=None)
@api_view(['GET'])
@permission_classes([IsAuthenticated])
@authentication_classes([SignatureAuthentication])
def media_urls(request, hash_path):
    # Note: CORS headers are NOT set here - django-cors-headers (CorsMiddleware,
    # configured in settings.py) adds them to every Django response automatically, so
    # setting them manually on these responses would just be redundant. The one exception
    # is the SERVE_X_ACCEL_REDIRECT path: nginx replaces this response entirely when it
    # follows the X-Accel-Redirect and serves the file itself, so the CORS header for that
    # case has to be configured in nginx's `location /redirect_media/` block instead.
    #
    # Looked up by the derived storage path (not by raw hash) because FileSerializer.name
    # (files/serializers.py) - used everywhere a file URL is handed to the frontend, e.g.
    # AuthenticatedImage's `src` - already returns this path via Django's FileField.url, and
    # the existing test suite (files/tests.py MediaUrlTestCase) exercises it this way too.
    try:
        file = _accessible_files(request).get(file=hash_path)

        # The access-control lookup above must happen before this check - otherwise a bare
        # hash + If-None-Match would let anyone probe "does a file with this hash exist" for
        # files they can't actually see.
        if request.META.get('HTTP_IF_NONE_MATCH') == file.hash:
            return HttpResponse(status=status.HTTP_304_NOT_MODIFIED)

        cache_headers = _cache_headers(file.hash)

        if settings.SERVE_X_ACCEL_REDIRECT:
            return HttpResponse(status=status.HTTP_200_OK,
                                content_type=file.mime_type,
                                headers={
                                    'X-Accel-Redirect': f'/redirect_media/{hash_path}',
                                    **cache_headers,
                                })
        else:
            # Read via the FieldFile itself (works against whatever storage backend is
            # actually configured) rather than assuming file.file.path is a real filesystem
            # path - the test suite swaps in an in-memory backend where that isn't true.
            with file.file.open('rb') as fh:
                content = fh.read()
            return HttpResponse(status=status.HTTP_200_OK,
                                content_type=file.mime_type,
                                headers=cache_headers,
                                content=content)

    except File.DoesNotExist:
        return Response(status=status.HTTP_404_NOT_FOUND)


def _thumbnail_rel_path(file_hash, size):
    # Mirrors files/models.py's hash_upload() sharding, under its own `thumbnails/<size>/`
    # subtree - reachable through the same nginx `/redirect_media/` alias as originals when
    # served from real disk, no separate nginx location needed.
    return os.path.join('thumbnails', str(size), file_hash[:2], file_hash[2:4], file_hash[4:6],
                         file_hash[6:] + '.jpg')


@swagger_auto_schema(method='GET', auto_schema=None)
@api_view(['GET'])
@permission_classes([IsAuthenticated])
@authentication_classes([SignatureAuthentication])
def thumbnail_urls(request, size, hash_path):
    if size not in THUMBNAIL_SIZES:
        return Response(status=status.HTTP_404_NOT_FOUND)

    try:
        file = _accessible_files(request).get(file=hash_path)

        etag = f'{file.hash}_{size}'
        if request.META.get('HTTP_IF_NONE_MATCH') == etag:
            return HttpResponse(status=status.HTTP_304_NOT_MODIFIED)

        # Read/write through the default storage backend, same as File.file itself, rather
        # than a hand-rolled filesystem path - correct regardless of storage backend (real
        # disk in production, in-memory under the test runner) and keeps the cache in the
        # same place originals live.
        rel_path = _thumbnail_rel_path(file.hash, size)
        if not default_storage.exists(rel_path):
            # Thumbnails are always re-encoded as JPEG regardless of the original format -
            # smaller and simpler than preserving e.g. PNG transparency at this scale.
            with file.file.open('rb') as fh:
                image = Image.open(fh)
                image.thumbnail((size, size))
                # Flatten through RGBA before dropping to RGB - some modes (grayscale+alpha,
                # palette-with-transparency, RGBA) store meaningless color/luminance data under
                # fully transparent pixels (often zeroed out, i.e. black). Converting straight
                # to RGB reveals that instead of "nothing there"; compositing onto an opaque
                # background first shows what the image is actually supposed to look like.
                rgba = image.convert('RGBA')
                flattened = Image.new('RGB', rgba.size, (255, 255, 255))
                flattened.paste(rgba, mask=rgba.getchannel('A'))
                buffer = io.BytesIO()
                flattened.save(buffer, 'JPEG', quality=90)
            default_storage.save(rel_path, ContentFile(buffer.getvalue()))

        cache_headers = _cache_headers(etag)

        if settings.SERVE_X_ACCEL_REDIRECT:
            return HttpResponse(status=status.HTTP_200_OK,
                                content_type='image/jpeg',
                                headers={
                                    'X-Accel-Redirect': f'/redirect_media/{rel_path}',
                                    **cache_headers,
                                })
        else:
            with default_storage.open(rel_path, 'rb') as fh:
                content = fh.read()
            return HttpResponse(status=status.HTTP_200_OK,
                                content_type='image/jpeg',
                                headers=cache_headers,
                                content=content)

    except File.DoesNotExist:
        return Response(status=status.HTTP_404_NOT_FOUND)


urlpatterns = [
    path('<int:size>/<path:hash_path>/', thumbnail_urls),
    path('<path:hash_path>', media_urls),
]
