import datetime

from django.utils import timezone
from django.db import transaction
from django.db import models as django_models
from django.db.models import Q, Count, CharField, Prefetch, Exists, OuterRef, Subquery
from django.db.models.fields.json import KeyTextTransform
from django.db.models.functions import Cast, Coalesce
from rest_framework import serializers, status, permissions
from rest_framework.pagination import LimitOffsetPagination
from rest_framework.response import Response
from rest_framework.views import APIView

from basic_info import models as basic_models
from business import models as business_models
from printing import models as printing_models
from api_v1.tasks import (
    ExternalPrintingOrderSnapshotSyncError,
    sync_external_printing_order_snapshot_impl,
)
from api_man.serializers import ProductSerializer
from api_v1.views.printing.serializers import (
    PlateOrderListSerializer as PlateOrderListV1Serializer,
    _serialize_plate_images,
)
from stateflow import models as stateflow_models
from stateflow import services as stateflow_services


class IsPrintingFactory(permissions.BasePermission):
    """仅允许印染工厂用户访问（与 api_v1 逻辑保持一致）"""

    message = '您没有访问印染订单的权限'

    def has_permission(self, request, view):
        if not request.user or not request.user.is_authenticated:
            return False
        if hasattr(request.user, 'employee'):
            return request.user.employee.merchant.type == basic_models.MerchantTypeEnum.FACTORY
        return False


def _serialize_plate_image_first(raw_value, request):
    images = _serialize_plate_images(raw_value, request)
    if not images:
        return []
    return images[:1]


class PrintingJobV2Serializer(serializers.ModelSerializer):
    """v2 独立的印染任务序列化器，包含开单数量"""

    external_order_id = serializers.CharField(source='printing_order.external_order_id', read_only=True)
    billed_quantity = serializers.DecimalField(max_digits=18, decimal_places=2, read_only=True)
    business_object_id = serializers.SerializerMethodField()
    width = serializers.SerializerMethodField()
    fabric = serializers.SerializerMethodField()
    curve = serializers.CharField(source='printing_order.curve', read_only=True, allow_null=True)
    new_curve = serializers.CharField(source='printing_order.new_curve', read_only=True, allow_null=True)
    product = ProductSerializer(read_only=True)

    class Meta:
        model = printing_models.PrintingJob
        fields = [
            'id',
            'original_id',
            'sub_id',
            'printing_order',
            'external_order_id',
            'product',
            'work_state',
            'quantity',
            'width',
            'fabric',
            'curve',
            'new_curve',
            'unit',
            'size',
            'pieces',
            'description',
            'business_object_id',
            'created_at',
            'updated_at',
            'billed_quantity',
        ]
        read_only_fields = ['id', 'created_at', 'updated_at', 'external_order_id', 'billed_quantity']

    def get_business_object_id(self, obj):
        return obj.business_object_id

    def get_width(self, obj: printing_models.PrintingJob) -> float:
        return obj.printing_order.width

    def get_fabric(self, obj: printing_models.PrintingJob) -> str:
        return obj.printing_order.fabric


class PrintingJobByCustomerView(APIView):
    """
    按客户与日期范围查询印染任务。

    必填 query 参数:
    - customer_id: 客户 ID
    - date_from: 开始日期 (YYYY-MM-DD)
    - date_to: 结束日期 (YYYY-MM-DD)，闭区间，包含 23:59:59
    可选过滤：
    - printing_order: 按印染主订单 ID
    - product_id / product_name / product_human_id / product_width_size / product_color
    """

    serializer_class = PrintingJobV2Serializer
    permission_classes = [permissions.AllowAny]

    def get(self, request):
        qp = request.query_params
        customer_id = qp.get('customer_id')
        date_from = qp.get('date_from')
        date_to = qp.get('date_to')
        printing_order_id = qp.get('printing_order')
        product_id = qp.get('product_id')
        product_name = qp.get('product_name')
        product_human_id = qp.get('product_human_id')
        product_width_size = qp.get('product_width_size')
        product_color = qp.get('product_color')

        if not customer_id:
            return Response({'detail': 'customer_id 为必填参数'}, status=status.HTTP_400_BAD_REQUEST)
        if not date_from or not date_to:
            return Response({'detail': 'date_from 与 date_to 为必填参数'}, status=status.HTTP_400_BAD_REQUEST)

        try:
            customer_id_int = int(customer_id)
        except (TypeError, ValueError):
            return Response({'detail': 'customer_id 必须为数字'}, status=status.HTTP_400_BAD_REQUEST)

        try:
            start_date = datetime.datetime.strptime(date_from, '%Y-%m-%d').date()
            end_date = datetime.datetime.strptime(date_to, '%Y-%m-%d').date()
        except ValueError:
            return Response({'detail': '日期格式需为 YYYY-MM-DD'}, status=status.HTTP_400_BAD_REQUEST)

        # 闭区间：包含当日 00:00:00 和 23:59:59.999999
        start_dt = datetime.datetime.combine(start_date, datetime.time.min)
        end_dt = datetime.datetime.combine(end_date, datetime.time.max)

        if timezone.is_naive(start_dt):
            start_dt = timezone.make_aware(start_dt, timezone.get_default_timezone())
        if timezone.is_naive(end_dt):
            end_dt = timezone.make_aware(end_dt, timezone.get_default_timezone())

        queryset = printing_models.PrintingJob.objects.select_related('printing_order', 'product').filter(
            printing_order__customer_id=customer_id_int,
            created_at__gte=start_dt,
            created_at__lte=end_dt,
        )

        # 可选过滤：printing_order
        if printing_order_id:
            try:
                queryset = queryset.filter(printing_order_id=int(printing_order_id))
            except (TypeError, ValueError):
                return Response({'detail': 'printing_order 必须为数字'}, status=status.HTTP_400_BAD_REQUEST)

        # 可选过滤：product
        if product_id:
            try:
                queryset = queryset.filter(product_id=int(product_id))
            except (TypeError, ValueError):
                return Response({'detail': 'product_id 必须为数字'}, status=status.HTTP_400_BAD_REQUEST)
        if product_name:
            queryset = queryset.filter(product__name__icontains=product_name)
        if product_human_id:
            queryset = queryset.filter(product__human_id__icontains=product_human_id)
        if product_width_size:
            try:
                width_decimal = float(product_width_size)
            except (TypeError, ValueError):
                return Response({'detail': 'product_width_size 必须为数字'}, status=status.HTTP_400_BAD_REQUEST)
            queryset = queryset.filter(product__width_size=width_decimal)
        if product_color:
            queryset = queryset.filter(product__color__icontains=product_color)

        queryset = queryset.order_by('-created_at')

        serializer = self.serializer_class(queryset, many=True)
        return Response(serializer.data)


class PrintingOrderWithoutSalesOrderSerializer(serializers.ModelSerializer):
    """未关联非作废销售单的印染订单列表序列化器。"""

    customer_name = serializers.CharField(source='customer.name', read_only=True)

    class Meta:
        model = printing_models.PrintingOrder
        fields = [
            'id',
            'human_id',
            'external_order_id',
            'customer',
            'customer_name',
            'fabric',
            'width',
            'is_urgent',
            'is_invalid',
            'outgoing_date',
            'created_at',
            'updated_at',
        ]
        read_only_fields = fields


class PrintingOrderWithoutSalesOrderView(APIView):
    """
    按客户查询尚未关联非作废销售单的印染订单。

    关联路径:
    PrintingOrder -> PrintingJob -> SalesOrderItem -> SalesOrder
    已作废 SalesOrder 不计为有效关联。
    """

    serializer_class = PrintingOrderWithoutSalesOrderSerializer
    permission_classes = [permissions.IsAuthenticated, IsPrintingFactory]

    def get(self, request):
        customer_id = request.query_params.get('customer_id')
        if not customer_id:
            return Response({'detail': 'customer_id 为必填参数'}, status=status.HTTP_400_BAD_REQUEST)

        try:
            customer_id_int = int(customer_id)
        except (TypeError, ValueError):
            return Response({'detail': 'customer_id 必须为数字'}, status=status.HTTP_400_BAD_REQUEST)

        merchant = request.user.employee.merchant
        active_sales_order_items = business_models.SalesOrderItem.objects.filter(
            printing_job__printing_order_id=OuterRef('pk'),
        ).exclude(
            sales_order__status=business_models.SalesOrderStatusEnum.CANCELLED,
        )

        queryset = (
            printing_models.PrintingOrder.objects
            .select_related('customer')
            .filter(customer_id=customer_id_int)
            .filter(Q(merchant=merchant) | Q(merchant__isnull=True, customer__merchant=merchant))
            .annotate(_has_active_sales_order=Exists(active_sales_order_items))
            .filter(_has_active_sales_order=False)
            .order_by('-created_at', '-id')
        )

        serializer = self.serializer_class(queryset, many=True)
        return Response(serializer.data)


