"""
Shipment 模块业务逻辑层
"""

from __future__ import annotations

from decimal import Decimal, InvalidOperation
from typing import List

from django.conf import settings
from django.contrib.auth import get_user_model
from django.db import transaction
from django.db.models import Count, Exists, IntegerField, OuterRef, QuerySet, Subquery
from django.db.models.functions import Coalesce
from django.utils import timezone

from api_v1.utils.wecom_webhook import send_wecom_webhook_message
from shipment.models import (
    ExternalFinishedProduct,
    SalesItem,
    SalesItemChangeRecord,
    SalesItemRebuildRecord,
    Shipment,
    ShipmentDelivery,
    ShipmentDeliveryStatus,
    ShipmentStatus,
)
from shipment.signals import shipment_created


UNSET = object()


def _resolve_user_merchant(user):
    emp = getattr(user, "employee", None)
    return getattr(emp, "merchant", None) if emp else None


def _resolve_user_employee(user):
    return getattr(user, "employee", None)


def _user_label(user) -> str:
    if not user:
        return "系统自动发送"
    emp = getattr(user, "employee", None)
    name = getattr(emp, "name", None) if emp is not None else None
    if name:
        return str(name)
    username = getattr(user, "username", None)
    if username:
        return str(username)
    return "系统自动发送"


def get_active_sales_items_queryset() -> QuerySet[SalesItem]:
    """
    返回未软删除的销售品查询集。
    """
    return SalesItem.objects.filter(delete_at__isnull=True)


def ensure_sales_item_can_be_soft_deleted(sales_item: SalesItem) -> None:
    """
    校验销售品是否允许软删除。

    当前规则：
    - 已关联出货单的销售品不允许删除
    """
    if sales_item.shipment_id is not None:
        raise ValueError("已关联出货单的销售品不允许删除")


def _get_printing_job_for_sales_item(*, printing_job_id: int, merchant):
    from printing.models import PrintingJob

    try:
        return PrintingJob.objects.select_related("printing_order").get(
            id=printing_job_id,
            merchant=merchant,
        )
    except PrintingJob.DoesNotExist:
        raise ValueError(f"生产任务 {printing_job_id} 不存在或不属于当前商户")


def _normalize_sales_item_quantity(quantity: str | Decimal) -> Decimal:
    try:
        return Decimal(str(quantity))
    except (InvalidOperation, ValueError, TypeError) as e:
        raise ValueError(f"数量 {quantity} 格式无效: {e}")


def _resolve_delivery_shipments(
    *,
    shipment_ids: list[int],
    merchant,
    current_delivery: ShipmentDelivery | None = None,
) -> list[Shipment]:
    normalized_ids = list(dict.fromkeys(shipment_ids or []))
    if not normalized_ids:
        return []

    shipments = list(
        Shipment.objects.filter(id__in=normalized_ids, merchant=merchant)
        .select_related("customer", "delivery")
        .order_by("id")
    )
    found_ids = {shipment.id for shipment in shipments}
    missing_ids = sorted(set(normalized_ids) - found_ids)
    if missing_ids:
        raise ValueError(f"以下出货单不存在或不属于当前商户: {missing_ids}")

    occupied_ids = sorted(
        shipment.id
        for shipment in shipments
        if shipment.delivery_id is not None
        and (current_delivery is None or shipment.delivery_id != current_delivery.id)
    )
    if occupied_ids:
        raise ValueError(f"以下出货单已关联到其他送货单: {occupied_ids}")

    not_approved_ids = sorted(
        shipment.id
        for shipment in shipments
        if shipment.status != ShipmentStatus.APPROVED
    )
    if not_approved_ids:
        raise ValueError(f"以下出货单未处于已审核状态，不能绑定到送货单: {not_approved_ids}")

    return shipments


def get_customers_with_unshipped_sales_items(*, merchant) -> QuerySet:
    """
    查询当前商户下“存在未出货销售品”的客户列表。

    返回 Customer 查询集，并附带:
    - unshipped_sales_items_count: 该客户未出货销售品数量
    """
    from basic_info.models import Customer

    base_sales_items = get_active_sales_items_queryset().filter(
        merchant=merchant,
        shipment__isnull=True,
        customer_id=OuterRef("pk"),
    )
    count_subquery = (
        base_sales_items.values("customer_id")
        .annotate(total=Count("id"))
        .values("total")[:1]
    )

    return (
        Customer.objects.filter(merchant=merchant)
        .annotate(has_unshipped_sales_items=Exists(base_sales_items))
        .filter(has_unshipped_sales_items=True)
        .annotate(
            unshipped_sales_items_count=Coalesce(
                Subquery(count_subquery, output_field=IntegerField()),
                0,
            )
        )
        .order_by("id")
    )


