from decimal import Decimal

from django.conf import settings
from django.urls import reverse
from rest_framework import serializers

from apps.boletas.models import Boleta
from apps.catalogos.models import Bascula, Bodega, Campo, Linea
from apps.catalogos.serializers import BodegaObjectField
from apps.recepciones.models import (
    Bonificacion,
    Deduccion,
    Flete,
    FotoRecepcion,
    Muestreo,
    PesoFlete,
    PesoRecepcion,
    Recepcion,
)
from apps.recepciones.utils import parse_boleta_qr_text
from utils.office import extract_office_id


def _choices_help_text(choices):
    first = choices[0] if choices else None
    if isinstance(first, (list, tuple)) and len(first) == 2:
        return "Valores permitidos: " + ", ".join([f"{v} ({label})" for v, label in choices])
    return "Valores permitidos: " + ", ".join([str(v) for v in choices])


class CampoLiteSerializer(serializers.ModelSerializer):
    class Meta:
        model = Campo
        fields = ["id", "nombre"]


class LineaLiteSerializer(serializers.ModelSerializer):
    class Meta:
        model = Linea
        fields = ["id", "nombre", "codigo"]


class BoletaLiteSerializer(serializers.ModelSerializer):
    productor_nombre = serializers.CharField(source="productor.nombre", read_only=True)
    productor_rfc = serializers.CharField(source="productor.rfc", read_only=True)
    contrato_numero = serializers.CharField(source="contrato.numero_contrato", read_only=True)
    variedad_nombre = serializers.CharField(source="variedad.nombre", read_only=True)
    campo = CampoLiteSerializer(read_only=True)
    linea = LineaLiteSerializer(read_only=True)

    class Meta:
        model = Boleta
        fields = [
            "id",
            "zafra",
            "folio",
            "estado",
            "peso_estimado",
            "origen_trigo",
            "placas_camion",
            "linea",
            "linea_nombre",
            "marca_unidad",
            "modelo_unidad",
            "chofer_nombre",
            "chofer_telefono",
            "chofer_firma_en_dispositivo",
            "productor",
            "productor_nombre",
            "productor_rfc",
            "contrato",
            "contrato_numero",
            "campo",
            "variedad_nombre",
        ]


class PesoRecepcionSerializer(serializers.ModelSerializer):
    class Meta:
        model = PesoRecepcion
        fields = ["id", "tipo", "valor", "es_automatico", "motivo_manual", "bascula", "created_at"]
        read_only_fields = ["id", "tipo", "created_at"]
        extra_kwargs = {
            "tipo": {"help_text": _choices_help_text(PesoRecepcion.TIPO_CHOICES)},
        }


class PesoFleteSerializer(serializers.ModelSerializer):
    class Meta:
        model = PesoFlete
        fields = ["id", "tipo", "valor", "es_automatico", "motivo_manual", "bascula", "created_at"]
        read_only_fields = ["id", "tipo", "created_at"]
        extra_kwargs = {
            "tipo": {"help_text": _choices_help_text(PesoFlete.TIPO_CHOICES)},
        }


class PesoCreateSerializer(serializers.Serializer):
    valor = serializers.DecimalField(max_digits=10, decimal_places=2)
    es_automatico = serializers.BooleanField(default=True)
    motivo_manual = serializers.CharField(required=False, allow_blank=True, default="")
    bascula_id = serializers.IntegerField(required=False, allow_null=True, min_value=1)
    access_token = serializers.CharField(required=False, allow_blank=True, default="")

    def validate(self, attrs):
        bascula_id = attrs.get("bascula_id")
        access_token = attrs.get("access_token")

        if bascula_id or access_token:
            if not bascula_id:
                raise serializers.ValidationError(
                    {"bascula_id": "bascula_id es requerido cuando se envía access_token."}
                )
            if not access_token:
                raise serializers.ValidationError(
                    {"access_token": "access_token es requerido cuando se envía bascula_id."}
                )
            if not settings.BASCULA_ACCESS_TOKEN or access_token != settings.BASCULA_ACCESS_TOKEN:
                raise serializers.ValidationError({"access_token": "Access token inválido."})

            try:
                attrs["bascula"] = Bascula.objects.get(pk=bascula_id)
            except Bascula.DoesNotExist:
                raise serializers.ValidationError({"bascula_id": "Báscula no encontrada."})

            attrs["es_automatico"] = True
        else:
            attrs["bascula"] = None
            if not attrs.get("es_automatico") and not attrs.get("motivo_manual"):
                raise serializers.ValidationError(
                    {"motivo_manual": "motivo_manual es requerido cuando es_automatico=false."}
                )

        return attrs