class PrintingJobBatchAdvancePreviewRequestSerializer(serializers.Serializer):
    """批量推进：预览/校验请求"""

    printing_job_ids = serializers.ListField(
        child=serializers.IntegerField(min_value=1),
        allow_empty=False,
        help_text='需要批量推进的 printing_job id 列表',
    )

    def validate_printing_job_ids(self, value):
        # 去重保持稳定性（前端可能重复传）
        deduped = list(dict.fromkeys(value))
        if not deduped:
            raise serializers.ValidationError('printing_job_ids 不能为空')
        return deduped


class PrintingJobBatchAdvanceSubmitRequestSerializer(PrintingJobBatchAdvancePreviewRequestSerializer):
    """批量推进：提交请求"""

    parameters = serializers.DictField(
        child=serializers.JSONField(),
        required=False,
        default=dict,
        help_text='与单条推进接口一致的工艺参数（将作为 **kwargs 传给 stateflow）',
    )


def _validate_jobs_for_batch_advance(printing_job_ids: list[int]):
    """
    批量推进的核心一致性校验（preview 与 submit 共用）

    规则：
    1) 所有 id 都存在
    2) 全部属于同一个 printing_order
    3) 全部存在 business_object（流程实例）
    4) 全部具有相同的 next_pending_state（下一待执行节点），否则不允许批量

    注意：这里不做“竞态”处理（preview 后 submit 前状态变化），submit 时会再次调用该函数重新校验。
    后续如需增强，可在 preview 返回 snapshot token，在 submit 校验 token 以提升用户体验。
    """
    # 查询并校验存在性
    qs = (
        printing_models.PrintingJob.objects
        .select_related('printing_order', 'business_object', 'business_object__process')
        .filter(id__in=printing_job_ids)
    )
    jobs = list(qs)
    found_ids = {j.id for j in jobs}
    missing_ids = [str(i) for i in printing_job_ids if i not in found_ids]
    if missing_ids:
        raise serializers.ValidationError({'detail': f'以下 printing_job 不存在: {", ".join(missing_ids)}'})

    # 同一订单
    order_ids = {j.printing_order_id for j in jobs}
    if len(order_ids) != 1:
        raise serializers.ValidationError({'detail': '所选明细不属于同一个 printing_order，无法批量推进'})
    printing_order_id = next(iter(order_ids))
    printing_order = jobs[0].printing_order

    # 必须有关联流程实例
    no_bo = [str(j.id) for j in jobs if not j.business_object_id]
    if no_bo:
        raise serializers.ValidationError({'detail': f'以下 printing_job 未关联流程实例（business_object），无法推进: {", ".join(no_bo)}'})

    # 计算并校验 next_pending_state 一致
    from stateflow import services as stateflow_services

    next_infos = []
    for j in jobs:
        info = stateflow_services.get_next_pending_state(j.business_object, include_parameters=True)
        if info is None:
            next_infos.append((j.id, None))
        else:
            next_infos.append((j.id, info))

    # 不能包含“无待执行节点”（流程已完成或无节点）
    cannot_advance = [str(job_id) for job_id, info in next_infos if info is None]
    if cannot_advance:
        raise serializers.ValidationError({'detail': f'以下 printing_job 没有待执行节点（流程已完成或无节点），无法批量推进: {", ".join(cannot_advance)}'})

    # 比对 state_id
    first_info = next_infos[0][1]
    target_state = first_info['state']
    target_order = first_info['order']
    target_state_id = target_state.id

    diff_jobs = []
    for job_id, info in next_infos:
        if info['state'].id != target_state_id:
            diff_jobs.append(str(job_id))
    if diff_jobs:
        raise serializers.ValidationError({'detail': f'所选明细当前待执行节点不一致，无法批量推进（不同节点的 jobs: {", ".join(diff_jobs)}）'})

    # 参数定义取目标节点（所有一致）
    target_parameters = first_info.get('parameters', []) or []

    return {
        'printing_order': printing_order,
        'printing_order_id': printing_order_id,
        'jobs': jobs,
        'target_state': target_state,
        'target_order': target_order,
        'target_parameters': target_parameters,
    }


class PrintingJobBatchAdvancePreviewView(APIView):
    """
    批量推进：预览

    作用：
    - 校验 printing_job_ids 是否可批量推进（同订单/同待执行节点）
    - 返回“下一步待执行节点”的信息及其工艺参数定义，供前端生成批量表单
    """

    permission_classes = [permissions.IsAuthenticated, IsPrintingFactory]

    def post(self, request):
        srz = PrintingJobBatchAdvancePreviewRequestSerializer(data=request.data)
        srz.is_valid(raise_exception=True)

        data = _validate_jobs_for_batch_advance(srz.validated_data['printing_job_ids'])

        from stateflow.serializers import StateParameterSerializer

        params_srz = StateParameterSerializer(
            data['target_parameters'],
            many=True,
            context={'request': request},
        )

        return Response({
            'printing_order_id': data['printing_order_id'],
            'printing_job_ids': [j.id for j in data['jobs']],
            'next_state': {
                'id': data['target_state'].id,
                'name': data['target_state'].name,
                'description': data['target_state'].description,
                'order': data['target_order'],
                'parameters': params_srz.data,
            }
        })


class PrintingJobBatchAdvanceSubmitView(APIView):
    """
    批量推进：提交

    规则：全成功/全失败
    - 任意一个 job 推进失败：整体回滚（不产生任何 stateflow 日志，也不产生批量推进记录）
    """

    permission_classes = [permissions.IsAuthenticated, IsPrintingFactory]

    def post(self, request):
        srz = PrintingJobBatchAdvanceSubmitRequestSerializer(data=request.data)
        srz.is_valid(raise_exception=True)

        payload = srz.validated_data
        parameters = payload.get('parameters') or {}

        data = _validate_jobs_for_batch_advance(payload['printing_job_ids'])

        from stateflow import services as stateflow_services

        # 全成功/全失败：用事务包住整个批量推进
        with transaction.atomic():
            record = printing_models.PrintingJobBatchAdvanceRecord.objects.create(
                printing_order=data['printing_order'],
                state=data['target_state'],
                created_by=request.user,
                parameters=parameters,
            )
            record.printing_jobs.set(data['jobs'])

            # 逐个复用单条推进逻辑
            last_message = None
            for job in data['jobs']:
                # 修复历史/异常数据：确保该 job 的流程实例正确绑定到 job（避免 BusinessObject.object_id/content_type 为空）
                stateflow_services.ensure_business_object_bound_to_instance(
                    job.business_object,
                    job,
                    default_name=f"PrintingJob-{job.id}",
                )
                ok, msg, _state_log = stateflow_services.advance_to_next_state(
                    job.business_object,
                    request.user,
                    **parameters
                )
                if not ok:
                    # 抛异常触发事务回滚，保证“全部失败”
                    raise serializers.ValidationError({'detail': msg})
                last_message = msg

        # 返回最新的 job 列表（可用于前端刷新）
        refreshed_jobs = (
            printing_models.PrintingJob.objects
            .select_related('printing_order', 'product')
            .filter(id__in=[j.id for j in data['jobs']])
            .order_by('id')
        )
        job_srz = PrintingJobV2Serializer(refreshed_jobs, many=True)

        return Response({
            'detail': last_message or '批量推进成功',
            'batch_id': record.id,
            'printing_order_id': data['printing_order_id'],
            'printing_job_ids': [j.id for j in data['jobs']],
            'jobs': job_srz.data,
        })