def get_sales_items_by_printing_order(
    printing_order_id: int | str,
    include_already_has_shipment: bool = False,
    merchant=None,
) -> QuerySet[SalesItem]:
    """
    通过生产订单 ID 或 external_order_id 查询对应的销售品。

    Args:
        printing_order_id: 生产订单内部 ID，或 external_order_id
        include_already_has_shipment: 是否包含已关联出货单的销售品，默认为 False
        merchant: 可选的商户约束；传入后会限定只在该商户下解析生产订单

    Returns:
        SalesItem 查询集
    """
    from printing.models import PrintingJob, PrintingOrder

    raw_value = str(printing_order_id).strip()
    order_queryset = PrintingOrder.objects.all()
    if merchant is not None:
        order_queryset = order_queryset.filter(merchant=merchant)

    resolved_order_ids: list[int] = []

    # 优先按内部 ID 解析，保持和当前 API 路径语义一致。
    if raw_value.isdigit():
        resolved_order_ids = list(
            order_queryset.filter(id=int(raw_value)).values_list("id", flat=True)[:1]
        )

    # 内部 ID 未命中时，再按 external_order_id 查询。
    if not resolved_order_ids and raw_value:
        resolved_order_ids = list(
            order_queryset.filter(external_order_id=raw_value).values_list(
                "id", flat=True
            )
        )

    if not resolved_order_ids:
        return SalesItem.objects.none().select_related("shipment").order_by("id")

    # 1. 获取目标生产订单下所有 PrintingJob 的 ID
    job_ids = PrintingJob.objects.filter(
        printing_order_id__in=resolved_order_ids
    ).values_list("id", flat=True)

    # 2. 查询 SalesItem，过滤 printing_job_id 在这些 job_ids 中
    queryset = get_active_sales_items_queryset().filter(
        printing_job_id__in=list(job_ids)
    )

    # 3. 根据参数决定是否过滤已出货的销售品
    if not include_already_has_shipment:
        queryset = queryset.filter(shipment__isnull=True)

    return queryset.select_related("shipment").order_by("id")


def get_sales_items_by_customer(
    *,
    merchant,
    customer_id: int,
    include_already_has_shipment: bool = False,
    external_order_id: str | None = None,
) -> QuerySet[SalesItem]:
    """
    通过客户ID查询对应销售品。

    仅返回当前商户下、customer_id 匹配的销售品。
    """
    queryset = get_active_sales_items_queryset().filter(
        merchant=merchant,
        customer_id=customer_id,
    )

    if external_order_id:
        from printing.models import PrintingJob

        printing_job_ids = PrintingJob.objects.filter(
            merchant=merchant,
            printing_order__external_order_id=external_order_id,
        ).values_list("id", flat=True)
        queryset = queryset.filter(printing_job_id__in=list(printing_job_ids))

    if not include_already_has_shipment:
        queryset = queryset.filter(shipment__isnull=True)

    return queryset.select_related("shipment").order_by("id")


@transaction.atomic
def update_sales_item(
    sales_item: SalesItem,
    *,
    quantity: str | Decimal | None = None,
    remark: str | None = None,
    position: str | None = None,
    operator=None,
) -> SalesItem:
    """
    更新销售品的非关系字段。

    当前仅允许修改:
    - quantity
    - remark
    - position
    """
    changes: dict[str, tuple[object, object]] = {}

    if quantity is not None:
        try:
            normalized_quantity = Decimal(str(quantity))
        except (InvalidOperation, ValueError, TypeError) as exc:
            raise ValueError(f"数量 {quantity} 格式无效: {exc}")
        if normalized_quantity != sales_item.quantity:
            changes["quantity"] = (sales_item.quantity, normalized_quantity)
            sales_item.quantity = normalized_quantity

    if remark is not None:
        normalized_remark = remark or ""
        if normalized_remark != sales_item.remark:
            changes["remark"] = (sales_item.remark, normalized_remark)
            sales_item.remark = normalized_remark

    if position is not None:
        normalized_position = position or ""
        if normalized_position != sales_item.position:
            changes["position"] = (sales_item.position, normalized_position)
            sales_item.position = normalized_position

    if not changes:
        return sales_item

    sales_item.save(update_fields=[*changes.keys(), "updated_at"])

    def _serialize_value(value):
        if isinstance(value, Decimal):
            return str(value)
        return value

    SalesItemChangeRecord.objects.create(
        sales_item=sales_item,
        operator=operator,
        operated_at=timezone.now(),
        before_values={
            field: _serialize_value(old_value)
            for field, (old_value, _) in changes.items()
        },
        after_values={
            field: _serialize_value(new_value)
            for field, (_, new_value) in changes.items()
        },
    )

    return sales_item