class PesoPorQrCreateSerializer(PesoCreateSerializer):
    """
    Variante de PesoCreateSerializer para básculas que no conocen el
    `recepcion_id`: identifican la recepción a partir del `qr` (o `folio` +
    `zafra_id`) leído de la boleta.
    """

    qr = serializers.CharField(required=False, allow_blank=False)
    folio = serializers.CharField(required=False, allow_blank=False)
    zafra_id = serializers.IntegerField(required=False)

    def validate(self, attrs):
        attrs = super().validate(attrs)

        if not attrs.get("qr") and not attrs.get("folio"):
            raise serializers.ValidationError(
                {"qr": "Debes enviar 'qr' o 'folio' para identificar la recepción."}
            )
        if attrs.get("qr") and attrs.get("folio"):
            raise serializers.ValidationError({"qr": "Envía solo uno de 'qr' o 'folio'."})

        return attrs

    def resolve_recepcion(self) -> Recepcion:
        data = self.validated_data

        if data.get("qr"):
            parsed = parse_boleta_qr_text(data["qr"])
            if not parsed:
                raise serializers.ValidationError({"qr": "Formato de QR inválido."})
            boleta_qs = Boleta.objects.filter(zafra_id=parsed["zafra_id"], folio=parsed["folio"])
        else:
            folio = str(data["folio"]).strip()
            if not folio:
                raise serializers.ValidationError({"folio": "Folio inválido."})
            boleta_qs = Boleta.objects.filter(folio=folio)
            if data.get("zafra_id"):
                boleta_qs = boleta_qs.filter(zafra_id=data["zafra_id"])

        count = boleta_qs.count()
        if count == 0:
            raise serializers.ValidationError({"detail": "Boleta no encontrada."})
        if count > 1:
            raise serializers.ValidationError(
                {"detail": "Folio ambiguo: existe en varias zafras. Envía zafra_id o usa el QR."}
            )

        boleta = boleta_qs.first()
        recepcion = getattr(boleta, "recepcion", None)
        if recepcion is None:
            raise serializers.ValidationError(
                {"detail": "La recepción para esta boleta aún no ha sido iniciada."}
            )
        return recepcion


class FotoRecepcionSerializer(serializers.ModelSerializer):
    imagen_url = serializers.SerializerMethodField()

    class Meta:
        model = FotoRecepcion
        fields = ["id", "tipo", "imagen_url", "created_at"]
        extra_kwargs = {
            "tipo": {"help_text": _choices_help_text(FotoRecepcion.TIPO_CHOICES)},
        }

    def get_imagen_url(self, obj):
        request = self.context.get("request")
        url = reverse("recepcion-foto-download", kwargs={"pk": obj.pk})
        if request:
            return request.build_absolute_uri(url)
        return url


class FotoRecepcionCreateSerializer(serializers.ModelSerializer):
    class Meta:
        model = FotoRecepcion
        fields = ["tipo", "imagen"]
        extra_kwargs = {
            "tipo": {"help_text": _choices_help_text(FotoRecepcion.TIPO_CHOICES)},
        }

    def validate_imagen(self, f):
        max_size = getattr(settings, "RECEPCIONES_MAX_FOTO_MB", 5) * 1024 * 1024
        if f.size > max_size:
            raise serializers.ValidationError("La imagen excede el tamaño máximo permitido.")
        name = (getattr(f, "name", "") or "").lower()
        if not (name.endswith(".jpg") or name.endswith(".jpeg") or name.endswith(".png")):
            raise serializers.ValidationError("Extensión inválida. Usa jpg o png.")
        return f