class PlateOrderByProcessNodeSerializer(serializers.ModelSerializer):
    """按流程节点筛选 PlateOrder 的列表序列化（尽量保持轻量）"""

    design_code = serializers.SerializerMethodField()
    customer_name = serializers.CharField(source='customer.name', read_only=True)
    business_object_id = serializers.SerializerMethodField()
    created_by = serializers.IntegerField(source='created_by_id', read_only=True)
    process_parameters = serializers.SerializerMethodField()
    plate_image = serializers.SerializerMethodField()

    class Meta:
        model = printing_models.PlateOrder
        fields = [
            'id',
            'original_id',
            'design_code',
            'plate_image',
            'customer',
            'customer_name',
            'style_name',
            'urgency_level',
            'is_invalid',
            'business_object_id',
            'created_by',
            'process_parameters',
            'created_at',
            'updated_at',
        ]
        read_only_fields = fields

    def get_design_code(self, obj: printing_models.PlateOrder) -> str | None:
        return obj.design_code or (str(obj.id) if obj.id else None)

    def get_business_object_id(self, obj: printing_models.PlateOrder) -> int | None:
        return obj.business_object_id

    def get_process_parameters(self, obj: printing_models.PlateOrder) -> list[dict]:
        """
        返回“订单维度”的工艺参数 key/value（当前 process_node 对应 state 的参数）。

        取值来源：
        - 优先取该订单 business_object 在目标 state 的**最新一次 StateFlowRecord**（可能是已撤销记录）
          对应的 StateLogParameterRecord 汇总（后提交覆盖先提交）。
        - 若从未提交过该 state 的参数：value 为 null（由前端自行用顶层 parameters 的默认值做兜底/占位）
        """
        keys: list[str] = self.context.get('target_parameter_keys') or []
        target_state_id: int | None = self.context.get('target_state_id')
        if not keys or not target_state_id:
            return [{'key': k, 'value': None} for k in keys]

        bo = getattr(obj, 'business_object', None)
        if not bo:
            return [{'key': k, 'value': None} for k in keys]

        logs = getattr(bo, '_prefetched_target_state_logs', None)
        if logs is None:
            # fallback：极少数情况下未预取
            logs = list(
                bo.state_logs.filter(state_id=target_state_id).order_by('-completed_at', '-id')[:1]
            )

        latest_log = logs[0] if logs else None
        summary: dict = {}
        if latest_log:
            param_records = getattr(latest_log, '_prefetched_parameter_records', None)
            if param_records is None:
                param_records = list(latest_log.parameter_records.all().order_by('created_at', 'id'))
            for rec in param_records:
                summary.update(rec.parameters or {})

        return [{'key': k, 'value': summary.get(k)} for k in keys]

    def get_plate_image(self, obj: printing_models.PlateOrder) -> list[dict]:
        return _serialize_plate_image_first(getattr(obj, 'plate_image', None), self.context.get('request'))


class PlateOrderByProcessNodeView(APIView):
    """
    按 process_node_id 查询“当前处于该节点（NEXT 模式：下一个待执行节点）”的 PlateOrder 列表。

    GET /api/v2/plate-orders/by-process-node/

    Query 参数:
    - process_node_id: 必填，ProcessNode.id
    - search: 可选。支持:
        - 纯数字：同时匹配 id 精确 + design_code icontains
        - 非纯数字：design_code icontains
    - ordering: 可选，默认 -created_at，支持: id / created_at / updated_at / design_code
    - limit/offset: 分页（limit 默认 20）
    """

    permission_classes = [permissions.IsAuthenticated, IsPrintingFactory]

    _ORDERING_FIELDS = {'id', 'created_at', 'updated_at', 'design_code'}

    def get(self, request):
        qp = request.query_params
        process_node_id = qp.get('process_node_id')
        param_key = (qp.get('param_key') or '').strip()
        param_value = (qp.get('param_value') or '').strip()
        date_from = qp.get('date_from')
        date_to = qp.get('date_to')
        if not process_node_id:
            return Response({'detail': 'process_node_id 为必填参数'}, status=status.HTTP_400_BAD_REQUEST)
        if (param_key and not param_value) or (param_value and not param_key):
            return Response({'detail': 'param_key 与 param_value 必须同时提供'}, status=status.HTTP_400_BAD_REQUEST)
        if (date_from and not date_to) or (date_to and not date_from):
            return Response({'detail': 'date_from 与 date_to 必须同时提供'}, status=status.HTTP_400_BAD_REQUEST)

        try:
            process_node_id_int = int(process_node_id)
        except (TypeError, ValueError):
            return Response({'detail': 'process_node_id 必须为数字'}, status=status.HTTP_400_BAD_REQUEST)

        try:
            process_node = (
                stateflow_models.ProcessNode.objects
                .select_related('process', 'state')
                .get(id=process_node_id_int)
            )
        except stateflow_models.ProcessNode.DoesNotExist:
            return Response({'detail': 'process_node 不存在'}, status=status.HTTP_404_NOT_FOUND)

        if date_from and date_to:
            try:
                start_date = datetime.datetime.strptime(date_from, '%Y-%m-%d').date()
                end_date = datetime.datetime.strptime(date_to, '%Y-%m-%d').date()
            except ValueError:
                return Response({'detail': '日期格式需为 YYYY-MM-DD'}, status=status.HTTP_400_BAD_REQUEST)

            # 闭区间：包含当日 00:00:00 和 23:59:59.999999
            start_dt = datetime.datetime.combine(start_date, datetime.time.min)
            end_dt = datetime.datetime.combine(end_date, datetime.time.max)

            if timezone.is_naive(start_dt):
                start_dt = timezone.make_aware(start_dt, timezone.get_default_timezone())
            if timezone.is_naive(end_dt):
                end_dt = timezone.make_aware(end_dt, timezone.get_default_timezone())

        # 目标节点信息
        target_state_id = process_node.state_id
        target_order = process_node.order
        process_id = process_node.process_id

        # 目标节点之前的所有 state_id（distinct，便于计数）
        prev_state_ids = list(
            stateflow_models.ProcessNode.objects
            .filter(process_id=process_id, order__lt=target_order)
            .order_by('order', 'id')
            .values_list('state_id', flat=True)
            .distinct()
        )

        queryset = (
            printing_models.PlateOrder.objects
            .select_related('customer', 'business_object')
            .filter(business_object__isnull=False, business_object__process_id=process_id)
        )

        if date_from and date_to:
            queryset = queryset.filter(created_at__gte=start_dt, created_at__lte=end_dt)

        # COMPLETED 模式判定：目标节点已完成（未撤销）
        if prev_state_ids:
            queryset = queryset.annotate(
                completed_prev_states=Count(
                    'business_object__state_logs__state_id',
                    filter=Q(
                        business_object__state_logs__is_cancelled=False,
                        business_object__state_logs__state_id__in=prev_state_ids,
                    ),
                    distinct=True,
                )
            ).filter(completed_prev_states=len(prev_state_ids))

        queryset = queryset.annotate(
            completed_target=Count(
                'business_object__state_logs__id',
                filter=Q(
                    business_object__state_logs__is_cancelled=False,
                    business_object__state_logs__state_id=target_state_id,
                ),
                distinct=True,
            )
        ).filter(completed_target__gt=0)

        if param_key and param_value:
            matching_logs = stateflow_models.StateFlowRecord.objects.filter(
                business_object_id=OuterRef('business_object_id'),
                state_id=target_state_id,
                is_cancelled=False,
                parameter_records__parameters__has_key=param_key,
            ).annotate(
                _param_value=KeyTextTransform(param_key, 'parameter_records__parameters')
            ).filter(
                _param_value__icontains=param_value,
            )
            queryset = queryset.annotate(_has_state_param=Exists(matching_logs)).filter(_has_state_param=True)

        # search：同时支持主键与 design_code icontains（不新增额外参数）
        search = (qp.get('search') or '').strip()
        if search:
            if search.isdigit():
                try:
                    search_id = int(search)
                except (TypeError, ValueError):
                    search_id = None
                cond = Q(design_code__icontains=search)
                if search_id is not None:
                    cond = cond | Q(id=search_id)
                queryset = queryset.filter(cond)
            else:
                queryset = queryset.filter(design_code__icontains=search)

        # ordering：默认 -created_at
        ordering = (qp.get('ordering') or '-created_at').strip() or '-created_at'
        direction = '-' if ordering.startswith('-') else ''
        field = ordering[1:] if ordering.startswith('-') else ordering
        if field not in self._ORDERING_FIELDS:
            return Response(
                {'detail': f'ordering 不支持: {ordering}（可选: {", ".join(sorted(self._ORDERING_FIELDS))}）'},
                status=status.HTTP_400_BAD_REQUEST,
            )

        # 为 design_code 排序提供兜底：为空时按主键字符串
        if field == 'design_code':
            queryset = queryset.annotate(
                design_code_normalized=Coalesce('design_code', Cast('id', output_field=CharField()))
            ).order_by(f'{direction}design_code_normalized', 'id')
        else:
            queryset = queryset.order_by(f'{direction}{field}', 'id')

        # 该节点参数模板（仅 key/value），同时用于 results[*].process_parameters 的 key 集合
        params = list(process_node.state.parameters.order_by('id').values('key', 'value'))
        target_keys = [p['key'] for p in params]

        # 预取：目标 state 的最新日志及其参数记录（用于拼装“订单维度”的参数值，避免 N+1）
        param_records_prefetch = Prefetch(
            'parameter_records',
            queryset=stateflow_models.StateLogParameterRecord.objects.order_by('created_at', 'id'),
            to_attr='_prefetched_parameter_records',
        )
        target_state_logs_qs = (
            stateflow_models.StateFlowRecord.objects
            .filter(state_id=target_state_id)
            .order_by('-completed_at', '-id')
            .prefetch_related(param_records_prefetch)
        )
        queryset = queryset.prefetch_related(
            Prefetch(
                'business_object__state_logs',
                queryset=target_state_logs_qs,
                to_attr='_prefetched_target_state_logs',
            )
        )

        # 分页（limit 默认 20）
        paginator = LimitOffsetPagination()
        paginator.default_limit = 20
        page = paginator.paginate_queryset(queryset, request, view=self)
        results = page if page is not None else list(queryset)

        srz = PlateOrderByProcessNodeSerializer(
            results,
            many=True,
            context={
                'request': request,
                'target_state_id': target_state_id,
                'target_parameter_keys': target_keys,
            },
        )

        return Response({
            'process_node': {
                'id': process_node.id,
                'process_id': process_id,
                'state_id': target_state_id,
                'state_name': process_node.state.name,
                'order': target_order,
            },
            'parameters': params,
            'count': getattr(paginator, 'count', len(results)),
            'next': paginator.get_next_link() if page is not None else None,
            'previous': paginator.get_previous_link() if page is not None else None,
            'results': srz.data,
        })