@transaction.atomic
def delete_sales_item(
    sales_item: SalesItem,
    *,
    deleted_by=None,
) -> SalesItem:
    """
    软删除销售品。
    """
    if sales_item.delete_at is not None:
        return sales_item

    ensure_sales_item_can_be_soft_deleted(sales_item)

    sales_item.delete_at = timezone.now()
    sales_item.delete_by = deleted_by
    sales_item.save(update_fields=["delete_at", "delete_by", "updated_at"])
    return sales_item


@transaction.atomic
def rebuild_sales_item(
    sales_item: SalesItem,
    *,
    new_printing_job_id: int,
    operator,
    quantity: str | Decimal | None = None,
) -> SalesItem:
    """
    软删除旧销售品，并基于新 printing_job 重建一个新销售品。
    """
    ensure_sales_item_can_be_soft_deleted(sales_item)

    merchant = sales_item.merchant
    printing_job = _get_printing_job_for_sales_item(
        printing_job_id=new_printing_job_id,
        merchant=merchant,
    )

    new_quantity = (
        _normalize_sales_item_quantity(quantity)
        if quantity is not None
        else sales_item.quantity
    )

    deleted_sales_item = delete_sales_item(sales_item, deleted_by=operator)

    customer_id = sales_item.customer_id
    if customer_id is None and printing_job.printing_order and printing_job.printing_order.customer:
        customer_id = printing_job.printing_order.customer_id

    new_sales_item = SalesItem.objects.create(
        shipment=None,
        merchant=merchant,
        name=sales_item.name,
        quantity=new_quantity,
        unit=sales_item.unit,
        created_by=sales_item.created_by,
        printing_job_id=printing_job.id,
        customer_id=customer_id,
        remark=sales_item.remark,
        position=sales_item.position,
    )

    SalesItemRebuildRecord.objects.create(
        old_sales_item=deleted_sales_item,
        new_sales_item=new_sales_item,
        operator=operator,
        rebuilt_at=timezone.now(),
        old_printing_job_id=deleted_sales_item.printing_job_id,
        new_printing_job_id=printing_job.id,
        old_quantity=deleted_sales_item.quantity,
        new_quantity=new_sales_item.quantity,
    )

    return new_sales_item


@transaction.atomic
def create_shipment_delivery(
    *,
    driver_name: str,
    vehicle_trip: str,
    contact_phone: str = "",
    vehicle_capacity: str = "",
    shipment_ids: list[int],
    shipment_order_ids: list | None = None,
    created_by,
    remark: str = "",
    internal_remark: str = "",
) -> ShipmentDelivery:
    merchant = _resolve_user_merchant(created_by)
    operator = _resolve_user_employee(created_by)
    if not merchant:
        raise ValueError("用户未关联商户，无法创建送货单")

    driver_name = (driver_name or "").strip()
    vehicle_trip = (vehicle_trip or "").strip()
    if not driver_name:
        raise ValueError("driver_name 不能为空")
    if not vehicle_trip:
        raise ValueError("vehicle_trip 不能为空")

    shipments = _resolve_delivery_shipments(
        shipment_ids=shipment_ids,
        merchant=merchant,
    )

    delivery = ShipmentDelivery.objects.create(
        merchant=merchant,
        driver_name=driver_name,
        vehicle_trip=vehicle_trip,
        contact_phone=(contact_phone or "").strip(),
        vehicle_capacity=(vehicle_capacity or "").strip(),
        remark=(remark or "").strip(),
        internal_remark=(internal_remark or "").strip(),
        shipment_order_ids=shipment_order_ids,
        created_by=created_by,
        operator=operator,
    )
    if shipments:
        Shipment.objects.filter(id__in=[shipment.id for shipment in shipments]).update(
            delivery=delivery
        )
    return delivery