class MuestreoSerializer(serializers.ModelSerializer):
    tecnico_id = serializers.IntegerField(source="tecnico_asignado_id", required=False, allow_null=True)

    class Meta:
        model = Muestreo
        fields = [
            "id",
            "tipo_muestreo",
            "tecnico_id",
            "estado",
            "fecha_asignacion",
            "created_at",
            "updated_at",
        ]
        read_only_fields = ["id", "estado", "fecha_asignacion", "created_at", "updated_at"]
        extra_kwargs = {
            "tipo_muestreo": {"help_text": _choices_help_text(Muestreo.TIPO_CHOICES)},
            "estado": {"help_text": _choices_help_text(Muestreo.ESTADO_CHOICES)},
        }


class MuestreoListSerializer(MuestreoSerializer):
    tecnico_nombre = serializers.SerializerMethodField()

    class Meta(MuestreoSerializer.Meta):
        fields = list(MuestreoSerializer.Meta.fields) + ["tecnico_nombre"]

    def get_tecnico_nombre(self, obj):
        tecnico = getattr(obj, "tecnico_asignado", None)
        if not tecnico:
            return None

        full_name = (tecnico.get_full_name() or "").strip() if hasattr(tecnico, "get_full_name") else ""
        if full_name:
            return full_name

        username = getattr(tecnico, "username", "")
        if username:
            return username

        return str(tecnico)


class MuestreoCreateSerializer(serializers.Serializer):
    tipo_muestreo = serializers.ChoiceField(
        choices=Muestreo.TIPO_CHOICES,
        help_text=_choices_help_text(Muestreo.TIPO_CHOICES),
    )
    tecnico_id = serializers.IntegerField(required=False, allow_null=True)


class DeduccionSerializer(serializers.ModelSerializer):
    parametro_nombre = serializers.CharField(source="parametro.nombre", read_only=True)

    class Meta:
        model = Deduccion
        fields = [
            "id",
            "parametro",
            "parametro_nombre",
            "kg_por_tonelada",
            "valor_deducido",
            "tipo_deduccion",
            "created_at",
        ]
        extra_kwargs = {
            "tipo_deduccion": {"help_text": _choices_help_text(Deduccion.TIPO_CHOICES)},
        }


class BonificacionSerializer(serializers.ModelSerializer):
    parametro_nombre = serializers.CharField(source="parametro.nombre", read_only=True)

    class Meta:
        model = Bonificacion
        fields = [
            "id",
            "parametro",
            "parametro_nombre",
            "monto_por_tonelada",
            "monto_total",
            "created_at",
        ]


class FleteRecepcionSerializer(serializers.ModelSerializer):
    boleta = BoletaLiteSerializer(read_only=True)
    deducciones = DeduccionSerializer(many=True, read_only=True)
    bonificaciones = BonificacionSerializer(many=True, read_only=True)

    class Meta:
        model = Recepcion
        fields = [
            "id",
            "boleta",
            "estado",
            "peso_neto_asignado",
            "bodega_destino",
            "observaciones",
            "deducciones",
            "bonificaciones",
        ]


class FleteSerializer(serializers.ModelSerializer):
    recepciones = FleteRecepcionSerializer(many=True, read_only=True)
    oficina_nombre = serializers.CharField(source="oficina.nombre", read_only=True)
    variedad_nombre = serializers.CharField(source="variedad.nombre", read_only=True)
    linea = LineaLiteSerializer(read_only=True)
    pesos = PesoFleteSerializer(many=True, read_only=True)

    class Meta:
        model = Flete
        fields = [
            "id",
            "oficina",
            "oficina_nombre",
            "variedad",
            "variedad_nombre",
            "estado",
            "placas_camion",
            "linea",
            "linea_nombre",
            "marca_unidad",
            "modelo_unidad",
            "chofer_nombre",
            "chofer_telefono",
            "chofer_firma_en_dispositivo",
            "origen_trigo",
            "observaciones",
            "fecha_cierre",
            "firmas",
            "alerta",
            "pesos",
            "recepciones",
            "created_at",
            "updated_at",
            "created_by",
            "updated_by",
        ]
        read_only_fields = [
            "estado",
            "fecha_cierre",
            "firmas",
            "alerta",
            "created_at",
            "updated_at",
            "created_by",
            "updated_by",
        ]


