import io
from decimal import Decimal, InvalidOperation

from django.db import transaction
from django.db.models import Exists, OuterRef
from django.http import HttpResponse
from drf_spectacular.types import OpenApiTypes
from drf_spectacular.utils import OpenApiExample, OpenApiParameter, OpenApiResponse, extend_schema, extend_schema_view
from rest_framework.decorators import action
from rest_framework.exceptions import ValidationError
from rest_framework.parsers import MultiPartParser
from rest_framework.response import Response
from rest_framework.viewsets import ModelViewSet

try:
    import openpyxl
except ImportError:  # pragma: no cover
    openpyxl = None

from apps.catalogos.models import (
    Bascula,
    Bodega,
    Campo,
    Castigo,
    Contrato,
    ContratoVariedad,
    Linea,
    Oficina,
    Parametro,
    Productor,
    Variedad,
    Zafra,
)
from apps.catalogos.permissions import (
    CanEditCatalogos,
    CanManageBodegas,
    CanManageCalidadCatalogos,
    CanViewOperationalCatalogos,
)
from apps.catalogos.serializers import (
    BasculaSerializer,
    BodegaSerializer,
    CampoSerializer,
    CastigoSerializer,
    ContratoSerializer,
    LineaSerializer,
    OficinaSerializer,
    ParametroSerializer,
    ProductorSerializer,
    VariedadSerializer,
    ZafraSerializer,
)
from utils.audit import AuditFieldsViewSetMixin
from utils.office import extract_office_id
from utils.pagination import StandardResultsSetPagination


@extend_schema_view(
    list=extend_schema(tags=["Catálogos"]),
    retrieve=extend_schema(tags=["Catálogos"]),
    create=extend_schema(tags=["Catálogos"]),
    update=extend_schema(tags=["Catálogos"]),
    partial_update=extend_schema(tags=["Catálogos"]),
    destroy=extend_schema(tags=["Catálogos"]),
)
class BaseCatalogosViewSet(AuditFieldsViewSetMixin, ModelViewSet):
    permission_classes = [CanEditCatalogos]
    ordering = ["id"]
    pagination_class = StandardResultsSetPagination


class OfficeScopedCatalogosViewSet(BaseCatalogosViewSet):
    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."})

        user = getattr(self.request, "user", None)
        serializer.save(oficina_id=office_id, created_by=user, updated_by=user)


class ZafraViewSet(OfficeScopedCatalogosViewSet):
    permission_classes = [CanViewOperationalCatalogos]
    queryset = Zafra.objects.all().order_by("-fecha_inicio")
    serializer_class = ZafraSerializer
    filterset_fields = ["activa", "oficina"]
    search_fields = ["nombre"]


class OficinaViewSet(BaseCatalogosViewSet):
    queryset = Oficina.objects.all().order_by("codigo")
    serializer_class = OficinaSerializer
    filterset_fields = ["activa"]
    search_fields = ["codigo", "nombre"]


class VariedadViewSet(OfficeScopedCatalogosViewSet):
    permission_classes = [CanViewOperationalCatalogos]
    queryset = Variedad.objects.all().order_by("nombre")
    serializer_class = VariedadSerializer
    filterset_fields = ["activa", "oficina"]
    search_fields = ["nombre"]
    pagination_class = StandardResultsSetPagination


class CampoViewSet(OfficeScopedCatalogosViewSet):
    permission_classes = [CanViewOperationalCatalogos]
    queryset = Campo.objects.all().order_by("nombre")
    serializer_class = CampoSerializer
    filterset_fields = ["oficina"]
    search_fields = ["nombre"]
    pagination_class = StandardResultsSetPagination


class LineaViewSet(OfficeScopedCatalogosViewSet):
    permission_classes = [CanViewOperationalCatalogos]
    queryset = Linea.objects.all().order_by("nombre")
    serializer_class = LineaSerializer
    filterset_fields = ["oficina"]
    search_fields = ["codigo", "nombre", "id_sap"]
    pagination_class = StandardResultsSetPagination