class PlateOrderByProcessSerializer(serializers.ModelSerializer):
    """按流程（process_id）查询 PlateOrder，附带所有节点的参数汇总（订单维度）"""

    design_code = serializers.SerializerMethodField()
    customer_name = serializers.CharField(source='customer.name', read_only=True)
    business_object_id = serializers.SerializerMethodField()
    created_by = serializers.IntegerField(source='created_by_id', read_only=True)
    process_params = serializers.SerializerMethodField()
    plate_image = serializers.SerializerMethodField()

    class Meta:
        model = printing_models.PlateOrder
        fields = [
            'id',
            'original_id',
            'design_code',
            'plate_image',
            'customer',
            'customer_name',
            'style_name',
            'urgency_level',
            'is_invalid',
            'business_object_id',
            'created_by',
            'process_params',
            'created_at',
            'updated_at',
        ]
        read_only_fields = fields

    def get_design_code(self, obj: printing_models.PlateOrder) -> str | None:
        return obj.design_code or (str(obj.id) if obj.id else None)

    def get_business_object_id(self, obj: printing_models.PlateOrder) -> int | None:
        return obj.business_object_id

    def get_process_params(self, obj: printing_models.PlateOrder) -> list[dict]:
        """
        返回所有流程节点的参数视图（订单维度）。

        约束（前端可依赖）：
        - 一定包含 process 的全部节点（按 order 升序）
        - 每个节点包含 is_executed（默认不含撤销记录）
        - params 的 key/顺序与该节点 State.parameters 的 key/顺序一致
        - 未执行或未提交的参数 value 为 null
        """
        nodes_info: list[dict] = self.context.get('process_nodes_info') or []
        if not nodes_info:
            return []

        bo = getattr(obj, 'business_object', None)
        if not bo:
            # 没有关联流程实例：全部视为未执行
            result = []
            for node in nodes_info:
                result.append({
                    'process_node_id': node['process_node_id'],
                    'state_id': node['state_id'],
                    'node_name': node['node_name'],
                    'order': node['order'],
                    'is_executed': False,
                    'params': [{'key': k, 'value': None} for k in (node.get('keys') or [])],
                })
            return result

        logs = getattr(bo, '_prefetched_state_logs_for_process_params', None)
        if logs is None:
            state_ids = [n['state_id'] for n in nodes_info]
            logs = list(
                bo.state_logs.filter(is_cancelled=False, state_id__in=state_ids)
                .order_by('-completed_at', '-id')
                .prefetch_related('parameter_records')
            )

        # logs 已按时间倒序：第一次出现的 state_id 即“最新一次非撤销执行记录”
        latest_log_by_state: dict[int, stateflow_models.StateFlowRecord] = {}
        for log in logs:
            if log.state_id not in latest_log_by_state:
                latest_log_by_state[log.state_id] = log

        result = []
        for node in nodes_info:
            state_id = node['state_id']
            keys = node.get('keys') or []
            log = latest_log_by_state.get(state_id)
            is_executed = log is not None

            summary: dict = {}
            if log is not None:
                param_records = getattr(log, '_prefetched_parameter_records', None)
                if param_records is None:
                    param_records = list(log.parameter_records.all().order_by('created_at', 'id'))
                for rec in param_records:
                    summary.update(rec.parameters or {})

            result.append({
                'process_node_id': node['process_node_id'],
                'state_id': state_id,
                'node_name': node['node_name'],
                'order': node['order'],
                'is_executed': is_executed,
                'params': [{'key': k, 'value': summary.get(k) if is_executed else None} for k in keys],
            })
        return result

    def get_plate_image(self, obj: printing_models.PlateOrder) -> list[dict]:
        return _serialize_plate_image_first(getattr(obj, 'plate_image', None), self.context.get('request'))


class PlateOrderByStateStatusSerializer(PlateOrderListV1Serializer):
    """按单个节点状态过滤 PlateOrder，并扩展状态流信息"""

    state_parameters = serializers.SerializerMethodField()
    state_log = serializers.SerializerMethodField()
    state_status = serializers.SerializerMethodField()

    class Meta(PlateOrderListV1Serializer.Meta):
        fields = list(PlateOrderListV1Serializer.Meta.fields) + [
            'state_status',
            'state_parameters',
            'state_log',
        ]
        read_only_fields = list(PlateOrderListV1Serializer.Meta.read_only_fields) + [
            'state_status',
            'state_parameters',
            'state_log',
        ]

    def get_state_status(self, obj: printing_models.PlateOrder) -> str:
        return self.context.get('requested_status', '')

    def _get_latest_state_log(self, obj: printing_models.PlateOrder):
        bo = getattr(obj, 'business_object', None)
        if not bo:
            return None
        logs = getattr(bo, '_prefetched_target_state_logs', None)
        if logs:
            return logs[0]
        target_state_id = self.context.get('target_state_id')
        if not target_state_id:
            return None
        return (
            bo.state_logs
            .filter(state_id=target_state_id)
            .order_by('-completed_at', '-id')
            .select_related('completed_by')
            .first()
        )

    def get_state_parameters(self, obj: printing_models.PlateOrder) -> list[dict]:
        keys: list[str] = self.context.get('target_parameter_keys') or []
        if not keys:
            return []

        bo = getattr(obj, 'business_object', None)
        if not bo:
            return [{'key': k, 'value': None} for k in keys]

        latest_log = self._get_latest_state_log(obj)
        if latest_log is None:
            return [{'key': k, 'value': None} for k in keys]

        param_records = getattr(latest_log, '_prefetched_parameter_records', None)
        if param_records is None:
            param_records = list(latest_log.parameter_records.all().order_by('created_at', 'id'))

        summary: dict = {}
        for rec in param_records:
            summary.update(rec.parameters or {})

        return [{'key': k, 'value': summary.get(k)} for k in keys]

    def get_state_log(self, obj: printing_models.PlateOrder) -> dict | None:
        log = self._get_latest_state_log(obj)
        if log is None:
            return None
        completed_by = log.completed_by
        return {
            'id': log.id,
            'state_id': log.state_id,
            'completed_at': log.completed_at,
            'completed_by': completed_by.id if completed_by else None,
            'completed_by_username': completed_by.username if completed_by else None,
            'is_cancelled': log.is_cancelled,
        }

    def to_representation(self, instance):
        data = super().to_representation(instance)
        plate_image = data.get('plate_image')
        if isinstance(plate_image, list):
            data['plate_image'] = plate_image[:1]
        return data