@transaction.atomic
def update_shipment_delivery(
    delivery: ShipmentDelivery,
    *,
    driver_name: str | None = None,
    vehicle_trip: str | None = None,
    contact_phone: str | None = None,
    vehicle_capacity: str | None = None,
    shipment_ids: list[int] | None = None,
    shipment_order_ids=UNSET,
    remark: str | None = None,
    internal_remark: str | None = None,
    operator=None,
) -> ShipmentDelivery:
    if driver_name is not None:
        normalized_driver_name = driver_name.strip()
        if not normalized_driver_name:
            raise ValueError("driver_name 不能为空")
        delivery.driver_name = normalized_driver_name

    if vehicle_trip is not None:
        normalized_vehicle_trip = vehicle_trip.strip()
        if not normalized_vehicle_trip:
            raise ValueError("vehicle_trip 不能为空")
        delivery.vehicle_trip = normalized_vehicle_trip

    if contact_phone is not None:
        delivery.contact_phone = (contact_phone or "").strip()

    if vehicle_capacity is not None:
        delivery.vehicle_capacity = (vehicle_capacity or "").strip()

    if remark is not None:
        delivery.remark = (remark or "").strip()

    if internal_remark is not None:
        delivery.internal_remark = (internal_remark or "").strip()

    if shipment_order_ids is not UNSET:
        delivery.shipment_order_ids = shipment_order_ids

    if shipment_ids is not None:
        shipments = _resolve_delivery_shipments(
            shipment_ids=shipment_ids,
            merchant=delivery.merchant,
            current_delivery=delivery,
        )
        Shipment.objects.filter(delivery=delivery).exclude(
            id__in=[shipment.id for shipment in shipments]
        ).update(delivery=None)
        if shipments:
            Shipment.objects.filter(id__in=[shipment.id for shipment in shipments]).update(
                delivery=delivery
            )
        else:
            Shipment.objects.filter(delivery=delivery).update(delivery=None)

    if operator is not None:
        delivery.operator = _resolve_user_employee(operator)

    delivery.save()
    return delivery


@transaction.atomic
def bind_shipments_to_delivery(
    delivery: ShipmentDelivery,
    *,
    shipment_ids: list[int],
    operator=None,
) -> ShipmentDelivery:
    shipments = _resolve_delivery_shipments(
        shipment_ids=shipment_ids,
        merchant=delivery.merchant,
        current_delivery=delivery,
    )
    if not shipments:
        return delivery

    Shipment.objects.filter(id__in=[shipment.id for shipment in shipments]).update(
        delivery=delivery
    )
    if operator is not None:
        delivery.operator = _resolve_user_employee(operator)
        delivery.save(update_fields=["operator", "updated_at"])
    return delivery


@transaction.atomic
def modify_shipment_delivery_status(
    delivery: ShipmentDelivery,
    *,
    target_status: int,
    operator=None,
) -> ShipmentDelivery:
    try:
        target_status = int(target_status)
    except (TypeError, ValueError):
        raise ValueError("无效的送货单状态")

    current_status = delivery.status
    if current_status == target_status:
        return delivery

    allowed_transitions = {
        ShipmentDeliveryStatus.PENDING: {ShipmentDeliveryStatus.IN_TRANSIT},
        ShipmentDeliveryStatus.IN_TRANSIT: {ShipmentDeliveryStatus.DELIVERED},
        ShipmentDeliveryStatus.DELIVERED: set(),
        ShipmentDeliveryStatus.CANCELLED: set(),
    }
    if target_status not in ShipmentDeliveryStatus.values:
        raise ValueError("无效的送货单状态")
    if target_status == ShipmentDeliveryStatus.CANCELLED:
        raise ValueError("取消送货单请使用独立的取消接口")
    if target_status not in allowed_transitions.get(current_status, set()):
        raise ValueError(
            f"不允许将送货单状态从 {delivery.get_status_display()} 修改为 "
            f"{ShipmentDeliveryStatus(target_status).label}"
        )

    now = timezone.now()
    delivery.status = target_status
    update_fields = ["status", "updated_at"]
    if operator is not None:
        delivery.operator = _resolve_user_employee(operator)
        update_fields.append("operator")
    if target_status == ShipmentDeliveryStatus.IN_TRANSIT:
        delivery.started_at = now
        update_fields.append("started_at")
    elif target_status == ShipmentDeliveryStatus.DELIVERED:
        delivery.delivered_at = now
        update_fields.append("delivered_at")

    delivery.save(update_fields=update_fields)
    return delivery