@extend_schema_view(
    list=extend_schema(
        tags=["Catálogos"],
        parameters=[
            OpenApiParameter(
                name="con_contrato_activo",
                type=OpenApiTypes.BOOL,
                required=False,
                description="Si es true/1, devuelve solo productores con al menos un contrato activo (opcionalmente filtrado por zafra).",
            ),
            OpenApiParameter(
                name="zafra_id",
                type=OpenApiTypes.INT,
                required=False,
                description="(Opcional) Filtra el contrato activo por zafra_id. Recomendado cuando con_contrato_activo=true.",
            ),
            OpenApiParameter(
                name="zafra",
                type=OpenApiTypes.INT,
                required=False,
                description="Alias de zafra_id.",
            ),
        ],
    )
)
class ProductorViewSet(BaseCatalogosViewSet):
    queryset = Productor.objects.all().order_by("nombre")
    serializer_class = ProductorSerializer
    filterset_fields = ["activo"]
    search_fields = ["nombre", "rfc", "email", "codigo_sica"]
    pagination_class = StandardResultsSetPagination

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

        raw_flag = self.request.query_params.get("con_contrato_activo")
        if raw_flag is None:
            return qs

        flag = str(raw_flag).strip().lower()
        truthy = flag in {"1", "true", "t", "yes", "y", "si", "sí"}
        if not truthy:
            return qs

        zafra_id = self.request.query_params.get("zafra") or self.request.query_params.get("zafra_id")
        contratos = Contrato.objects.filter(productor_id=OuterRef("pk"), activo=True)
        if zafra_id is not None:
            zafra_id = str(zafra_id).strip()
            if zafra_id.isdigit():
                contratos = contratos.filter(zafra_id=int(zafra_id))

        return qs.annotate(_tiene_contrato_activo=Exists(contratos)).filter(_tiene_contrato_activo=True)


