from django.conf import settings
from django.db import transaction
from django.http import FileResponse, Http404
from django.utils import timezone
import django_filters
from drf_spectacular.types import OpenApiTypes
from drf_spectacular.utils import OpenApiResponse, extend_schema, extend_schema_view, inline_serializer
from rest_framework import status
from rest_framework.decorators import action
from rest_framework.exceptions import ValidationError
from rest_framework.parsers import FormParser, MultiPartParser
from rest_framework.response import Response
from rest_framework.serializers import CharField, IntegerField, Serializer
from rest_framework.views import APIView
from rest_framework.viewsets import ModelViewSet, ReadOnlyModelViewSet

from apps.muestreo_laboratorio.models import FotoMuestra, FotoResultado, ParametroResultado, ResultadoLaboratorio, TomaMuestra
from apps.muestreo_laboratorio.permissions import CanAccessMuestreoModule, CanLabWork, CanTakeSample
from apps.muestreo_laboratorio.serializers import (
    AdjuntarInformeSerializer,
    FotoMuestraCreateSerializer,
    FotoResultadoCreateSerializer,
    ParametroResultadoWriteSerializer,
    ResultadoLaboratorioCreateSerializer,
    ResultadoLaboratorioSerializer,
    ResultadoLaboratorioUpdateSerializer,
    TomaMuestraSerializer,
    TomaMuestraUpsertSerializer,
)
from apps.muestreo_laboratorio.utils import compute_tags_and_alertas
from apps.recepciones.models import Muestreo
from apps.usuarios.permissions import IsAdmin
from utils.audit import AuditFieldsViewSetMixin
from utils.office import extract_office_id
from utils.pagination import StandardResultsSetPagination


ErrorDetailSerializer = inline_serializer(
    name="ErrorDetail",
    fields={
        "detail": CharField(),
    },
)

FotoCreatedSerializer = inline_serializer(
    name="FotoCreated",
    fields={
        "foto": inline_serializer(
            name="FotoCreatedItem",
            fields={
                "id": IntegerField(),
                "tipo": CharField(),
            },
        ),
    },
)

class ParametroUpsertItemSchemaSerializer(Serializer):
    id = IntegerField()
    parametro = IntegerField()

class ParametrosUpsertResponseSchemaSerializer(Serializer):
    parametros = ParametroUpsertItemSchemaSerializer(many=True)


class OrdenMuestreoFilter(django_filters.FilterSet):
    created_date = django_filters.DateFilter(field_name="created_at", lookup_expr="date")
    created_date_gte = django_filters.DateFilter(field_name="created_at", lookup_expr="date__gte")
    created_date_lte = django_filters.DateFilter(field_name="created_at", lookup_expr="date__lte")

    class Meta:
        model = Muestreo
        fields = {
            "estado": ["exact"],
            "tipo_muestreo": ["exact"],
            "tecnico_asignado": ["exact"],
            "recepcion": ["exact"],
            "recepcion__boleta__zafra": ["exact"],
            "recepcion__boleta__productor": ["exact"],
            "recepcion__boleta__contrato": ["exact"],
        }