@transaction.atomic
def cancel_shipment_delivery(
    delivery: ShipmentDelivery,
    *,
    cancelled_by=None,
    operator=None,
) -> ShipmentDelivery:
    if delivery.status == ShipmentDeliveryStatus.CANCELLED:
        return delivery

    delivery.status = ShipmentDeliveryStatus.CANCELLED
    delivery.cancelled_at = timezone.now()
    delivery.cancelled_by = cancelled_by
    if operator is not None:
        delivery.operator = _resolve_user_employee(operator)

    delivery.save(
        update_fields=[
            "status",
            "cancelled_at",
            "cancelled_by",
            "operator",
            "updated_at",
        ]
    )
    return delivery


@transaction.atomic
def delete_shipment_delivery(delivery: ShipmentDelivery) -> None:
    Shipment.objects.filter(delivery=delivery).update(delivery=None)
    delivery.delete()


@transaction.atomic
def modify_status(
    shipment: Shipment,
    *,
    target_status: int,
    operator=None,
    approved_by=None,
) -> Shipment:
    """
    修改出货单状态，并记录状态修改时间。

    规则：
    - 草稿 -> 只能已发布
    - 已发布 -> 已审核 / 已驳回 / 已取消
    - 已驳回 -> 已取消
    - 已审核 -> 已取消
    - 已取消 -> 不可再变
    - 重复设置同一状态保持幂等，直接返回
    """
    current_status = shipment.status
    if current_status == target_status:
        return shipment

    allowed_transitions = {
        ShipmentStatus.DRAFT: {ShipmentStatus.PUBLISHED},
        ShipmentStatus.PUBLISHED: {
            ShipmentStatus.APPROVED,
            ShipmentStatus.REJECTED,
            ShipmentStatus.CANCELLED,
        },
        ShipmentStatus.REJECTED: {
            # 驳回现在会解绑销售品并回退到待分配池。
            # 在没有重新绑定流程前，暂时关闭 REJECTED -> APPROVED，避免审核空出货单。
            # ShipmentStatus.APPROVED,
            ShipmentStatus.CANCELLED,
        },
        ShipmentStatus.APPROVED: {
            ShipmentStatus.CANCELLED,
        },
        ShipmentStatus.CANCELLED: set(),
    }

    if target_status not in allowed_transitions.get(current_status, set()):
        raise ValueError(
            f"不允许将出货单状态从 {shipment.get_status_display()} 修改为 "
            f"{ShipmentStatus(target_status).label}"
        )

    if target_status == ShipmentStatus.APPROVED and approved_by is None:
        raise ValueError("目标状态为已审核时，approved_by 不能为空")

    update_fields = [
        "status",
        "status_modified_at",
        "cancelled_by",
        "approved_by",
        "updated_at",
    ]

    if target_status == ShipmentStatus.REJECTED:
        bound_sales_item_qs = SalesItem.objects.select_for_update().filter(shipment=shipment)
        shipment.rejected_sales_item_ids = list(
            bound_sales_item_qs.order_by("id").values_list("id", flat=True)
        )
        bound_sales_item_qs.update(shipment=None)
        update_fields.append("rejected_sales_item_ids")

    shipment.status = target_status
    shipment.status_modified_at = timezone.now()

    if target_status == ShipmentStatus.CANCELLED:
        shipment.cancelled_by = operator

    if target_status == ShipmentStatus.APPROVED:
        shipment.approved_by = approved_by

    shipment.save(update_fields=update_fields)
    return shipment