class PlateOrderByProcessView(APIView):
    """
    按 process_id 查询 PlateOrder 列表，并在每条 PlateOrder 中返回 process 的所有节点参数结构。

    GET /api/v2/plate-orders/by-process/

    Query 参数:
    - process_id: 必填，Process.id
    - date_from/date_to: 必填，YYYY-MM-DD（按 PlateOrder.created_at 闭区间过滤）
    - plate_order: 可选。支持:
        - 纯数字：同时匹配 id 精确 + design_code icontains
        - 非纯数字：design_code icontains
    - ordering: 可选，默认 -created_at，支持: id / created_at / updated_at / design_code
    - limit/offset: 分页（limit 默认 20）
    """

    permission_classes = [permissions.IsAuthenticated, IsPrintingFactory]

    _ORDERING_FIELDS = {'id', 'created_at', 'updated_at', 'design_code'}

    def get(self, request):
        qp = request.query_params

        process_id = qp.get('process_id')
        if not process_id:
            return Response({'detail': 'process_id 为必填参数'}, status=status.HTTP_400_BAD_REQUEST)
        try:
            process_id_int = int(process_id)
        except (TypeError, ValueError):
            return Response({'detail': 'process_id 必须为数字'}, status=status.HTTP_400_BAD_REQUEST)

        date_from = qp.get('date_from')
        date_to = qp.get('date_to')
        if not date_from or not date_to:
            return Response({'detail': 'date_from 与 date_to 为必填参数'}, status=status.HTTP_400_BAD_REQUEST)
        try:
            start_date = datetime.datetime.strptime(date_from, '%Y-%m-%d').date()
            end_date = datetime.datetime.strptime(date_to, '%Y-%m-%d').date()
        except ValueError:
            return Response({'detail': '日期格式需为 YYYY-MM-DD'}, status=status.HTTP_400_BAD_REQUEST)

        # 闭区间：包含当日 00:00:00 和 23:59:59.999999
        start_dt = datetime.datetime.combine(start_date, datetime.time.min)
        end_dt = datetime.datetime.combine(end_date, datetime.time.max)
        if timezone.is_naive(start_dt):
            start_dt = timezone.make_aware(start_dt, timezone.get_default_timezone())
        if timezone.is_naive(end_dt):
            end_dt = timezone.make_aware(end_dt, timezone.get_default_timezone())

        try:
            process = stateflow_models.Process.objects.get(id=process_id_int)
        except stateflow_models.Process.DoesNotExist:
            return Response({'detail': 'process 不存在'}, status=status.HTTP_404_NOT_FOUND)

        # 该 process 的所有节点（含参数定义 key 顺序）
        params_prefetch = Prefetch(
            'state__parameters',
            queryset=stateflow_models.StateParameter.objects.order_by('id'),
        )
        process_nodes = list(
            stateflow_models.ProcessNode.objects
            .filter(process_id=process_id_int)
            .select_related('state')
            .prefetch_related(params_prefetch)
            .order_by('order', 'id')
        )
        process_nodes_info = []
        state_ids = []
        for pn in process_nodes:
            keys = [p.key for p in pn.state.parameters.all()]
            process_nodes_info.append({
                'process_node_id': pn.id,
                'state_id': pn.state_id,
                'node_name': pn.state.name,
                'order': pn.order,
                'keys': keys,
            })
            state_ids.append(pn.state_id)

        queryset = (
            printing_models.PlateOrder.objects
            .select_related('customer', 'business_object')
            .filter(
                process=process_id_int,
                created_at__gte=start_dt,
                created_at__lte=end_dt,
            )
        )

        # plate_order：同时支持主键与 design_code icontains（不新增额外参数）
        plate_order = (qp.get('plate_order') or '').strip()
        if plate_order:
            if plate_order.isdigit():
                try:
                    pid = int(plate_order)
                except (TypeError, ValueError):
                    pid = None
                cond = Q(design_code__icontains=plate_order)
                if pid is not None:
                    cond = cond | Q(id=pid)
                queryset = queryset.filter(cond)
            else:
                queryset = queryset.filter(design_code__icontains=plate_order)

        # ordering：默认 -created_at
        ordering = (qp.get('ordering') or '-created_at').strip() or '-created_at'
        direction = '-' if ordering.startswith('-') else ''
        field = ordering[1:] if ordering.startswith('-') else ordering
        if field not in self._ORDERING_FIELDS:
            return Response(
                {'detail': f'ordering 不支持: {ordering}（可选: {", ".join(sorted(self._ORDERING_FIELDS))}）'},
                status=status.HTTP_400_BAD_REQUEST,
            )

        if field == 'design_code':
            queryset = queryset.annotate(
                design_code_normalized=Coalesce('design_code', Cast('id', output_field=CharField()))
            ).order_by(f'{direction}design_code_normalized', 'id')
        else:
            queryset = queryset.order_by(f'{direction}{field}', 'id')

        # 预取：非撤销的状态日志及其参数记录（用于计算 is_executed + value）
        state_ids_unique = sorted(set(state_ids))
        param_records_prefetch = Prefetch(
            'parameter_records',
            queryset=stateflow_models.StateLogParameterRecord.objects.order_by('created_at', 'id'),
            to_attr='_prefetched_parameter_records',
        )
        logs_qs = (
            stateflow_models.StateFlowRecord.objects
            .filter(is_cancelled=False)
            .order_by('-completed_at', '-id')
            .prefetch_related(param_records_prefetch)
        )
        if state_ids_unique:
            logs_qs = logs_qs.filter(state_id__in=state_ids_unique)
        queryset = queryset.prefetch_related(
            Prefetch(
                'business_object__state_logs',
                queryset=logs_qs,
                to_attr='_prefetched_state_logs_for_process_params',
            )
        )

        paginator = LimitOffsetPagination()
        paginator.default_limit = 20
        page = paginator.paginate_queryset(queryset, request, view=self)
        results = page if page is not None else list(queryset)

        srz = PlateOrderByProcessSerializer(
            results,
            many=True,
            context={
                'request': request,
                'process_nodes_info': process_nodes_info,
            },
        )

        return Response({
            'process': {
                'id': process.id,
                'name': process.name,
                'node_count': len(process_nodes_info),
            },
            'count': getattr(paginator, 'count', len(results)),
            'next': paginator.get_next_link() if page is not None else None,
            'previous': paginator.get_previous_link() if page is not None else None,
            'results': srz.data,
        })