@extend_schema_view(
    list=extend_schema(tags=["Muestreo laboratorio"], summary="Listar órdenes de muestreo"),
    retrieve=extend_schema(tags=["Muestreo laboratorio"], summary="Detalle de orden de muestreo"),
)
class OrdenMuestreoViewSet(ReadOnlyModelViewSet):
    permission_classes = [CanAccessMuestreoModule]
    pagination_class = StandardResultsSetPagination
    filterset_class = OrdenMuestreoFilter
    search_fields = [
        "recepcion__boleta__folio",
        "recepcion__boleta__productor__nombre",
        "recepcion__boleta__contrato__numero_contrato",
    ]

    queryset = (
        Muestreo.objects.select_related(
            "recepcion",
            "recepcion__boleta",
            "recepcion__bodega_destino",
            "recepcion__boleta__productor",
            "recepcion__boleta__contrato",
            "recepcion__boleta__variedad",
            "recepcion__boleta__zafra",
            "tecnico_asignado",
        )
        .all()
        .order_by("-id")
    )

    def get_queryset(self):
        qs = super().get_queryset()

        if self.action == "retrieve":
            return (
                qs.select_related(
                    "toma_muestra",
                    "resultado_laboratorio",
                )
                .prefetch_related(
                    "recepcion__pesos",
                    "recepcion__fotos",
                    "recepcion__deducciones__parametro",
                    "toma_muestra__fotos",
                    "resultado_laboratorio__parametros__parametro",
                    "resultado_laboratorio__fotos",
                )
            )

        return qs

    def get_serializer_class(self):
        from apps.muestreo_laboratorio.serializers import OrdenMuestreoDetailSerializer, OrdenMuestreoListSerializer
        from apps.recepciones.serializers import MuestreoSerializer

        if self.action == "list":
            return OrdenMuestreoListSerializer
        if self.action == "retrieve":
            return OrdenMuestreoDetailSerializer
        return MuestreoSerializer

    def get_permissions(self):
        if self.action in ["tomar_muestra", "fotos"]:
            return [CanTakeSample()]

        if self.action in ["parametros", "publicar"]:
            return [CanLabWork()]
        return [p() for p in self.permission_classes]

    @extend_schema(
        tags=["Muestreo laboratorio"],
        summary="Registrar toma de muestra (campo)",
        request=TomaMuestraUpsertSerializer,
        responses={
            200: OpenApiResponse(response=TomaMuestraSerializer),
            400: OpenApiResponse(response=ErrorDetailSerializer, description="Solicitud inválida"),
            403: OpenApiResponse(description="Sin permisos"),
            404: OpenApiResponse(response=ErrorDetailSerializer, description="No encontrado"),
        },
    )
    @action(detail=True, methods=["post"], url_path="tomar-muestra")
    def tomar_muestra(self, request, pk=None):
        muestreo = self.get_object()
        self.check_object_permissions(request, muestreo)

        existing = getattr(muestreo, "toma_muestra", None)
        serializer = TomaMuestraUpsertSerializer(instance=existing, data=request.data, partial=True)
        serializer.is_valid(raise_exception=True)

        with transaction.atomic():
            if existing:
                toma = serializer.save(updated_by=request.user)
            else:
                toma = serializer.save(muestreo=muestreo, created_by=request.user, updated_by=request.user)

            if muestreo.estado == "pendiente":
                muestreo.estado = "tomado"
                muestreo.save(update_fields=["estado", "updated_at"])

        return Response(TomaMuestraSerializer(toma, context={"request": request}).data)

    @extend_schema(
        tags=["Muestreo laboratorio"],
        summary="Subir foto de toma de muestra",
        request=FotoMuestraCreateSerializer,
        responses={
            201: OpenApiResponse(response=FotoCreatedSerializer, description="Creado"),
            400: OpenApiResponse(response=ErrorDetailSerializer, description="Solicitud inválida"),
            403: OpenApiResponse(description="Sin permisos"),
            404: OpenApiResponse(response=ErrorDetailSerializer, description="No encontrado"),
        },
    )
    @action(
        detail=True,
        methods=["post"],
        url_path="fotos",
        parser_classes=[MultiPartParser, FormParser],
    )
    def fotos(self, request, pk=None):
        muestreo = self.get_object()
        self.check_object_permissions(request, muestreo)

        toma = getattr(muestreo, "toma_muestra", None)
        if not toma:
            return Response({"detail": "Falta toma de muestra."}, status=400)

        serializer = FotoMuestraCreateSerializer(data=request.data)
        serializer.is_valid(raise_exception=True)
        foto = serializer.save(toma_muestra=toma)

        return Response({"foto": {"id": foto.id, "tipo": foto.tipo}}, status=status.HTTP_201_CREATED)

    @extend_schema(
        tags=["Muestreo laboratorio"],
        summary="Capturar/actualizar parámetros (sin crear resultado explícitamente)",
        description=(
            "Upsert de parámetros por orden de muestreo. "
            "Si el resultado de laboratorio no existe, se crea automáticamente y el muestreo pasa a 'en_laboratorio'."
        ),
        request=ParametroResultadoWriteSerializer(many=True),
        responses={
            200: OpenApiResponse(
                response=inline_serializer(
                    name="OrdenParametrosUpsertResponse",
                    fields={
                        "resultado_id": IntegerField(),
                        "parametros": ParametroUpsertItemSchemaSerializer(many=True),
                    },
                )
            ),
            400: OpenApiResponse(response=ErrorDetailSerializer, description="Solicitud inválida"),
            403: OpenApiResponse(description="Sin permisos"),
            404: OpenApiResponse(response=ErrorDetailSerializer, description="No encontrado"),
        },
    )
    @action(detail=True, methods=["post"], url_path="parametros")
    def parametros(self, request, pk=None):
        muestreo = self.get_object()
        self.check_object_permissions(request, muestreo)

        office_id = extract_office_id(request)
        if office_id is None:
            raise ValidationError({"office_id": "office_id es requerido."})
        if muestreo.oficina_id is not None and muestreo.oficina_id != office_id:
            raise ValidationError({"detail": "La orden de muestreo no pertenece a la oficina indicada."})

        items = request.data
        if not isinstance(items, list):
            return Response({"detail": "Se espera una lista de parámetros."}, status=400)

        with transaction.atomic():
            resultado = getattr(muestreo, "resultado_laboratorio", None)
            if not resultado:
                resultado = ResultadoLaboratorio.objects.create(
                    muestreo=muestreo,
                    created_by=request.user,
                    updated_by=request.user,
                )

                if muestreo.estado in {"pendiente", "tomado"}:
                    muestreo.estado = "en_laboratorio"
                    muestreo.save(update_fields=["estado", "updated_at"])
            else:
                if resultado.updated_by_id != request.user.id:
                    ResultadoLaboratorio.objects.filter(pk=resultado.pk).update(updated_by=request.user)

            out = []
            for item in items:
                pr = (
                    ParametroResultado.objects.filter(
                        resultado_laboratorio=resultado,
                        parametro_id=item.get("parametro"),
                    )
                    .select_related("parametro")
                    .first()
                )

                if pr:
                    s = ParametroResultadoWriteSerializer(instance=pr, data=item, partial=True)
                    s.is_valid(raise_exception=True)
                    obj = s.save(updated_by=request.user)
                else:
                    s = ParametroResultadoWriteSerializer(data=item)
                    s.is_valid(raise_exception=True)
                    obj = s.save(
                        resultado_laboratorio=resultado,
                        created_by=request.user,
                        updated_by=request.user,
                    )

                out.append({"id": obj.id, "parametro": obj.parametro_id})

            tags, alertas = compute_tags_and_alertas(resultado.parametros.select_related("parametro").all())
            ResultadoLaboratorio.objects.filter(pk=resultado.pk).update(tags=tags, alertas=alertas)

        return Response({"resultado_id": resultado.id, "parametros": out})

    @extend_schema(
        tags=["Muestreo laboratorio"],
        summary="Publicar resultado de laboratorio (por orden)",
        description=(
            "Publica el resultado asociado a la orden. "
            "Requiere que exista un ResultadoLaboratorio (puede crearse automáticamente al capturar parámetros)."
        ),
        responses={
            200: OpenApiResponse(response=ResultadoLaboratorioSerializer),
            400: OpenApiResponse(response=ErrorDetailSerializer, description="No se puede publicar"),
            403: OpenApiResponse(description="Sin permisos"),
            404: OpenApiResponse(response=ErrorDetailSerializer, description="No encontrado"),
        },
    )
    @action(detail=True, methods=["post"], url_path="publicar")
    def publicar(self, request, pk=None):
        muestreo = self.get_object()
        self.check_object_permissions(request, muestreo)

        office_id = extract_office_id(request)
        if office_id is None:
            raise ValidationError({"office_id": "office_id es requerido."})
        if muestreo.oficina_id is not None and muestreo.oficina_id != office_id:
            raise ValidationError({"detail": "La orden de muestreo no pertenece a la oficina indicada."})

        resultado = getattr(muestreo, "resultado_laboratorio", None)
        if not resultado:
            return Response({"detail": "Falta resultado de laboratorio."}, status=400)

        if resultado.estado == ResultadoLaboratorio.ESTADO_PUBLICADO:
            return Response({"detail": "El resultado ya está publicado."}, status=400)

        required = getattr(settings, "MUESTREO_LAB_PARAMETROS_REQUERIDOS", [])
        if required:
            presentes = set(
                resultado.parametros.select_related("parametro").values_list("parametro_id", flat=True)
            )
            presentes_nombres = set(
                x.lower()
                for x in resultado.parametros.select_related("parametro").values_list("parametro__nombre", flat=True)
            )

            faltantes = []
            for r in required:
                if isinstance(r, int):
                    if r not in presentes:
                        faltantes.append(r)
                else:
                    rn = str(r).strip().lower()
                    if rn and rn not in presentes_nombres:
                        faltantes.append(r)

            if faltantes:
                return Response({"detail": "Faltan parámetros requeridos.", "faltantes": faltantes}, status=400)

        tags, alertas = compute_tags_and_alertas(resultado.parametros.select_related("parametro").all())

        with transaction.atomic():
            resultado.estado = ResultadoLaboratorio.ESTADO_PUBLICADO
            resultado.fecha_publicacion = timezone.now()
            resultado.publicado_por = request.user
            resultado.tags = tags
            resultado.alertas = alertas
            resultado.updated_by = request.user
            resultado.save(
                update_fields=[
                    "estado",
                    "fecha_publicacion",
                    "publicado_por",
                    "tags",
                    "alertas",
                    "updated_by",
                    "updated_at",
                ]
            )

            if resultado.muestreo.estado != "completado":
                resultado.muestreo.estado = "completado"
                resultado.muestreo.save(update_fields=["estado", "updated_at"])
                # Al completar el muestreo se avanza la recepción a muestreo_completado.
                from apps.recepciones.models import Recepcion
                recepcion = getattr(resultado.muestreo, "recepcion", None)
                if recepcion and recepcion.estado == Recepcion.ESTADO_MUESTREO_PENDIENTE:
                    recepcion.estado = Recepcion.ESTADO_MUESTREO_COMPLETADO
                    recepcion.updated_by = request.user
                    recepcion.save(update_fields=["estado", "updated_by", "updated_at"])

        return Response(ResultadoLaboratorioSerializer(resultado, context={"request": request}).data)