class RecepcionSerializer(serializers.ModelSerializer):
    boleta = BoletaLiteSerializer(read_only=True)
    bodega_destino_nombre = serializers.CharField(source="bodega_destino.nombre", read_only=True)
    flete = FleteSerializer(read_only=True)
    pesos = serializers.SerializerMethodField()
    fotos = FotoRecepcionSerializer(many=True, read_only=True)
    muestreo = serializers.SerializerMethodField()
    deducciones = DeduccionSerializer(many=True, read_only=True)
    bonificaciones = BonificacionSerializer(many=True, read_only=True)

    placas_camion = serializers.SerializerMethodField()
    linea_nombre = serializers.SerializerMethodField()
    marca_unidad = serializers.SerializerMethodField()
    modelo_unidad = serializers.SerializerMethodField()
    chofer_nombre = serializers.SerializerMethodField()
    chofer_telefono = serializers.SerializerMethodField()
    chofer_firma_en_dispositivo = serializers.SerializerMethodField()
    origen_trigo = serializers.SerializerMethodField()

    peso_bruto = serializers.SerializerMethodField()
    peso_neto = serializers.SerializerMethodField()

    class Meta:
        model = Recepcion
        fields = [
            "id",
            "boleta",
            "flete",
            "bodega_destino",
            "bodega_destino_nombre",
            "estado",
            "rechazo_calidad",
            "rechazos_calidad",
            "calidad_calculada",
            "placas_camion",
            "linea_nombre",
            "marca_unidad",
            "modelo_unidad",
            "chofer_nombre",
            "chofer_telefono",
            "chofer_firma_en_dispositivo",
            "origen_trigo",
            "observaciones",
            "fecha_cierre",
            "firmas",
            "alerta",
            "pesos",
            "fotos",
            "muestreo",
            "deducciones",
            "bonificaciones",
            "peso_bruto",
            "peso_neto",
            "created_at",
            "updated_at",
            "created_by",
            "updated_by",
        ]
        read_only_fields = [
            "estado",
            "fecha_cierre",
            "firmas",
            "alerta",
            "rechazo_calidad",
            "rechazos_calidad",
            "calidad_calculada",
            "created_at",
            "updated_at",
            "created_by",
            "updated_by",
        ]
        extra_kwargs = {
            "estado": {"help_text": _choices_help_text(Recepcion.ESTADO_CHOICES)},
        }

    def _camion_source(self, obj: Recepcion):
        return obj.flete if obj.flete_id else obj.boleta

    def get_placas_camion(self, obj):
        return getattr(self._camion_source(obj), "placas_camion", "")

    def get_linea_nombre(self, obj):
        return getattr(self._camion_source(obj), "linea_nombre", "")

    def get_marca_unidad(self, obj):
        return getattr(self._camion_source(obj), "marca_unidad", "")

    def get_modelo_unidad(self, obj):
        return getattr(self._camion_source(obj), "modelo_unidad", "")

    def get_chofer_nombre(self, obj):
        return getattr(self._camion_source(obj), "chofer_nombre", "")

    def get_chofer_telefono(self, obj):
        return getattr(self._camion_source(obj), "chofer_telefono", "")

    def get_chofer_firma_en_dispositivo(self, obj):
        return bool(getattr(self._camion_source(obj), "chofer_firma_en_dispositivo", False))

    def get_origen_trigo(self, obj):
        return getattr(self._camion_source(obj), "origen_trigo", "")

    def get_pesos(self, obj):
        if obj.flete_id:
            return PesoFleteSerializer(obj.flete.pesos.all(), many=True).data
        return PesoRecepcionSerializer(obj.pesos.all(), many=True).data

    def get_muestreo(self, obj):
        if obj.flete_id:
            m = getattr(obj.flete, "muestreo", None)
        else:
            m = getattr(obj, "muestreo", None)
        if m is None:
            return None
        return MuestreoSerializer(m).data

    def _peso_recepcion(self, obj, tipo: str):
        p = next((x for x in obj.pesos.all() if x.tipo == tipo), None)
        return p.valor if p else None

    def _peso_flete(self, obj, tipo: str):
        p = next((x for x in obj.flete.pesos.all() if x.tipo == tipo), None)
        return p.valor if p else None

    def get_peso_bruto(self, obj):
        if obj.flete_id:
            lleno = self._peso_flete(obj, PesoFlete.TIPO_LLENO)
            vacio = self._peso_flete(obj, PesoFlete.TIPO_VACIO)
        else:
            lleno = self._peso_recepcion(obj, PesoRecepcion.TIPO_LLENO)
            vacio = self._peso_recepcion(obj, PesoRecepcion.TIPO_VACIO)
        if lleno is None or vacio is None:
            return None
        return lleno - vacio

    def get_peso_neto(self, obj):
        if obj.flete_id:
            if obj.peso_neto_asignado is not None:
                bruto = Decimal(obj.peso_neto_asignado)
            else:
                return None
        else:
            bruto = self.get_peso_bruto(obj)
        if bruto is None:
            return None

        total = Decimal("0")
        for d in obj.deducciones.all():
            if d.tipo_deduccion == "fijo":
                total += d.valor_deducido
            elif d.tipo_deduccion == "porcentaje":
                total += (bruto * d.valor_deducido) / Decimal("100")

        return bruto - total