class PlateOrderByStateStatusView(APIView):
    """按 process_id + state_id + status 过滤 PlateOrder。"""

    permission_classes = [permissions.IsAuthenticated, IsPrintingFactory]

    _ORDERING_FIELDS = {'id', 'created_at', 'updated_at', 'design_code'}
    _SUPPORTED_STATUS = {'completed', 'not_started', 'cancelled', 'in_progress'}

    def get(self, request):
        qp = request.query_params

        process_id = qp.get('process_id')
        if not process_id:
            return Response({'detail': 'process_id 为必填参数'}, status=status.HTTP_400_BAD_REQUEST)
        try:
            process_id_int = int(process_id)
        except (TypeError, ValueError):
            return Response({'detail': 'process_id 必须为数字'}, status=status.HTTP_400_BAD_REQUEST)

        state_id_raw = qp.get('state_id')
        state_id_int: int | None = None
        if state_id_raw not in (None, ''):
            try:
                state_id_int = int(state_id_raw)
            except (TypeError, ValueError):
                return Response({'detail': 'state_id 必须为数字'}, status=status.HTTP_400_BAD_REQUEST)

        status_value = (qp.get('status') or 'completed').strip().lower()
        if state_id_int is None:
            if qp.get('status') and status_value != 'not_started':
                return Response({'detail': 'state_id 为空时仅支持 status=not_started'}, status=status.HTTP_400_BAD_REQUEST)
            status_value = 'not_started'
        elif status_value == 'not_started':
            return Response({'detail': 'state_id 不为空时不支持 status=not_started'}, status=status.HTTP_400_BAD_REQUEST)
        elif status_value not in self._SUPPORTED_STATUS:
            options = ', '.join(sorted(self._SUPPORTED_STATUS))
            return Response({'detail': f'status 不支持: {status_value}（可选: {options}）'}, status=status.HTTP_400_BAD_REQUEST)

        try:
            process = stateflow_models.Process.objects.get(id=process_id_int)
        except stateflow_models.Process.DoesNotExist:
            return Response({'detail': 'process 不存在'}, status=status.HTTP_404_NOT_FOUND)

        state = None
        state_parameters_template: list[dict] = []
        target_keys: list[str] = []
        representative_order: int | None = None
        process_node_ids: list[int] = []

        base_bo = stateflow_models.BusinessObject.objects.filter(
            process_id=process_id_int,
            plate_order__isnull=False,
        )

        if state_id_int is None:
            filtered_bo = base_bo.annotate(
                has_any_log=Exists(
                    stateflow_models.StateFlowRecord.objects.filter(business_object_id=OuterRef('pk'))
                )
            ).filter(has_any_log=False)
            queryset = (
                printing_models.PlateOrder.objects
                .select_related('customer', 'business_object', 'created_by')
                .filter(process=process_id_int, business_object_id__in=filtered_bo.values('id'))
            )
        else:
            try:
                state = stateflow_models.State.objects.get(id=state_id_int)
            except stateflow_models.State.DoesNotExist:
                return Response({'detail': 'state 不存在'}, status=status.HTTP_404_NOT_FOUND)

            process_nodes = list(
                stateflow_models.ProcessNode.objects
                .filter(process_id=process_id_int, state_id=state_id_int)
                .order_by('order', 'id')
            )
            if not process_nodes:
                return Response({'detail': 'state 不属于该 process'}, status=status.HTTP_400_BAD_REQUEST)

            state_parameters_template = [
                {'key': param.key, 'value': param.value}
                for param in state.parameters.order_by('id')
            ]
            target_keys = [item['key'] for item in state_parameters_template]
            representative_order = process_nodes[0].order
            process_node_ids = [node.id for node in process_nodes]

            filtered_bo = stateflow_services.query_business_objects_by_state_status(
                state_ids=[state_id_int],
                status=status_value,
                process_id=process_id_int,
                base_queryset=base_bo,
            )

            queryset = (
                printing_models.PlateOrder.objects
                .select_related('customer', 'business_object', 'created_by')
                .filter(process=process_id_int, business_object_id__in=filtered_bo.values('id'))
            )

            param_records_prefetch = Prefetch(
                'parameter_records',
                queryset=stateflow_models.StateLogParameterRecord.objects.order_by('created_at', 'id'),
                to_attr='_prefetched_parameter_records',
            )
            target_logs_qs = (
                stateflow_models.StateFlowRecord.objects
                .filter(state_id=state_id_int)
                .order_by('-completed_at', '-id')
                .select_related('completed_by')
                .prefetch_related(param_records_prefetch)
            )
            queryset = queryset.prefetch_related(
                Prefetch(
                    'business_object__state_logs',
                    queryset=target_logs_qs,
                    to_attr='_prefetched_target_state_logs',
                )
            )

            if status_value == 'completed':
                latest_completed_state_subquery = (
                    stateflow_models.StateFlowRecord.objects
                    .filter(
                        business_object_id=OuterRef('business_object_id'),
                        is_cancelled=False,
                    )
                    .order_by('-completed_at', '-id')
                )
                queryset = queryset.annotate(
                    latest_completed_state_id=Subquery(
                        latest_completed_state_subquery.values('state_id')[:1]
                    )
                ).filter(latest_completed_state_id=state_id_int)

        ordering = (qp.get('ordering') or '-created_at').strip() or '-created_at'
        direction = '-' if ordering.startswith('-') else ''
        field = ordering[1:] if ordering.startswith('-') else ordering
        if field not in self._ORDERING_FIELDS:
            options = ', '.join(sorted(self._ORDERING_FIELDS))
            return Response({'detail': f'ordering 不支持: {ordering}（可选: {options}）'}, status=status.HTTP_400_BAD_REQUEST)

        if field == 'design_code':
            queryset = queryset.annotate(
                design_code_normalized=Coalesce('design_code', Cast('id', output_field=CharField()))
            ).order_by(f'{direction}design_code_normalized', 'id')
        else:
            queryset = queryset.order_by(f'{direction}{field}', 'id')

        paginator = LimitOffsetPagination()
        paginator.default_limit = 20
        page = paginator.paginate_queryset(queryset, request, view=self)
        results = page if page is not None else list(queryset)

        srz = PlateOrderByStateStatusSerializer(
            results,
            many=True,
            context={
                'request': request,
                'target_state_id': state_id_int,
                'target_parameter_keys': target_keys,
                'requested_status': status_value,
            },
        )

        return Response({
            'process': {
                'id': process.id,
                'name': process.name,
            },
            'state': {
                'id': state.id if state else None,
                'name': state.name if state else None,
                'order': representative_order,
                'process_node_ids': process_node_ids,
            } if state else None,
            'status': status_value,
            'state_parameters': state_parameters_template,
            'count': getattr(paginator, 'count', len(results)),
            'next': paginator.get_next_link() if page is not None else None,
            'previous': paginator.get_previous_link() if page is not None else None,
            'results': srz.data,
        })


# 允许批量更新的字段白名单：只改这里即可增减
PLATE_ORDER_BATCH_UPDATE_ALLOWED_FIELDS = [
    "original_id",
    "design_code",
    "plate_type",
    "plate_date",
    "plate_method",
    "image_name",
    "plate_notes",
    "reprint_reason",
    "urgency_level",
    "is_invalid",
    "customer",
    "area",
    "default_address",
    "salesperson",
    "merchandiser",
    "designer",
    "style_name",
    "fabric",
    "fabric_source",
    "width",
    "production_method",
    "is_mark_frame",
    "drawing_rating",
    "color_matching_rating",
    "sample_rating",
    "difficulty_rating",
    "sample_meter",
    "required_sample_meters",
    "required_completion_date",
    "completion_date",
    "approval_result",
    "is_ordered",
    "customer_feedback",
]


class PlateOrderBatchUpdateDataSerializer(serializers.ModelSerializer):
    """
    PlateOrder 批量更新允许字段（白名单）。

    说明：
    - 不允许更新 process/business_object/created_by 等会触发流程副作用或越权的字段
    - plate_image 属于结构化字段（且 v1 有额外转换逻辑），暂不纳入批量更新，避免前端误用
    """

    class Meta:
        model = printing_models.PlateOrder
        fields = PLATE_ORDER_BATCH_UPDATE_ALLOWED_FIELDS


class PlateOrderBatchUpdateRequestSerializer(serializers.Serializer):
    plate_order_ids = serializers.ListField(
        child=serializers.IntegerField(min_value=1),
        allow_empty=False,
        help_text="需要批量更新的 PlateOrder id 列表（同一组 data 会应用到所有 id）",
    )
    data = serializers.DictField(
        child=serializers.JSONField(),
        allow_empty=False,
        help_text='需要更新的字段集合，例如 {"urgency_level": "加急", "designer": 123}',
    )
    dry_run = serializers.BooleanField(
        required=False,
        default=False,
        help_text="仅校验与预览，不实际写库",
    )

    def validate_plate_order_ids(self, value):
        # 去重保持稳定性（前端可能重复传）
        deduped = list(dict.fromkeys(value))
        if not deduped:
            raise serializers.ValidationError("plate_order_ids 不能为空")
        return deduped

    def validate_data(self, value):
        data_srz = PlateOrderBatchUpdateDataSerializer(data=value, partial=True)
        data_srz.is_valid(raise_exception=True)
        if not data_srz.validated_data:
            raise serializers.ValidationError({"detail": "data 不能为空"})
        return data_srz.validated_data