class ResultadoLaboratorioFilter(django_filters.FilterSet):
    created_date = django_filters.DateFilter(field_name="created_at", lookup_expr="date")
    created_date_gte = django_filters.DateFilter(field_name="created_at", lookup_expr="date__gte")
    created_date_lte = django_filters.DateFilter(field_name="created_at", lookup_expr="date__lte")

    class Meta:
        model = ResultadoLaboratorio
        fields = {
            "estado": ["exact"],
            "muestreo": ["exact"],
            "muestreo__recepcion": ["exact"],
            "muestreo__recepcion__boleta__zafra": ["exact"],
            "muestreo__recepcion__boleta__productor": ["exact"],
            "muestreo__recepcion__boleta__contrato": ["exact"],
        }


@extend_schema_view(
    list=extend_schema(tags=["Muestreo laboratorio"], summary="Listar resultados de laboratorio"),
    retrieve=extend_schema(tags=["Muestreo laboratorio"], summary="Detalle de resultado de laboratorio"),
    create=extend_schema(
        tags=["Muestreo laboratorio"],
        summary="Crear resultado de laboratorio",
        request=ResultadoLaboratorioCreateSerializer,
        responses={
            201: OpenApiResponse(response=ResultadoLaboratorioSerializer, description="Creado"),
            400: OpenApiResponse(response=ErrorDetailSerializer, description="Solicitud inválida"),
            403: OpenApiResponse(description="Sin permisos"),
        },
    ),
    update=extend_schema(
        tags=["Muestreo laboratorio"],
        summary="Actualizar resultado de laboratorio",
        request=ResultadoLaboratorioUpdateSerializer,
        responses={
            200: OpenApiResponse(response=ResultadoLaboratorioSerializer),
            400: OpenApiResponse(response=ErrorDetailSerializer, description="Solicitud inválida"),
            403: OpenApiResponse(description="Sin permisos"),
            404: OpenApiResponse(response=ErrorDetailSerializer, description="No encontrado"),
        },
    ),
    partial_update=extend_schema(
        tags=["Muestreo laboratorio"],
        summary="Actualizar parcialmente resultado de laboratorio",
        request=ResultadoLaboratorioUpdateSerializer,
        responses={
            200: OpenApiResponse(response=ResultadoLaboratorioSerializer),
            400: OpenApiResponse(response=ErrorDetailSerializer, description="Solicitud inválida"),
            403: OpenApiResponse(description="Sin permisos"),
            404: OpenApiResponse(response=ErrorDetailSerializer, description="No encontrado"),
        },
    ),
    destroy=extend_schema(tags=["Muestreo laboratorio"], summary="Eliminar resultado de laboratorio"),
)
class ResultadoLaboratorioViewSet(AuditFieldsViewSetMixin, ModelViewSet):
    permission_classes = [CanAccessMuestreoModule]
    pagination_class = StandardResultsSetPagination
    filterset_class = ResultadoLaboratorioFilter

    office_filter_field = "muestreo__oficina_id"

    queryset = (
        ResultadoLaboratorio.objects.select_related(
            "muestreo",
            "muestreo__recepcion",
            "muestreo__recepcion__boleta",
            "muestreo__recepcion__boleta__productor",
            "muestreo__recepcion__boleta__contrato",
            "muestreo__recepcion__boleta__variedad",
            "muestreo__recepcion__boleta__zafra",
            "publicado_por",
        )
        .prefetch_related("parametros__parametro", "fotos")
        .all()
        .order_by("-id")
    )

    def get_permissions(self):
        if self.action == "destroy":
            return [IsAdmin()]
        if self.action in [
            "create",
            "update",
            "partial_update",
            "adjuntar_informe",
            "fotos",
            "publicar",
            "parametros",
        ]:
            return [CanLabWork()]
        return [p() for p in self.permission_classes]

    def get_serializer_class(self):
        if self.action == "create":
            return ResultadoLaboratorioCreateSerializer
        if self.action in ["update", "partial_update"]:
            return ResultadoLaboratorioUpdateSerializer
        return ResultadoLaboratorioSerializer

    def create(self, request, *args, **kwargs):
        serializer = self.get_serializer(data=request.data)
        serializer.is_valid(raise_exception=True)
        self.perform_create(serializer)
        instance = serializer.instance
        out = ResultadoLaboratorioSerializer(instance, context={"request": request}).data
        headers = self.get_success_headers(serializer.data)
        return Response(out, status=status.HTTP_201_CREATED, headers=headers)

    def update(self, request, *args, **kwargs):
        partial = kwargs.pop("partial", False)
        instance = self.get_object()
        serializer = self.get_serializer(instance, data=request.data, partial=partial)
        serializer.is_valid(raise_exception=True)
        self.perform_update(serializer)
        instance.refresh_from_db()
        out = ResultadoLaboratorioSerializer(instance, context={"request": request}).data
        return Response(out)

    def perform_create(self, serializer):
        office_id = extract_office_id(self.request)
        if office_id is None:
            raise ValidationError({"office_id": "office_id es requerido."})

        muestreo_id = serializer.validated_data.get("muestreo_id")
        if muestreo_id is not None:
            if not Muestreo.objects.filter(id=muestreo_id, oficina_id=office_id).exists():
                raise ValidationError({"muestreo_id": "El muestreo no pertenece a la oficina indicada."})

        obj = serializer.save(created_by=self.request.user, updated_by=self.request.user)
        if obj.muestreo.estado in {"pendiente", "tomado"}:
            obj.muestreo.estado = "en_laboratorio"
            obj.muestreo.save(update_fields=["estado", "updated_at"])

    @extend_schema(
        tags=["Muestreo laboratorio"],
        summary="Adjuntar informe PDF al resultado",
        request=AdjuntarInformeSerializer,
        responses={
            200: OpenApiResponse(response=ResultadoLaboratorioSerializer),
            400: OpenApiResponse(response=ErrorDetailSerializer, description="Solicitud inválida"),
            403: OpenApiResponse(description="Sin permisos"),
            404: OpenApiResponse(response=ErrorDetailSerializer, description="No encontrado"),
        },
    )
    @action(
        detail=True,
        methods=["post"],
        url_path="adjuntar-informe",
        parser_classes=[MultiPartParser, FormParser],
    )
    def adjuntar_informe(self, request, pk=None):
        resultado = self.get_object()
        serializer = AdjuntarInformeSerializer(data=request.data)
        serializer.is_valid(raise_exception=True)

        resultado.informe = serializer.validated_data["informe"]
        resultado.updated_by = request.user
        resultado.save(update_fields=["informe", "updated_by", "updated_at"])

        return Response(ResultadoLaboratorioSerializer(resultado, context={"request": request}).data)

    @extend_schema(
        tags=["Muestreo laboratorio"],
        summary="Subir foto de resultado de laboratorio",
        request=FotoResultadoCreateSerializer,
        responses={
            201: OpenApiResponse(response=FotoCreatedSerializer, description="Creado"),
            400: OpenApiResponse(response=ErrorDetailSerializer, description="Solicitud inválida"),
            403: OpenApiResponse(description="Sin permisos"),
            404: OpenApiResponse(response=ErrorDetailSerializer, description="No encontrado"),
        },
    )
    @action(
        detail=True,
        methods=["post"],
        url_path="fotos",
        parser_classes=[MultiPartParser, FormParser],
    )
    def fotos(self, request, pk=None):
        resultado = self.get_object()
        serializer = FotoResultadoCreateSerializer(data=request.data)
        serializer.is_valid(raise_exception=True)
        foto = serializer.save(resultado_laboratorio=resultado)
        return Response({"foto": {"id": foto.id, "tipo": foto.tipo}}, status=status.HTTP_201_CREATED)

    @extend_schema(
        tags=["Muestreo laboratorio"],
        summary="Capturar/actualizar parámetros del resultado",
        request=ParametroResultadoWriteSerializer(many=True),
        responses={
            200: OpenApiResponse(response=ParametrosUpsertResponseSchemaSerializer),
            400: OpenApiResponse(response=ErrorDetailSerializer, description="Solicitud inválida"),
            403: OpenApiResponse(description="Sin permisos"),
            404: OpenApiResponse(response=ErrorDetailSerializer, description="No encontrado"),
        },
    )
    @action(detail=True, methods=["post"], url_path="parametros")
    def parametros(self, request, pk=None):
        resultado = self.get_object()

        items = request.data
        if not isinstance(items, list):
            return Response({"detail": "Se espera una lista de parámetros."}, status=400)

        out = []
        with transaction.atomic():
            for item in items:
                pr = ParametroResultado.objects.filter(
                    resultado_laboratorio=resultado, parametro_id=item.get("parametro")
                ).select_related("parametro").first()

                if pr:
                    s = ParametroResultadoWriteSerializer(instance=pr, data=item, partial=True)
                    s.is_valid(raise_exception=True)
                    obj = s.save(updated_by=request.user)
                else:
                    s = ParametroResultadoWriteSerializer(data=item)
                    s.is_valid(raise_exception=True)
                    obj = s.save(resultado_laboratorio=resultado, created_by=request.user, updated_by=request.user)
                out.append({"id": obj.id, "parametro": obj.parametro_id})

            tags, alertas = compute_tags_and_alertas(resultado.parametros.select_related("parametro").all())
            ResultadoLaboratorio.objects.filter(pk=resultado.pk).update(tags=tags, alertas=alertas)

        return Response({"parametros": out})

    @extend_schema(
        tags=["Muestreo laboratorio"],
        summary="Publicar resultado de laboratorio",
        description="Cambia el estado a publicado, valida parámetros requeridos (si aplica) y recalcula tags/alertas.",
        responses={
            200: OpenApiResponse(response=ResultadoLaboratorioSerializer),
            400: OpenApiResponse(response=ErrorDetailSerializer, description="No se puede publicar"),
            403: OpenApiResponse(description="Sin permisos"),
            404: OpenApiResponse(response=ErrorDetailSerializer, description="No encontrado"),
        },
    )
    @action(detail=True, methods=["post"], url_path="publicar")
    def publicar(self, request, pk=None):
        resultado = self.get_object()

        if resultado.estado == ResultadoLaboratorio.ESTADO_PUBLICADO:
            return Response({"detail": "El resultado ya está publicado."}, status=400)

        required = getattr(settings, "MUESTREO_LAB_PARAMETROS_REQUERIDOS", [])
        if required:
            presentes = set(
                resultado.parametros.select_related("parametro").values_list("parametro_id", flat=True)
            )
            presentes_nombres = set(
                x.lower()
                for x in resultado.parametros.select_related("parametro").values_list("parametro__nombre", flat=True)
            )

            faltantes = []
            for r in required:
                if isinstance(r, int):
                    if r not in presentes:
                        faltantes.append(r)
                else:
                    rn = str(r).strip().lower()
                    if rn and rn not in presentes_nombres:
                        faltantes.append(r)

            if faltantes:
                return Response({"detail": "Faltan parámetros requeridos.", "faltantes": faltantes}, status=400)

        tags, alertas = compute_tags_and_alertas(resultado.parametros.select_related("parametro").all())

        with transaction.atomic():
            resultado.estado = ResultadoLaboratorio.ESTADO_PUBLICADO
            resultado.fecha_publicacion = timezone.now()
            resultado.publicado_por = request.user
            resultado.tags = tags
            resultado.alertas = alertas
            resultado.updated_by = request.user
            resultado.save(
                update_fields=[
                    "estado",
                    "fecha_publicacion",
                    "publicado_por",
                    "tags",
                    "alertas",
                    "updated_by",
                    "updated_at",
                ]
            )

            if resultado.muestreo.estado != "completado":
                resultado.muestreo.estado = "completado"
                resultado.muestreo.save(update_fields=["estado", "updated_at"])
                # Al completar el muestreo se avanza la recepción a muestreo_completado.
                from apps.recepciones.models import Recepcion
                recepcion = getattr(resultado.muestreo, "recepcion", None)
                if recepcion and recepcion.estado == Recepcion.ESTADO_MUESTREO_PENDIENTE:
                    recepcion.estado = Recepcion.ESTADO_MUESTREO_COMPLETADO
                    recepcion.updated_by = request.user
                    recepcion.save(update_fields=["estado", "updated_by", "updated_at"])

        return Response(ResultadoLaboratorioSerializer(resultado, context={"request": request}).data)