class RecepcionIdentificarResponseSerializer(RecepcionSerializer):
    created = serializers.BooleanField()

    class Meta(RecepcionSerializer.Meta):
        fields = list(RecepcionSerializer.Meta.fields) + ["created"]


class RecepcionCreateSerializer(serializers.ModelSerializer):
    class Meta:
        model = Recepcion
        fields = ["boleta", "bodega_destino"]

    def validate_bodega_destino(self, bodega: Bodega | None):
        if bodega is None:
            return None

        request = self.context.get("request")
        office_id = extract_office_id(request) if request is not None else None
        bodega_oficina_id = getattr(bodega, "oficina_id", None)
        if office_id is not None and bodega_oficina_id is not None and int(bodega_oficina_id) != int(office_id):
            raise serializers.ValidationError("La bodega destino no pertenece a la oficina indicada.")
        return bodega


class RecepcionUpdateSerializer(serializers.Serializer):
    bodega_destino = BodegaObjectField(
        queryset=Bodega.objects.all(), required=False, allow_null=True
    )
    placas_camion = serializers.CharField(required=False, allow_blank=True)
    linea = serializers.IntegerField(required=False, allow_null=True)
    linea_id = serializers.IntegerField(required=False, allow_null=True, write_only=True)
    linea_nombre = serializers.CharField(required=False, allow_blank=True)
    marca_unidad = serializers.CharField(required=False, allow_blank=True)
    modelo_unidad = serializers.CharField(required=False, allow_blank=True)
    chofer_nombre = serializers.CharField(required=False, allow_blank=True)
    chofer_telefono = serializers.CharField(required=False, allow_blank=True)
    chofer_firma_en_dispositivo = serializers.BooleanField(required=False)
    origen_trigo = serializers.CharField(required=False, allow_blank=True)
    observaciones = serializers.CharField(required=False, allow_blank=True)

    def _validate_linea(self, value):
        if value in (None, ""):
            return None
        try:
            linea_id = int(value)
        except (TypeError, ValueError) as e:
            raise serializers.ValidationError("Línea inválida.") from e

        try:
            linea = Linea.objects.get(pk=linea_id)
        except Linea.DoesNotExist as e:
            raise serializers.ValidationError("Línea no encontrada.") from e

        request = self.context.get("request")
        office_id = extract_office_id(request) if request is not None else None
        linea_oficina_id = getattr(linea, "oficina_id", None)
        if office_id is not None and linea_oficina_id is not None and int(linea_oficina_id) != int(office_id):
            raise serializers.ValidationError("La línea no pertenece a la oficina indicada.")
        return linea_id

    def validate_linea(self, value):
        return self._validate_linea(value)

    def validate_linea_id(self, value):
        return self._validate_linea(value)

    def validate(self, attrs):
        if "linea" in attrs:
            linea = attrs.pop("linea")
            if "linea_id" in attrs and linea != attrs["linea_id"]:
                raise serializers.ValidationError({"linea": "linea y linea_id no pueden ser diferentes."})
            attrs["linea_id"] = linea
        return attrs

    def validate_bodega_destino(self, bodega):
        if bodega is None:
            return None

        request = self.context.get("request")
        office_id = extract_office_id(request) if request is not None else None
        bodega_oficina_id = getattr(bodega, "oficina_id", None)
        if office_id is not None and bodega_oficina_id is not None and int(bodega_oficina_id) != int(office_id):
            raise serializers.ValidationError("La bodega destino no pertenece a la oficina indicada.")
        return bodega

    def validate_placas_camion(self, v):
        return (v or "").strip()

    def validate_chofer_telefono(self, v):
        v = (v or "").strip()
        if v and not v.isdigit():
            raise serializers.ValidationError("El teléfono debe ser numérico.")
        return v

    def update(self, instance: Recepcion, validated_data):
        updated_by = validated_data.pop("updated_by", None)

        if "bodega_destino" in validated_data:
            instance.bodega_destino = validated_data["bodega_destino"]

        # Los datos del camión se guardan en el flete cuando la recepción pertenece a uno.
        camion_target = instance.flete if instance.flete_id else instance.boleta
        camion_update_fields = []
        for field in [
            "placas_camion",
            "linea_nombre",
            "marca_unidad",
            "modelo_unidad",
            "chofer_nombre",
            "chofer_telefono",
            "chofer_firma_en_dispositivo",
            "origen_trigo",
        ]:
            if field in validated_data:
                setattr(camion_target, field, validated_data[field])
                camion_update_fields.append(field)

        if "linea_id" in validated_data:
            camion_target.linea_id = validated_data["linea_id"]
            camion_update_fields.append("linea")
            if camion_target.linea_id:
                camion_target.linea_nombre = camion_target.linea.nombre
                camion_update_fields.append("linea_nombre")

        if camion_update_fields:
            if updated_by is not None:
                camion_target.updated_by = updated_by
                camion_update_fields.extend(["updated_by", "updated_at"])
            else:
                camion_update_fields.append("updated_at")
            camion_target.save(update_fields=list(dict.fromkeys(camion_update_fields)))

        if "observaciones" in validated_data:
            instance.observaciones = validated_data["observaciones"]

        recepcion_update_fields = []
        if "bodega_destino" in validated_data:
            recepcion_update_fields.append("bodega_destino")
        if "observaciones" in validated_data:
            recepcion_update_fields.append("observaciones")

        if recepcion_update_fields:
            if updated_by is not None:
                instance.updated_by = updated_by
                recepcion_update_fields.extend(["updated_by", "updated_at"])
            else:
                recepcion_update_fields.append("updated_at")
            instance.save(update_fields=list(dict.fromkeys(recepcion_update_fields)))

        return instance

    def to_representation(self, instance: Recepcion):
        camion_source = instance.flete if instance.flete_id else instance.boleta
        return {
            "bodega_destino": getattr(instance.bodega_destino, "pk", None) if instance.bodega_destino else None,
            "placas_camion": getattr(camion_source, "placas_camion", "") if camion_source else "",
            "linea": getattr(camion_source, "linea_id", None) if camion_source else None,
            "linea_id": getattr(camion_source, "linea_id", None) if camion_source else None,
            "linea_nombre": getattr(camion_source, "linea_nombre", "") if camion_source else "",
            "marca_unidad": getattr(camion_source, "marca_unidad", "") if camion_source else "",
            "modelo_unidad": getattr(camion_source, "modelo_unidad", "") if camion_source else "",
            "chofer_nombre": getattr(camion_source, "chofer_nombre", "") if camion_source else "",
            "chofer_telefono": getattr(camion_source, "chofer_telefono", "") if camion_source else "",
            "chofer_firma_en_dispositivo": bool(getattr(camion_source, "chofer_firma_en_dispositivo", False)) if camion_source else False,
            "origen_trigo": getattr(camion_source, "origen_trigo", "") if camion_source else "",
            "observaciones": getattr(instance, "observaciones", "") or "",
        }