class PlateOrderBatchUpdateView(APIView):
    """
    PlateOrder 批量更新（同一份 data 应用到多个 plate_order）。

    POST /api/v2/plate-orders/batch-update/
    Body:
    {
      "plate_order_ids": [1, 2, 3],
      "data": {"urgency_level": "加急", "designer": 10},
      "dry_run": false
    }

    规则：
    - 全成功/全失败（任意 id 不存在直接 400，不做部分更新）
    - 需要 printing.change_plateorder 权限
    - 如包含 is_invalid：
      - is_invalid=true 需要 printing.can_invalidate_plateorder
      - is_invalid=false 需要 printing.can_activate_plateorder
    """

    permission_classes = [permissions.IsAuthenticated, IsPrintingFactory]

    def post(self, request):
        # 注意：Django 会缓存 has_perm 结果（user._perm_cache）。
        # 在测试中 APIClient.force_authenticate 会复用同一个 user 实例，
        # 可能导致“中途赋权后第二次请求仍判定无权限”的假阴性；这里主动清理缓存更稳妥。
        for cache_attr in ("_perm_cache", "_user_perm_cache", "_group_perm_cache"):
            if hasattr(request.user, cache_attr):
                delattr(request.user, cache_attr)

        if not request.user.has_perm("printing.change_plateorder"):
            return Response({"detail": "您没有权限批量更新开版订单"}, status=status.HTTP_403_FORBIDDEN)

        # 先对 data 做“字段白名单”校验（保证错误输出为顶层 detail，便于前端/测试消费）
        raw_data = request.data.get("data") if isinstance(request.data, dict) else None
        if isinstance(raw_data, dict):
            allowed = set(PLATE_ORDER_BATCH_UPDATE_ALLOWED_FIELDS)
            unknown = sorted(set(raw_data.keys()) - allowed)
            if unknown:
                return Response(
                    {"detail": f"不支持批量更新字段: {', '.join(unknown)}", "allowed_fields": sorted(allowed)},
                    status=status.HTTP_400_BAD_REQUEST,
                )

        srz = PlateOrderBatchUpdateRequestSerializer(data=request.data)
        srz.is_valid(raise_exception=True)
        payload = srz.validated_data

        plate_order_ids: list[int] = payload["plate_order_ids"]
        data: dict = payload["data"]
        dry_run: bool = payload.get("dry_run", False)

        # is_invalid 权限语义：沿用 v1 的作废/恢复权限
        if "is_invalid" in data:
            if data["is_invalid"] is True and not request.user.has_perm("printing.can_invalidate_plateorder"):
                return Response({"detail": "您没有权限作废开版订单"}, status=status.HTTP_403_FORBIDDEN)
            if data["is_invalid"] is False and not request.user.has_perm("printing.can_activate_plateorder"):
                return Response({"detail": "您没有权限恢复开版订单"}, status=status.HTTP_403_FORBIDDEN)

        qs = printing_models.PlateOrder.objects.filter(id__in=plate_order_ids)
        found_ids = list(qs.values_list("id", flat=True))
        found_set = set(found_ids)
        missing_ids = [str(i) for i in plate_order_ids if i not in found_set]
        if missing_ids:
            return Response({"detail": f"以下 PlateOrder 不存在: {', '.join(missing_ids)}"}, status=status.HTTP_400_BAD_REQUEST)
        # 返回/展示时保持与入参一致的顺序
        found_ids = [i for i in plate_order_ids if i in found_set]

        # 将 validated_data 转换为 queryset.update 可用的 kwargs（处理 FK -> *_id）
        update_kwargs: dict = {}
        for k, v in data.items():
            try:
                field = printing_models.PlateOrder._meta.get_field(k)
            except Exception:
                update_kwargs[k] = v
                continue

            if isinstance(field, django_models.ForeignKey):
                update_kwargs[f"{k}_id"] = v.pk if v is not None else None
            else:
                update_kwargs[k] = v

        update_kwargs["updated_at"] = timezone.now()

        if dry_run:
            return Response(
                {
                    "dry_run": True,
                    "plate_order_ids": found_ids,
                    "matched_count": len(found_ids),
                    "data": request.data.get("data") or {},
                }
            )

        with transaction.atomic():
            updated_count = qs.update(**update_kwargs)

        return Response(
            {
                "detail": "批量更新成功",
                "updated_count": updated_count,
                "plate_order_ids": found_ids,
            }
        )


class BatchAdvanceRecordSerializer(serializers.ModelSerializer):
    """批量操作记录序列化器"""
    
    state_id = serializers.IntegerField(source='state.id', read_only=True)
    state_name = serializers.CharField(source='state.name', read_only=True)
    created_by_username = serializers.CharField(source='created_by.username', read_only=True, allow_null=True)
    created_by_name = serializers.SerializerMethodField()
    printing_job_ids = serializers.SerializerMethodField()
    printing_job_count = serializers.SerializerMethodField()
    
    class Meta:
        model = printing_models.PrintingJobBatchAdvanceRecord
        fields = [
            'id',
            'printing_order',
            'state',
            'state_id',
            'state_name',
            'created_by',
            'created_by_username',
            'created_by_name',
            'parameters',
            'only_parameters',
            'printing_job_ids',
            'printing_job_count',
            'created_at',
        ]
        read_only_fields = fields
    
    def get_created_by_name(self, obj):
        """获取操作人员工姓名"""
        user = getattr(obj, 'created_by', None)
        if not user:
            return None
        emp = getattr(user, 'employee', None)
        return getattr(emp, 'name', None)
    
    def get_printing_job_ids(self, obj):
        """获取涉及的印染明细 ID 列表"""
        # 使用预取的数据避免 N+1
        if hasattr(obj, '_prefetched_objects_cache') and 'printing_jobs' in obj._prefetched_objects_cache:
            return [job.id for job in obj.printing_jobs.all()]
        return list(obj.printing_jobs.values_list('id', flat=True))
    
    def get_printing_job_count(self, obj):
        """获取涉及的印染明细数量"""
        if hasattr(obj, '_prefetched_objects_cache') and 'printing_jobs' in obj._prefetched_objects_cache:
            return len(obj.printing_jobs.all())
        return obj.printing_jobs.count()


class PrintingOrderBatchAdvanceRecordsView(APIView):
    """
    获取印染订单的批量推进记录
    
    根据 printing_order_id 查询该订单下所有的批量推进审计记录。
    
    GET /api/v2/printing-orders/{printing_order_id}/batch-advance-records/
    
    响应示例:
    {
        "printing_order_id": 55,
        "records": [
            {
                "id": 9,
                "printing_order": 55,
                "state": 7,
                "state_id": 7,
                "state_name": "染色",
                "created_by": 1001,
                "created_by_username": "factory_user",
                "created_by_name": "张三",
                "parameters": {"temperature": "25.5"},
                "printing_job_ids": [101, 102, 103],
                "printing_job_count": 3,
                "created_at": "2025-12-14T10:00:00+08:00"
            }
        ],
        "total_count": 1
    }
    """
    
    permission_classes = [permissions.IsAuthenticated]
    
    def get(self, request, printing_order_id):
        # 验证订单是否存在
        try:
            printing_order = printing_models.PrintingOrder.objects.get(id=printing_order_id)
        except printing_models.PrintingOrder.DoesNotExist:
            return Response(
                {'error': f'未找到 ID 为 {printing_order_id} 的印染订单'},
                status=status.HTTP_404_NOT_FOUND
            )
        
        # 查询批量推进记录，按创建时间倒序
        records = printing_models.PrintingJobBatchAdvanceRecord.objects.filter(
            printing_order_id=printing_order_id
        ).select_related(
            'state',
            'created_by',
            'created_by__employee',
        ).prefetch_related(
            'printing_jobs',
        ).order_by('-created_at')
        
        serializer = BatchAdvanceRecordSerializer(records, many=True)
        
        return Response({
            'printing_order_id': printing_order_id,
            'records': serializer.data,
            'total_count': records.count(),
        })