class FotoMuestraDownloadView(APIView):
    permission_classes = [CanAccessMuestreoModule]

    @extend_schema(
        tags=["Muestreo laboratorio"],
        summary="Descargar foto de toma de muestra",
        description="Devuelve el archivo asociado a una foto (binario).",
        responses={
            200: OpenApiResponse(response=OpenApiTypes.BINARY, description="Archivo"),
            404: OpenApiResponse(response=ErrorDetailSerializer, description="No encontrado"),
        },
    )
    def get(self, request, pk: int):
        office_id = extract_office_id(request)
        if office_id is None:
            raise ValidationError({"office_id": "office_id es requerido."})

        try:
            foto = FotoMuestra.objects.select_related("toma_muestra", "toma_muestra__muestreo").get(pk=pk)
        except FotoMuestra.DoesNotExist as e:
            raise Http404() from e

        muestreo = getattr(getattr(foto, "toma_muestra", None), "muestreo", None)
        if getattr(muestreo, "oficina_id", None) != office_id:
            raise Http404()

        if not foto.imagen:
            raise Http404()

        resp = FileResponse(foto.imagen.open("rb"), content_type="application/octet-stream")
        resp["Content-Disposition"] = f"inline; filename=\"{foto.imagen.name.split('/')[-1]}\""
        return resp


