from io import BytesIO

from django.http import HttpResponse
from django.shortcuts import get_object_or_404
from PIL import Image as PillowImage
from rest_framework import parsers, status, viewsets
from rest_framework.decorators import action
from rest_framework.permissions import BasePermission, SAFE_METHODS
from rest_framework.response import Response
from rest_framework.views import APIView

# Reuse the same permission used by ChronographAnalysis (anonymous-friendly)
from apps.tools.permissions import IsOwnerOrUnclaimed


class IsGroupPhotoOwner(BasePermission):
    """
    For GroupPhoto objects, ownership is via photo.uploaded_by.
    Read is open; mutations require being the uploader (or anonymous upload).
    """
    def has_permission(self, request, view):
        return True

    def has_object_permission(self, request, view, obj):
        if request.method in SAFE_METHODS:
            return True
        uploader = obj.photo.uploaded_by if obj.photo_id else None
        if uploader is None:
            return True
        return request.user.is_authenticated and uploader == request.user

from .analysis import compute_group_size as _compute_group_size
from .models import GroupPhoto, GroupPhotoAnalysis, Photo, PointOfImpact
from .serializers import (
    GroupPhotoAnalysisSerializer,
    GroupPhotoSerializer,
    PhotoMetaSerializer,
    PointOfImpactSerializer,
)

# Allowed image MIME types
_ALLOWED_CONTENT_TYPES = {'image/jpeg', 'image/png', 'image/webp', 'image/gif'}


class PhotoUploadView(APIView):
    """
    POST multipart/form-data with a 'file' field → create a Photo.

    Optional fields: description (string).
    Returns PhotoMetaSerializer data (no binary).
    Anonymous uploads are allowed (same pattern as ChronographAnalysis).
    """
    parser_classes = [parsers.MultiPartParser]
    permission_classes = [IsOwnerOrUnclaimed]

    def post(self, request):
        f = request.FILES.get('file')
        if not f:
            return Response({'detail': 'No file provided.'}, status=status.HTTP_400_BAD_REQUEST)

        if f.content_type not in _ALLOWED_CONTENT_TYPES:
            return Response(
                {'detail': f'Unsupported file type: {f.content_type}. '
                           f'Allowed: {", ".join(sorted(_ALLOWED_CONTENT_TYPES))}'},
                status=status.HTTP_400_BAD_REQUEST,
            )

        raw = f.read()
        try:
            img = PillowImage.open(BytesIO(raw))
            img.verify()  # check it is a valid image
            img = PillowImage.open(BytesIO(raw))  # re-open after verify (verify closes the stream)
            width, height = img.size
        except Exception:
            return Response({'detail': 'Invalid or corrupt image file.'}, status=status.HTTP_400_BAD_REQUEST)

        photo = Photo.objects.create(
            data=raw,
            content_type=f.content_type,
            size=len(raw),
            width=width,
            height=height,
            uploaded_by=request.user if request.user.is_authenticated else None,
            description=request.data.get('description', ''),
        )
        return Response(PhotoMetaSerializer(photo).data, status=status.HTTP_201_CREATED)


def photo_data_view(request, pk):
    """
    GET /api/photos/{pk}/data/  →  serve the raw image bytes.
    Public — no authentication required.
    """
    photo = get_object_or_404(Photo, pk=pk)
    return HttpResponse(bytes(photo.data), content_type=photo.content_type)