class PrintingOrderExternalSnapshotSyncRequestSerializer(serializers.Serializer):
    external_order_id = serializers.CharField(max_length=100, help_text='外部订单编号')
    allow_reset_stateflow = serializers.BooleanField(
        required=False,
        default=False,
        help_text='是否允许先撤销目标订单下已有的所有工序，再执行覆盖同步',
    )

    def validate_external_order_id(self, value):
        normalized = str(value or '').strip()
        if not normalized:
            raise serializers.ValidationError('external_order_id 不能为空')
        return normalized


class PrintingOrderExternalSnapshotSyncView(APIView):
    """
    按 external_order_id 触发外部订单快照同步。

    行为：
    - 若目标订单任意 printing_job 已关联销售品，则立即失败并记录审计
    - 若目标订单存在已执行工序，默认失败；可通过 allow_reset_stateflow=true 先撤销全部工序后再覆盖
    - 成功时复用现有外部订单映射逻辑覆盖 PrintingOrder / PrintingJob
    """

    permission_classes = [permissions.IsAuthenticated, IsPrintingFactory]

    def post(self, request):
        srz = PrintingOrderExternalSnapshotSyncRequestSerializer(data=request.data)
        srz.is_valid(raise_exception=True)

        payload = srz.validated_data
        try:
            result = sync_external_printing_order_snapshot_impl(
                external_order_id=payload['external_order_id'],
                operator_user=request.user,
                allow_reset_stateflow=payload.get('allow_reset_stateflow', False),
            )
        except ExternalPrintingOrderSnapshotSyncError as exc:
            return Response(
                {
                    'detail': str(exc),
                    'audit_id': exc.audit_id,
                    'external_order_id': payload['external_order_id'],
                },
                status=exc.status_code,
            )

        return Response(
            {
                'detail': '外部订单快照同步成功',
                **result,
            },
            status=status.HTTP_200_OK,
        )


class PrintingJobBatchAddParametersRequestSerializer(serializers.Serializer):
    """批量补充参数请求序列化器"""
    printing_job_ids = serializers.ListField(
        child=serializers.IntegerField(),
        min_length=1,
        help_text='印染明细ID列表'
    )
    state_id = serializers.IntegerField(help_text='目标流程节点ID')
    parameters = serializers.DictField(
        child=serializers.CharField(allow_blank=True),
        help_text='要补充的工艺参数'
    )
    remark = serializers.CharField(required=False, default='', allow_blank=True, help_text='备注说明')


def _validate_jobs_for_batch_add_parameters(printing_job_ids: list[int], state_id: int) -> dict:
    """
    验证批量补充参数的 jobs 并返回相关数据
    
    验证规则：
    1. 所有 job 必须存在
    2. 所有 job 必须属于同一个 printing_order
    3. 所有 job 必须有 business_object
    4. 所有 job 的 business_object 必须有对应 state_id 的 state_log（未撤销）
    
    返回：
    {
        'jobs': QuerySet[PrintingJob],
        'printing_order': PrintingOrder,
        'printing_order_id': int,
        'state': State,
        'state_logs': dict[int, StateFlowRecord],  # job_id -> state_log
    }
    """
    from stateflow import models as stateflow_models
    
    jobs = (
        printing_models.PrintingJob.objects
        .select_related('printing_order', 'business_object')
        .filter(id__in=printing_job_ids)
    )
    
    found_ids = set(jobs.values_list('id', flat=True))
    missing_ids = [i for i in printing_job_ids if i not in found_ids]
    if missing_ids:
        raise serializers.ValidationError({
            'detail': f'以下 PrintingJob 不存在: {missing_ids}',
            'missing_jobs': missing_ids,
        })
    
    # 验证同一订单
    order_ids = set(jobs.values_list('printing_order_id', flat=True))
    if len(order_ids) != 1:
        raise serializers.ValidationError({
            'detail': '所有 PrintingJob 必须属于同一个 PrintingOrder',
        })
    
    printing_order_id = order_ids.pop()
    printing_order = printing_models.PrintingOrder.objects.get(id=printing_order_id)
    
    # 验证 state 存在
    try:
        state = stateflow_models.State.objects.get(id=state_id)
    except stateflow_models.State.DoesNotExist:
        raise serializers.ValidationError({
            'detail': f'State ID {state_id} 不存在',
        })
    
    # 验证所有 job 有 business_object 和对应的 state_log
    jobs_without_bo = []
    jobs_without_state_log = []
    state_logs = {}  # job_id -> state_log
    
    for job in jobs:
        if not job.business_object:
            jobs_without_bo.append(job.id)
            continue
        
        # 查找对应 state_id 的 state_log（未撤销）
        state_log = job.business_object.state_logs.filter(
            state_id=state_id,
            is_cancelled=False
        ).first()
        
        if not state_log:
            jobs_without_state_log.append(job.id)
        else:
            state_logs[job.id] = state_log
    
    if jobs_without_bo:
        raise serializers.ValidationError({
            'detail': f'以下 PrintingJob 没有流程实例 (business_object): {jobs_without_bo}',
            'missing_business_object': jobs_without_bo,
        })
    
    if jobs_without_state_log:
        raise serializers.ValidationError({
            'detail': f'以下 PrintingJob 没有对应 state_id={state_id} 的状态流转记录（或已撤销）: {jobs_without_state_log}',
            'missing_state_log': jobs_without_state_log,
        })
    
    return {
        'jobs': jobs,
        'printing_order': printing_order,
        'printing_order_id': printing_order_id,
        'state': state,
        'state_logs': state_logs,
    }


class PrintingJobBatchAddParametersView(APIView):
    """
    批量补充工艺参数
    
    为多个 PrintingJob 的已完成状态流转记录批量补充工艺参数。
    
    POST /api/v2/printing-jobs/batch-add-parameters/
    
    请求体:
    {
        "printing_job_ids": [101, 102, 103],
        "state_id": 7,
        "parameters": {
            "temperature": "26.0",
            "operator": "李四"
        },
        "remark": "批量补充参数备注"
    }
    
    响应:
    {
        "detail": "批量补充参数成功",
        "batch_record_id": 15,
        "printing_order_id": 55,
        "printing_job_ids": [101, 102, 103],
        "state_id": 7,
        "state_name": "染色",
        "parameters": {...},
        "affected_count": 3
    }
    """
    
    permission_classes = [permissions.IsAuthenticated, IsPrintingFactory]
    
    def post(self, request):
        srz = PrintingJobBatchAddParametersRequestSerializer(data=request.data)
        srz.is_valid(raise_exception=True)
        
        payload = srz.validated_data
        printing_job_ids = payload['printing_job_ids']
        state_id = payload['state_id']
        parameters = payload['parameters']
        remark = payload.get('remark', '')
        
        if not parameters:
            raise serializers.ValidationError({
                'detail': '参数不能为空',
            })
        
        # 验证数据
        data = _validate_jobs_for_batch_add_parameters(printing_job_ids, state_id)
        
        from stateflow import services as stateflow_services
        
        # 全成功/全失败
        with transaction.atomic():
            # 创建批量操作记录
            record = printing_models.PrintingJobBatchAdvanceRecord.objects.create(
                printing_order=data['printing_order'],
                state=data['state'],
                created_by=request.user,
                parameters=parameters,
                only_parameters=True,  # 标记为仅补充参数
            )
            record.printing_jobs.set(data['jobs'])
            
            # 逐个补充参数
            for job in data['jobs']:
                state_log = data['state_logs'][job.id]
                stateflow_services.add_parameters_to_state_log(
                    state_log,
                    remark=remark,
                    **parameters
                )
        
        return Response({
            'detail': '批量补充参数成功',
            'batch_record_id': record.id,
            'printing_order_id': data['printing_order_id'],
            'printing_job_ids': [j.id for j in data['jobs']],
            'state_id': state_id,
            'state_name': data['state'].name,
            'parameters': parameters,
            'affected_count': len(data['jobs']),
        })