class FotoResultadoDownloadView(APIView):
    permission_classes = [CanAccessMuestreoModule]

    @extend_schema(
        tags=["Muestreo laboratorio"],
        summary="Descargar foto de resultado de laboratorio",
        description="Devuelve el archivo asociado a una foto (binario).",
        responses={
            200: OpenApiResponse(response=OpenApiTypes.BINARY, description="Archivo"),
            404: OpenApiResponse(response=ErrorDetailSerializer, description="No encontrado"),
        },
    )
    def get(self, request, pk: int):
        office_id = extract_office_id(request)
        if office_id is None:
            raise ValidationError({"office_id": "office_id es requerido."})

        try:
            foto = FotoResultado.objects.select_related("resultado_laboratorio", "resultado_laboratorio__muestreo").get(
                pk=pk
            )
        except FotoResultado.DoesNotExist as e:
            raise Http404() from e

        muestreo = getattr(getattr(foto, "resultado_laboratorio", None), "muestreo", None)
        if getattr(muestreo, "oficina_id", None) != office_id:
            raise Http404()

        if not foto.imagen:
            raise Http404()

        resp = FileResponse(foto.imagen.open("rb"), content_type="application/octet-stream")
        resp["Content-Disposition"] = f"inline; filename=\"{foto.imagen.name.split('/')[-1]}\""
        return resp


class InformeResultadoDownloadView(APIView):
    permission_classes = [CanAccessMuestreoModule]

    @extend_schema(
        tags=["Muestreo laboratorio"],
        summary="Descargar informe PDF del resultado",
        description="Devuelve el informe PDF asociado al resultado (binario).",
        responses={
            200: OpenApiResponse(response=OpenApiTypes.BINARY, description="Archivo"),
            404: OpenApiResponse(response=ErrorDetailSerializer, description="No encontrado"),
        },
    )
    def get(self, request, pk: int):
        office_id = extract_office_id(request)
        if office_id is None:
            raise ValidationError({"office_id": "office_id es requerido."})

        try:
            resultado = ResultadoLaboratorio.objects.select_related("muestreo").get(pk=pk)
        except ResultadoLaboratorio.DoesNotExist as e:
            raise Http404() from e

        if getattr(getattr(resultado, "muestreo", None), "oficina_id", None) != office_id:
            raise Http404()

        if not resultado.informe:
            raise Http404()

        resp = FileResponse(resultado.informe.open("rb"), content_type="application/octet-stream")
        resp["Content-Disposition"] = f"inline; filename=\"{resultado.informe.name.split('/')[-1]}\""
        return resp