class IdentificarBoletaSerializer(serializers.Serializer):
    bodega_destino = BodegaObjectField(
        queryset=Bodega.objects.all(), required=False, allow_null=True
    )
    qr = serializers.CharField(required=False, allow_blank=False)
    folio = serializers.CharField(required=False, allow_blank=False)
    zafra_id = serializers.IntegerField(required=False)

    def validate_bodega_destino(self, bodega):
        if bodega is None:
            return None

        request = self.context.get("request")
        office_id = extract_office_id(request) if request is not None else None
        bodega_oficina_id = getattr(bodega, "oficina_id", None)
        if office_id is not None and bodega_oficina_id is not None and int(bodega_oficina_id) != int(office_id):
            raise serializers.ValidationError("La bodega destino no pertenece a la oficina indicada.")

        return bodega

    def validate(self, attrs):
        if not attrs.get("qr") and not attrs.get("folio"):
            raise serializers.ValidationError("Debes enviar 'qr' o 'folio'.")
        if attrs.get("qr") and attrs.get("folio"):
            raise serializers.ValidationError("Envía solo uno de 'qr' o 'folio'.")
        return attrs

    def resolve_boleta(self) -> Boleta:
        data = self.validated_data
        if data.get("qr"):
            parsed = parse_boleta_qr_text(data["qr"])
            if not parsed:
                raise serializers.ValidationError({"qr": "Formato de QR inválido."})
            try:
                return Boleta.objects.select_related("zafra", "productor", "contrato", "variedad").get(
                    zafra_id=parsed["zafra_id"],
                    folio=parsed["folio"],
                )
            except Boleta.DoesNotExist:
                raise serializers.ValidationError({"detail": "Boleta no encontrada."})

        folio = str(data["folio"]).strip()
        if not folio:
            raise serializers.ValidationError({"folio": "Folio inválido."})

        qs = Boleta.objects.select_related("zafra", "productor", "contrato", "variedad").filter(folio=folio)
        if data.get("zafra_id"):
            qs = qs.filter(zafra_id=data["zafra_id"])

        count = qs.count()
        if count == 0:
            raise serializers.ValidationError({"detail": "Boleta no encontrada."})
        if count > 1:
            raise serializers.ValidationError(
                {
                    "detail": "Folio ambiguo: existe en varias zafras. Envía zafra_id o usa el QR.",
                }
            )
        return qs.first()