class GroupPhotoViewSet(viewsets.ModelViewSet):
    """
    CRUD for GroupPhoto objects.

    Filter by ?shot_group=<id> to list photos for a specific shot group.

    Nested sub-resources:
      GET/PUT/PATCH  .../analysis/           upsert GroupPhotoAnalysis
      GET/POST       .../points/             list / add PointOfImpact
      PATCH/DELETE   .../points/{poi_pk}/    update / remove a PointOfImpact
    """
    serializer_class = GroupPhotoSerializer
    permission_classes = [IsGroupPhotoOwner]

    def get_queryset(self):
        qs = GroupPhoto.objects.select_related('photo', 'analysis').prefetch_related('points_of_impact')
        shot_group = self.request.query_params.get('shot_group')
        if shot_group:
            qs = qs.filter(shot_group_id=shot_group)
        return qs

    # ── Analysis ──────────────────────────────────────────────────────────────

    @action(detail=True, methods=['get', 'put', 'patch'], url_path='analysis')
    def analysis(self, request, pk=None):
        group_photo = self.get_object()

        if request.method == 'GET':
            try:
                serializer = GroupPhotoAnalysisSerializer(group_photo.analysis)
                return Response(serializer.data)
            except GroupPhotoAnalysis.DoesNotExist:
                return Response({}, status=status.HTTP_200_OK)

        # PUT / PATCH — upsert
        partial = request.method == 'PATCH'
        try:
            instance = group_photo.analysis
        except GroupPhotoAnalysis.DoesNotExist:
            instance = None

        serializer = GroupPhotoAnalysisSerializer(
            instance,
            data=request.data,
            partial=partial,
        )
        serializer.is_valid(raise_exception=True)
        serializer.save(group_photo=group_photo)
        return Response(serializer.data)

    # ── Points of impact ──────────────────────────────────────────────────────

    @action(detail=True, methods=['get', 'post'], url_path='points')
    def points(self, request, pk=None):
        group_photo = self.get_object()

        if request.method == 'GET':
            pois = group_photo.points_of_impact.all()
            return Response(PointOfImpactSerializer(pois, many=True).data)

        serializer = PointOfImpactSerializer(data=request.data)
        serializer.is_valid(raise_exception=True)
        poi = serializer.save(group_photo=group_photo)
        return Response(PointOfImpactSerializer(poi).data, status=status.HTTP_201_CREATED)

    @action(
        detail=True,
        methods=['patch', 'delete'],
        url_path=r'points/(?P<poi_pk>[^/.]+)',
    )
    def point_detail(self, request, pk=None, poi_pk=None):
        group_photo = self.get_object()
        poi = get_object_or_404(PointOfImpact, pk=poi_pk, group_photo=group_photo)

        if request.method == 'DELETE':
            poi.delete()
            return Response(status=status.HTTP_204_NO_CONTENT)

        serializer = PointOfImpactSerializer(poi, data=request.data, partial=True)
        serializer.is_valid(raise_exception=True)
        serializer.save()
        return Response(serializer.data)

    # ── Group size computation ─────────────────────────────────────────────────

    @action(detail=True, methods=['post'], url_path='compute-group-size')
    def compute_group_size(self, request, pk=None):
        """
        Compute GroupPhotoAnalysis metrics from PointOfImpact real-world
        coordinates (x_mm, y_mm). Requires ≥ 2 POIs with mm coordinates set.
        Upserts the GroupPhotoAnalysis and returns the updated record.
        """
        group_photo = self.get_object()

        pois = list(
            group_photo.points_of_impact
            .filter(x_mm__isnull=False, y_mm__isnull=False)
            .values_list('x_mm', 'y_mm')
        )

        if len(pois) < 2:
            return Response(
                {'detail': 'At least 2 points of impact with real-world '
                           'coordinates (x_mm, y_mm) are required.'},
                status=status.HTTP_400_BAD_REQUEST,
            )

        distance_m = None
        if group_photo.shot_group_id and group_photo.shot_group.distance_m:
            distance_m = float(group_photo.shot_group.distance_m)

        metrics = _compute_group_size(
            [(float(x), float(y)) for x, y in pois],
            distance_m=distance_m,
        )

        try:
            instance = group_photo.analysis
        except GroupPhotoAnalysis.DoesNotExist:
            instance = None

        serializer = GroupPhotoAnalysisSerializer(
            instance,
            data=metrics,
            partial=bool(instance),
        )
        serializer.is_valid(raise_exception=True)
        serializer.save(group_photo=group_photo)
        return Response(serializer.data)