@transaction.atomic
def update_shipment(
    shipment: Shipment,
    *,
    customer_id: int | None = None,
    shipment_date=None,
    address: str | None = None,
    contact_name: str | None = None,
    contact_phone: str | None = None,
    area: str | None = None,
    remark: str | None = None,
    external_id: str | None = None,
) -> Shipment:
    """
    更新出货单业务数据。

    说明：
    - 仅允许修改未关联送货单的出货单
    - 不处理状态变更
    """
    from basic_info.models import Customer

    if shipment.delivery_id is not None:
        raise ValueError("已关联送货单的出货单不允许修改")

    if customer_id is not None:
        customer = Customer.objects.filter(id=customer_id).first()
        if customer is None:
            raise ValueError(f"客户 {customer_id} 不存在")
        if customer.merchant_id != shipment.merchant_id:
            raise ValueError("无权限绑定该客户")
        shipment.customer = customer

    if shipment_date is not None:
        shipment.shipment_date = shipment_date
    if address is not None:
        shipment.address = address or ""
    if contact_name is not None:
        shipment.contact_name = contact_name or ""
    if contact_phone is not None:
        shipment.contact_phone = contact_phone or ""
    if area is not None:
        shipment.area = (area or "").strip()
    if remark is not None:
        shipment.remark = remark or ""
    if external_id is not None:
        shipment.external_id = (external_id or "").strip() or None

    shipment.save()
    return shipment


@transaction.atomic
@transaction.atomic
def create_shipment(
    customer_id: int,
    shipment_date,
    sales_item_ids: List[int],
    created_by,
    address: str = "",
    contact_name: str = "",
    contact_phone: str = "",
    remark: str = "",
    area: str = "",
) -> Shipment:
    """
    创建出货单并关联销售品

    Args:
        customer_id: 客户ID
        shipment_date: 出货日期
        sales_item_ids: 要关联的销售品ID列表
        created_by: 创建人
        remark: 备注

    Returns:
        创建的 Shipment 实例

    Raises:
        ValueError: 如果销售品不存在或已被关联到其他出货单
    """
    from basic_info.models import Customer

    # 验证客户存在
    try:
        customer = Customer.objects.get(id=customer_id)
    except Customer.DoesNotExist:
        raise ValueError(f"客户 {customer_id} 不存在")

    # merchant 隔离：必须能解析出当前用户 merchant
    emp = getattr(created_by, "employee", None)
    merchant = getattr(emp, "merchant", None) if emp else None
    if not merchant:
        # superuser 也必须绑定 merchant（避免产生无法隔离的数据）
        raise ValueError("用户未关联商户，无法创建出货单")
    if customer.merchant_id != merchant.id:
        raise ValueError("无权限为该客户创建出货单")

    # 验证销售品
    if sales_item_ids:
        from printing.models import PrintingJob

        # 查询销售品
        sales_items = get_active_sales_items_queryset().filter(id__in=sales_item_ids)
        found_ids = set(sales_items.values_list("id", flat=True))
        missing_ids = set(sales_item_ids) - found_ids

        if missing_ids:
            raise ValueError(f"以下销售品不存在: {list(missing_ids)}")

        # 检查是否有已关联出货单的销售品
        already_shipped = sales_items.filter(shipment__isnull=False)
        if already_shipped.exists():
            shipped_ids = list(already_shipped.values_list("id", flat=True))
            raise ValueError(f"以下销售品已关联到其他出货单: {shipped_ids}")

        sales_items_list = list(sales_items)
        missing_printing_job_ids = [
            item.id for item in sales_items_list if not item.printing_job_id
        ]
        if missing_printing_job_ids:
            raise ValueError(
                f"以下销售品缺少关联生产任务，无法创建出货单: {missing_printing_job_ids}"
            )

        printing_job_ids = {item.printing_job_id for item in sales_items_list}
        printing_job_map = {
            job.id: job.printing_order_id
            for job in PrintingJob.objects.filter(id__in=printing_job_ids).only(
                "id", "printing_order_id"
            )
        }

        invalid_printing_job_ids = sorted(printing_job_ids - set(printing_job_map.keys()))
        if invalid_printing_job_ids:
            affected_item_ids = sorted(
                item.id
                for item in sales_items_list
                if item.printing_job_id in invalid_printing_job_ids
            )
            raise ValueError(
                f"以下销售品关联的生产任务不存在，无法创建出货单: {affected_item_ids}"
            )

        printing_order_ids = {
            printing_job_map[item.printing_job_id] for item in sales_items_list
        }
        if len(printing_order_ids) > 1:
            raise ValueError("出货单中的销售品必须来自同一个生产订单")

    # 创建出货单
    shipment = Shipment.objects.create(
        merchant=merchant,
        customer=customer,
        shipment_date=shipment_date,
        address=address or "",
        contact_name=contact_name or "",
        contact_phone=contact_phone or "",
        area=(area or "").strip(),
        remark=remark,
        created_by=created_by,
    )

    # 关联销售品
    if sales_item_ids:
        updated = get_active_sales_items_queryset().filter(
            id__in=sales_item_ids, merchant=merchant
        ).update(shipment=shipment)
        if updated != len(sales_item_ids):
            raise ValueError("存在不属于当前商户的销售品，无法关联到出货单")

    try:
        shipment_created.send(
            sender=Shipment,
            instance=shipment,
            created_by=created_by,
        )
    except Exception:
        import logging
        logging.getLogger(__name__).exception(
            "[shipment.services] 触发 shipment_created signal 失败（已忽略）"
        )

    return shipment