class ContratoViewSet(OfficeScopedCatalogosViewSet):
    permission_classes = [CanViewOperationalCatalogos]
    queryset = (
        Contrato.objects.select_related("productor", "zafra")
        .prefetch_related("variedades", "variedades__variedad")
        .all()
        .order_by("-id")
    )
    serializer_class = ContratoSerializer
    filterset_fields = ["activo", "zafra", "productor", "variedades__variedad", "oficina"]
    search_fields = ["numero_contrato"]

    @extend_schema(
        tags=["Catálogos"],
        summary="Descargar template de carga de contratos",
        description=(
            "Devuelve un archivo .xlsx con las columnas requeridas para la carga masiva de contratos. "
            "Cada fila representa una variedad de un contrato; repita el contrato en varias filas para asignarle múltiples variedades. "
            "Columnas: `numero_contrato`, `productor_rfc`, `zafra`, `variedad`, `peso_teorico`, `activo`."
        ),
        responses={
            200: OpenApiResponse(response=OpenApiTypes.BINARY, description="Archivo .xlsx template"),
        },
    )
    @action(detail=False, methods=["get"], url_path="descargar-template")
    def descargar_template(self, request):
        if openpyxl is None:
            return Response({"detail": "openpyxl no está instalado en el servidor."}, status=500)

        wb = openpyxl.Workbook()
        ws = wb.active
        ws.title = "Contratos"

        headers = ["numero_contrato", "productor_rfc", "zafra", "variedad", "peso_teorico", "activo"]
        ws.append(headers)

        ejemplo_filas = [
            ["C-2025-001", "ABCD010101ABC", "Zafra 2025", "Trigo harinero", "1500.500", "SI"],
            ["C-2025-001", "ABCD010101ABC", "Zafra 2025", "Trigo cristalino", "2000.000", "SI"],
            ["C-2025-002", "EFGH020202DEF", "Zafra 2025", "Trigo harinero", "1800.000", "SI"],
        ]
        for fila in ejemplo_filas:
            ws.append(fila)

        buffer = io.BytesIO()
        wb.save(buffer)
        buffer.seek(0)

        response = HttpResponse(
            buffer.read(),
            content_type="application/vnd.openxmlformats-officedocument.spreadsheetml.sheet",
        )
        response["Content-Disposition"] = 'attachment; filename="template_contratos.xlsx"'
        return response

    @extend_schema(
        tags=["Catálogos"],
        summary="Cargar contratos desde Excel",
        description=(
            "Crea o actualiza contratos a partir de un archivo .xlsx. "
            "Cada fila es una variedad de un contrato; las filas con el mismo `numero_contrato` y `zafra` se agrupan en un solo contrato. "
            "La variedad se busca por nombre y debe existir en la oficina indicada por el query param `office_id`. "
            "Las variedades previamente asignadas al contrato se reemplazan por las del archivo."
        ),
        request={
            "multipart/form-data": {
                "type": "object",
                "required": ["archivo"],
                "properties": {
                    "archivo": {"type": "string", "format": "binary"},
                },
            }
        },
        responses={
            200: OpenApiResponse(description="Contratos cargados correctamente"),
            400: OpenApiResponse(description="Error de validación del archivo"),
        },
    )
    @action(detail=False, methods=["post"], url_path="cargar-excel", parser_classes=[MultiPartParser])
    def cargar_excel(self, request):
        if openpyxl is None:
            return Response({"detail": "openpyxl no está instalado en el servidor."}, status=500)

        office_id = extract_office_id(request)
        if office_id is None:
            raise ValidationError({"office_id": "office_id es requerido."})

        archivo = request.FILES.get("archivo")
        if not archivo:
            raise ValidationError({"archivo": "Se requiere un archivo .xlsx."})

        try:
            wb = openpyxl.load_workbook(io.BytesIO(archivo.read()), data_only=True)
        except Exception as exc:
            raise ValidationError({"archivo": f"No se pudo leer el archivo Excel: {exc}"}) from exc

        ws = wb.active
        filas = list(ws.iter_rows(min_row=2, values_only=True))
        if not filas:
            raise ValidationError({"archivo": "El archivo no contiene filas de datos (desde la fila 2)."})

        user = request.user

        rfc_a_productor = {p.rfc.upper(): p for p in Productor.objects.all()}
        nombre_a_zafra = {z.nombre.strip(): z for z in Zafra.objects.all()}
        nombre_a_variedad = {v.nombre.strip(): v for v in Variedad.objects.all()}

        parsed_rows = []
        for idx, fila in enumerate(filas, start=2):
            if all(cell is None or str(cell).strip() == "" for cell in fila):
                continue

            if len(fila) < 5:
                raise ValidationError(
                    {
                        "archivo": (
                            f"Fila {idx}: se esperan al menos 5 columnas "
                            "(numero_contrato, productor_rfc, zafra, variedad, peso_teorico)."
                        )
                    }
                )

            numero_contrato = str(fila[0]).strip() if fila[0] is not None else ""
            productor_rfc = str(fila[1]).strip().upper() if fila[1] is not None else ""
            zafra_nombre = str(fila[2]).strip() if fila[2] is not None else ""
            variedad_nombre = str(fila[3]).strip() if fila[3] is not None else ""
            raw_peso = fila[4]
            raw_activo = fila[5] if len(fila) > 5 else None

            if not numero_contrato:
                raise ValidationError({"archivo": f"Fila {idx}: `numero_contrato` es requerido."})
            if not productor_rfc:
                raise ValidationError({"archivo": f"Fila {idx}: `productor_rfc` es requerido."})
            if not zafra_nombre:
                raise ValidationError({"archivo": f"Fila {idx}: `zafra` es requerida."})
            if not variedad_nombre:
                raise ValidationError({"archivo": f"Fila {idx}: `variedad` es requerida."})

            productor = rfc_a_productor.get(productor_rfc)
            if productor is None:
                raise ValidationError(
                    {"archivo": f"Fila {idx}: productor con RFC `{productor_rfc}` no encontrado."}
                )

            zafra = nombre_a_zafra.get(zafra_nombre)
            if zafra is None:
                raise ValidationError({"archivo": f"Fila {idx}: zafra `{zafra_nombre}` no encontrada."})

            variedad = nombre_a_variedad.get(variedad_nombre)
            if variedad is None:
                raise ValidationError({"archivo": f"Fila {idx}: variedad `{variedad_nombre}` no encontrada."})

            if office_id is not None and zafra.oficina_id is not None and zafra.oficina_id != office_id:
                raise ValidationError(
                    {"archivo": f"Fila {idx}: zafra `{zafra_nombre}` no pertenece a la oficina indicada."}
                )
            if office_id is not None and variedad.oficina_id is not None and variedad.oficina_id != office_id:
                raise ValidationError(
                    {"archivo": f"Fila {idx}: variedad `{variedad_nombre}` no pertenece a la oficina indicada."}
                )

            try:
                peso_teorico = Decimal(str(raw_peso).replace(",", "").strip()) if raw_peso is not None else None
            except InvalidOperation:
                raise ValidationError({"archivo": f"Fila {idx}: `peso_teorico` inválido: '{raw_peso}'."})

            activo = True
            if raw_activo is not None:
                activo_str = str(raw_activo).strip().lower()
                activo = activo_str in {"1", "true", "t", "yes", "y", "si", "sí"}

            parsed_rows.append(
                {
                    "fila": idx,
                    "numero_contrato": numero_contrato,
                    "productor_id": productor.id,
                    "zafra_id": zafra.id,
                    "variedad": variedad,
                    "peso_teorico": peso_teorico,
                    "activo": activo,
                }
            )

        if not parsed_rows:
            raise ValidationError({"archivo": "No se encontraron filas válidas para importar."})

        grupos = {}
        for row in parsed_rows:
            key = (row["numero_contrato"], row["zafra_id"])
            if key not in grupos:
                grupos[key] = {
                    "productor_id": row["productor_id"],
                    "activo": row["activo"],
                    "variedades": [],
                }
            grupo = grupos[key]
            if grupo["productor_id"] != row["productor_id"]:
                raise ValidationError(
                    {
                        "archivo": (
                            f"Fila {row['fila']}: el contrato `{row['numero_contrato']}` "
                            f"con zafra `{row['zafra_id']}` tiene productores diferentes."
                        )
                    }
                )
            if grupo["activo"] != row["activo"]:
                raise ValidationError(
                    {
                        "archivo": (
                            f"Fila {row['fila']}: el contrato `{row['numero_contrato']}` "
                            f"con zafra `{row['zafra_id']}` tiene valores de `activo` diferentes."
                        )
                    }
                )

            variedad_ids = {v["variedad"].id for v in grupo["variedades"]}
            if row["variedad"].id in variedad_ids:
                raise ValidationError(
                    {
                        "archivo": (
                            f"Fila {row['fila']}: variedad `{row['variedad'].nombre}` duplicada "
                            f"para el contrato `{row['numero_contrato']}`."
                        )
                    }
                )

            grupo["variedades"].append(
                {
                    "variedad": row["variedad"],
                    "peso_teorico": row["peso_teorico"],
                }
            )

        contratos_creados = 0
        contratos_actualizados = 0

        with transaction.atomic():
            for (numero_contrato, zafra_id), grupo in grupos.items():
                contrato, created = Contrato.objects.update_or_create(
                    numero_contrato=numero_contrato,
                    zafra_id=zafra_id,
                    defaults={
                        "productor_id": grupo["productor_id"],
                        "activo": grupo["activo"],
                        "oficina_id": office_id,
                        "updated_by": user,
                    },
                )
                if created:
                    contrato.created_by = user
                    contrato.save(update_fields=["created_by"])
                    contratos_creados += 1
                else:
                    contratos_actualizados += 1

                ContratoVariedad.objects.filter(contrato=contrato).delete()
                for item in grupo["variedades"]:
                    ContratoVariedad.objects.create(
                        contrato=contrato,
                        variedad=item["variedad"],
                        peso_teorico=item["peso_teorico"],
                        created_by=user,
                        updated_by=user,
                    )

        return Response(
            {
                "detail": "Contratos cargados correctamente.",
                "contratos_creados": contratos_creados,
                "contratos_actualizados": contratos_actualizados,
                "total_variedades": len(parsed_rows),
            },
            status=200,
        )