class AgregarBoletaSerializer(IdentificarBoletaSerializer):
    pass


class DistribuirPesoItemSerializer(serializers.Serializer):
    recepcion_id = serializers.IntegerField(min_value=1)
    peso_neto_asignado = serializers.DecimalField(
        max_digits=12,
        decimal_places=3,
        min_value=Decimal("0"),
    )


class DistribuirPesoSerializer(serializers.Serializer):
    distribuciones = DistribuirPesoItemSerializer(many=True)


class CerrarRecepcionSerializer(serializers.Serializer):
    firma_receptor = serializers.CharField(required=False, allow_blank=False)
    firma_chofer = serializers.CharField(required=False, allow_blank=False)
    decision = serializers.ChoiceField(
        choices=["aceptar_con_comentario", "rechazar", "escalar"],
        required=False,
        help_text=_choices_help_text(["aceptar_con_comentario", "rechazar", "escalar"]),
    )
    comentario = serializers.CharField(required=False, allow_blank=True, default="")


class ResolverDisputaRecepcionSerializer(serializers.Serializer):
    decision = serializers.ChoiceField(
        choices=["autorizar", "rechazar"],
        required=True,
        help_text=_choices_help_text(["autorizar", "rechazar"]),
    )
    comentario = serializers.CharField(required=False, allow_blank=True, default="")


class ReabrirRecepcionSerializer(serializers.Serializer):
    motivo = serializers.CharField(required=True, allow_blank=False)