def render_shipment_created_markdown(
    *,
    shipment_id: str,
    customer_name: str,
    shipment_date: str,
    items_count: str,
    sender_label: str,
) -> str:
    template = getattr(
        settings,
        "SHIPMENT_CREATED_WECOM_MARKDOWN_TEMPLATE",
        (
            "### 出货单创建\n"
            "\n"
            "- **出货单ID**：`{shipment_id}`\n"
            "- **客户**：{customer_name}\n"
            "- **出货日期**：`{shipment_date}`\n"
            "- **销售品数量**：`{items_count}`\n"
            "- **发送者**：{sender}\n"
        ),
    )
    return template.format(
        shipment_id=str(shipment_id or "-"),
        customer_name=str(customer_name or "-"),
        shipment_date=str(shipment_date or "-"),
        items_count=str(items_count or "0"),
        sender=str(sender_label or "系统自动发送"),
    )


def send_shipment_created_wecom(
    *,
    shipment_id: int,
    created_by_id: int | None = None,
    key: str | None = None,
    timeout_seconds: float = 10.0,
    dry_run: bool = False,
) -> dict:
    shipment = (
        Shipment.objects.select_related("customer")
        .prefetch_related("items")
        .filter(id=int(shipment_id))
        .first()
    )
    if not shipment:
        raise ValueError(f"Shipment 不存在：id={shipment_id}")

    created_by = None
    if created_by_id:
        created_by = get_user_model().objects.filter(id=int(created_by_id)).first()

    customer_name = getattr(getattr(shipment, "customer", None), "name", None) or "-"
    shipment_date_text = (
        shipment.shipment_date.isoformat() if getattr(shipment, "shipment_date", None) else "-"
    )
    items_count = shipment.items.filter(delete_at__isnull=True).count()
    sender_label = _user_label(created_by)

    msg = render_shipment_created_markdown(
        shipment_id=str(shipment.id),
        customer_name=str(customer_name),
        shipment_date=str(shipment_date_text),
        items_count=str(items_count),
        sender_label=str(sender_label),
    )

    if dry_run:
        return {
            "dry_run": True,
            "message": msg,
            "shipment_id": shipment.id,
            "customer_name": str(customer_name),
            "shipment_date": str(shipment_date_text),
            "items_count": items_count,
            "sender_label": str(sender_label),
        }

    resp = send_wecom_webhook_message(
        content=msg,
        msgtype="markdown",
        key=key,
        timeout_seconds=timeout_seconds,
    )
    return {
        "dry_run": False,
        "message": msg,
        "shipment_id": shipment.id,
        "customer_name": str(customer_name),
        "shipment_date": str(shipment_date_text),
        "items_count": items_count,
        "sender_label": str(sender_label),
        "wecom": resp.raw,
        "ok": resp.ok,
        "errcode": resp.errcode,
        "errmsg": resp.errmsg,
    }