class BodegaViewSet(OfficeScopedCatalogosViewSet):
    permission_classes = [CanManageBodegas]
    queryset = Bodega.objects.all().order_by("codigo")
    serializer_class = BodegaSerializer
    filterset_fields = ["activa", "oficina", "es_rechazo", "es_externo"]
    search_fields = ["codigo", "nombre", "codigo_sap"]


class BasculaViewSet(OfficeScopedCatalogosViewSet):
    queryset = Bascula.objects.all().order_by("codigo")
    serializer_class = BasculaSerializer
    filterset_fields = ["oficina"]
    search_fields = ["codigo", "modelo"]


@extend_schema_view(
    list=extend_schema(
        tags=["Catálogos"],
        summary="Listar parámetros",
        description=(
            "Lista los parámetros de calidad. "
            "Permite filtrar por `activo` y `id`. "
            "Cuando se filtra por un único `id`, la respuesta incluye el listado completo de castigos asociados."
        ),
        parameters=[
            OpenApiParameter(
                name="id",
                type=OpenApiTypes.INT,
                location=OpenApiParameter.QUERY,
                required=False,
                description="ID exacto del parámetro. Si se envía, la respuesta incluye los castigos del parámetro.",
            ),
        ],
    ),
    retrieve=extend_schema(
        tags=["Catálogos"],
        summary="Obtener parámetro",
        description="Devuelve un parámetro por su ID incluyendo el listado completo de castigos asociados.",
    ),
    create=extend_schema(
        tags=["Catálogos"],
        summary="Crear parámetro",
        description="Crea un nuevo parámetro de calidad.",
    ),
    update=extend_schema(
        tags=["Catálogos"],
        summary="Actualizar parámetro",
        description="Reemplaza completamente un parámetro existente.",
    ),
    partial_update=extend_schema(
        tags=["Catálogos"],
        summary="Actualizar parcialmente parámetro",
        description="Actualiza campos específicos de un parámetro existente.",
    ),
    destroy=extend_schema(
        tags=["Catálogos"],
        summary="Eliminar parámetro",
        description="Elimina un parámetro por su ID.",
        responses={204: OpenApiResponse(description="Eliminado correctamente")},
    ),
)
class ParametroViewSet(BaseCatalogosViewSet):
    permission_classes = [CanManageCalidadCatalogos]
    queryset = Parametro.objects.prefetch_related("castigos").all().order_by("nombre")
    serializer_class = ParametroSerializer
    filterset_fields = ["activo", "id"]
    search_fields = ["nombre", "unidad"]

    def get_serializer_context(self):
        context = super().get_serializer_context()
        include = self.action == "retrieve"
        if self.action == "list":
            request = self.request
            param_id = request.query_params.get("id") or request.query_params.get("pk")
            include = bool(param_id)
        context["include_castigos"] = include
        return context


@extend_schema_view(
    list=extend_schema(
        tags=["Catálogos"],
        summary="Listar castigos",
        description=(
            "Devuelve la tabla de castigos configurada por parámetro de calidad. "
            "Use el query param `parametro` para filtrar por ID de parámetro. "
            "El campo `parametro` se devuelve como un objeto con `id` y `nombre`."
        ),
        parameters=[
            OpenApiParameter(
                name="parametro",
                type=OpenApiTypes.INT,
                location=OpenApiParameter.QUERY,
                required=False,
                description="ID del parámetro para filtrar los castigos.",
            ),
        ],
    ),
    retrieve=extend_schema(
        tags=["Catálogos"],
        summary="Obtener castigo",
        description="Devuelve un castigo por su ID.",
    ),
    create=extend_schema(
        tags=["Catálogos"],
        summary="Crear castigo",
        description="Crea un rango de castigo/bonificación/rechazo asociado a un parámetro de calidad.",
        request=CastigoSerializer,
        responses={201: CastigoSerializer},
        examples=[
            OpenApiExample(
                "Castigo normal",
                value={
                    "parametro": 1,
                    "limite_superior": "12.000",
                    "descontar_kg_por_tonelada": "2.00",
                    "bonificar_por_tonelada": "0.00",
                    "es_rechazo": False,
                },
                request_only=True,
            ),
            OpenApiExample(
                "Rechazo",
                value={
                    "parametro": 1,
                    "limite_superior": "14.000",
                    "descontar_kg_por_tonelada": "0.00",
                    "bonificar_por_tonelada": "0.00",
                    "es_rechazo": True,
                },
                request_only=True,
            ),
        ],
    ),
    update=extend_schema(
        tags=["Catálogos"],
        summary="Actualizar castigo",
        description="Reemplaza completamente un castigo existente.",
        request=CastigoSerializer,
        responses={200: CastigoSerializer},
    ),
    partial_update=extend_schema(
        tags=["Catálogos"],
        summary="Actualizar parcialmente castigo",
        description="Actualiza campos específicos de un castigo existente.",
        request=CastigoSerializer,
        responses={200: CastigoSerializer},
    ),
    destroy=extend_schema(
        tags=["Catálogos"],
        summary="Eliminar castigo",
        description="Elimina un castigo por su ID.",
        responses={204: OpenApiResponse(description="Eliminado correctamente")},
    ),
)
class CastigoViewSet(BaseCatalogosViewSet):
    permission_classes = [CanManageCalidadCatalogos]
    queryset = Castigo.objects.select_related("parametro").all().order_by("-id")
    serializer_class = CastigoSerializer
    filterset_fields = ["parametro"]

    @extend_schema(
        tags=["Catálogos"],
        summary="Descargar template de carga de castigos",
        description=(
            "Devuelve un archivo .xlsx con las columnas requeridas para la carga masiva de castigos: "
            "`limite_superior`, `descontar`, `bonificar`. "
            "La última fila de datos debe tener el string `RECHAZO` en la columna `bonificar`."
        ),
        responses={
            200: OpenApiResponse(response=OpenApiTypes.BINARY, description="Archivo .xlsx template"),
        },
    )
    @action(detail=False, methods=["get"], url_path="descargar-template")
    def descargar_template(self, request):
        if openpyxl is None:
            return Response({"detail": "openpyxl no está instalado en el servidor."}, status=500)

        wb = openpyxl.Workbook()
        ws = wb.active
        ws.title = "Castigos"

        headers = ["limite_superior", "descontar", "bonificar"]
        ws.append(headers)

        ejemplo_filas = [
            [10.0, 0.0, 5.0],
            [12.0, 2.0, 0.0],
            [14.0, 0.0, "RECHAZO"],
        ]
        for fila in ejemplo_filas:
            ws.append(fila)

        buffer = io.BytesIO()
        wb.save(buffer)
        buffer.seek(0)

        response = HttpResponse(
            buffer.read(),
            content_type="application/vnd.openxmlformats-officedocument.spreadsheetml.sheet",
        )
        response["Content-Disposition"] = 'attachment; filename="template_castigos.xlsx"'
        return response

    @extend_schema(
        tags=["Catálogos"],
        summary="Cargar tabla de castigos desde Excel",
        description=(
            "Reemplaza atómicamente toda la tabla de castigos para el parámetro "
            "con las filas del archivo .xlsx. "
            "El archivo debe tener tres columnas en orden: `limite_superior`, `descontar`, `bonificar`. "
            "La última fila debe tener el string literal `RECHAZO` en la columna `bonificar`; "
            "esa fila activa el rechazo del lote cuando el valor supera su límite."
        ),
        request={
            "multipart/form-data": {
                "type": "object",
                "required": ["archivo", "parametro_id"],
                "properties": {
                    "archivo": {"type": "string", "format": "binary"},
                    "parametro_id": {"type": "integer"},
                },
            }
        },
        responses={
            200: OpenApiResponse(description="Tabla cargada correctamente"),
            400: OpenApiResponse(description="Error de validación del archivo"),
        },
    )
    @action(detail=False, methods=["post"], url_path="cargar-tabla", parser_classes=[MultiPartParser])
    def cargar_tabla(self, request):
        if openpyxl is None:
            return Response({"detail": "openpyxl no está instalado en el servidor."}, status=500)

        archivo = request.FILES.get("archivo")
        if not archivo:
            raise ValidationError({"archivo": "Se requiere un archivo .xlsx."})

        parametro_id = request.data.get("parametro_id")
        if not parametro_id:
            raise ValidationError({"parametro_id": "Se requiere parametro_id."})

        try:
            parametro_id = int(parametro_id)
        except (TypeError, ValueError):
            raise ValidationError({"detail": "parametro_id debe ser un entero."})

        try:
            parametro = Parametro.objects.get(pk=parametro_id)
        except Parametro.DoesNotExist:
            raise ValidationError({"parametro_id": f"Parámetro con id={parametro_id} no existe."})

        try:
            wb = openpyxl.load_workbook(io.BytesIO(archivo.read()), data_only=True)
        except Exception as exc:
            raise ValidationError({"archivo": f"No se pudo leer el archivo Excel: {exc}"}) from exc

        ws = wb.active
        filas = list(ws.iter_rows(min_row=2, values_only=True))
        if not filas:
            raise ValidationError({"archivo": "El archivo no contiene filas de datos (desde la fila 2)."})

        castigos_nuevos = []
        user = request.user
        for idx, fila in enumerate(filas, start=2):
            if len(fila) < 3:
                raise ValidationError({"archivo": f"Fila {idx}: se esperan 3 columnas (limite_superior, descontar, bonificar)."})

            raw_limite, raw_descontar, raw_bonificar = fila[0], fila[1], fila[2]

            if raw_limite is None:
                raise ValidationError({"archivo": f"Fila {idx}: `limite_superior` no puede estar vacío."})

            try:
                limite_superior = Decimal(str(raw_limite))
            except InvalidOperation:
                raise ValidationError({"archivo": f"Fila {idx}: `limite_superior` inválido: '{raw_limite}'."})

            es_rechazo = str(raw_bonificar).strip().upper() == "RECHAZO" if raw_bonificar is not None else False

            if es_rechazo:
                descontar = Decimal("0")
                bonificar = Decimal("0")
            else:
                try:
                    descontar = Decimal(str(raw_descontar)) if raw_descontar is not None else Decimal("0")
                except InvalidOperation:
                    raise ValidationError({"archivo": f"Fila {idx}: `descontar` inválido: '{raw_descontar}'."})
                try:
                    bonificar = Decimal(str(raw_bonificar)) if raw_bonificar is not None else Decimal("0")
                except InvalidOperation:
                    raise ValidationError({"archivo": f"Fila {idx}: `bonificar` inválido: '{raw_bonificar}'."})

            if limite_superior < 0:
                raise ValidationError({"archivo": f"Fila {idx}: `limite_superior` debe ser >= 0."})
            if descontar < 0:
                raise ValidationError({"archivo": f"Fila {idx}: `descontar` debe ser >= 0."})
            if bonificar < 0:
                raise ValidationError({"archivo": f"Fila {idx}: `bonificar` debe ser >= 0."})

            castigos_nuevos.append(
                Castigo(
                    parametro=parametro,
                    limite_superior=limite_superior,
                    descontar_kg_por_tonelada=descontar,
                    bonificar_por_tonelada=bonificar,
                    es_rechazo=es_rechazo,
                    created_by=user,
                    updated_by=user,
                )
            )

        if not castigos_nuevos:
            raise ValidationError({"archivo": "No se encontraron filas válidas para importar."})

        with transaction.atomic():
            Castigo.objects.filter(parametro=parametro).delete()
            Castigo.objects.bulk_create(castigos_nuevos)

        return Response(
            {
                "detail": f"Tabla cargada correctamente. {len(castigos_nuevos)} filas importadas.",
                "parametro_id": parametro.id,
                "filas_importadas": len(castigos_nuevos),
            },
            status=200,
        )