@transaction.atomic
def create_external_shipment(
    customer_id: int,
    shipment_date,
    external_id: str,
    external_finished_products: List[dict],
    created_by,
    address: str = "",
    contact_name: str = "",
    contact_phone: str = "",
    remark: str = "",
    area: str = "",
) -> Shipment:
    """
    创建出货单（external 版），并批量写入外部成品表并关联到出货单。

    特点：
    - 不绑定任何 SalesItem
    - external_id 必填
    - external_finished_products 必填（至少 1 条）
    """
    from basic_info.models import Customer

    # 验证客户存在
    try:
        customer = Customer.objects.get(id=customer_id)
    except Customer.DoesNotExist:
        raise ValueError(f"客户 {customer_id} 不存在")

    # merchant 隔离
    emp = getattr(created_by, "employee", None)
    merchant = getattr(emp, "merchant", None) if emp else None
    if not merchant:
        raise ValueError("用户未关联商户，无法创建出货单")
    if customer.merchant_id != merchant.id:
        raise ValueError("无权限为该客户创建出货单")

    external_id = (external_id or "").strip()
    if not external_id:
        raise ValueError("external_id 不能为空")
    if not external_finished_products:
        raise ValueError("external_finished_products 不能为空")

    shipment = Shipment.objects.create(
        merchant=merchant,
        customer=customer,
        shipment_date=shipment_date,
        address=address or "",
        contact_name=contact_name or "",
        contact_phone=contact_phone or "",
        area=(area or "").strip(),
        remark=remark,
        created_by=created_by,
        external_id=external_id,
    )

    objs = []
    for item in external_finished_products:
        objs.append(
            ExternalFinishedProduct(
                shipment=shipment,
                style_name=item.get("style_name", ""),
                num_of_rolls=item.get("num_of_rolls", 0),
                remark=item.get("remark") or "",
                created_by=created_by,
            )
        )

    ExternalFinishedProduct.objects.bulk_create(objs)
    return shipment


@transaction.atomic
def create_sales_item(
    printing_job_id: int,
    name: str,
    quantity: str,
    unit: int,
    created_by,
    customer_id: int | None = None,
    remark: str = "",
    position: str = "",
    merge_remark: dict | None = None,
) -> SalesItem:
    """
    手动创建销售品

    Args:
        printing_job_id: 生产任务ID（必填）
        name: 销售品名称
        quantity: 数量（字符串，会被转换为Decimal）
        unit: 单位（1=米, 2=件, 3=码, 4=个）
        created_by: 创建人
        customer_id: 客户ID（可选）
        remark: 备注（可选）
        position: 货位（可选）

    Returns:
        创建的 SalesItem 实例

    Raises:
        ValueError: 如果生产任务不存在或不属于当前商户
    """
    # 获取当前用户的商户
    emp = getattr(created_by, "employee", None)
    merchant = getattr(emp, "merchant", None) if emp else None
    if not merchant:
        raise ValueError("用户未关联商户，无法创建销售品")

    # 验证生产任务存在且属于当前商户
    printing_job = _get_printing_job_for_sales_item(
        printing_job_id=printing_job_id,
        merchant=merchant,
    )

    # 转换数量为Decimal
    quantity_decimal = _normalize_sales_item_quantity(quantity)

    # 如果未提供客户ID，尝试从主订单获取
    if (
        customer_id is None
        and printing_job.printing_order
        and printing_job.printing_order.customer
    ):
        customer_id = printing_job.printing_order.customer_id

    # 创建销售品
    sales_item = SalesItem.objects.create(
        shipment=None,  # 初始状态：待分配
        merchant=merchant,
        name=name,
        quantity=quantity_decimal,
        unit=unit,
        created_by=created_by,
        printing_job_id=printing_job.id,
        customer_id=customer_id,
        remark=remark,
        position=position,
        merge_remark=merge_remark,
    )

    return sales_item


def get_merge_remark_jobs_by_main_job(
    printing_job_id: int,
    merchant,
) -> list[list[int]]:
    """
    查找指定 printing_job_id 作为 main_job 的所有销售品，返回每条记录的 jobs 列表。

    同一个 printing_job_id 可能出现在多条销售品的 main_job 中，
    因此返回值是一个二维列表，每个元素对应一条匹配记录的 jobs。

    Args:
        printing_job_id: 要查询的生产任务ID
        merchant: 当前商户

    Returns:
        匹配记录的 jobs 列表集合，无匹配时返回空列表。
        示例: [[9777, 9776], [9777, 9780]]
    """
    merge_remarks = list(
        get_active_sales_items_queryset()
        .filter(merchant=merchant, merge_remark__main_job=printing_job_id)
        .values_list("merge_remark", flat=True)
    )
    if not merge_remarks:
        return []
    return [mr.get("jobs", []) for mr in merge_remarks if mr]
