package com.ruoyi.asset.service.impl;

import java.util.ArrayList;
import java.util.Arrays;
import java.util.Comparator;
import java.util.Date;
import java.util.HashSet;
import java.util.List;
import java.util.Objects;
import java.util.Set;
import com.ruoyi.asset.constant.AssetStatus;
import com.ruoyi.asset.constant.ReturnOrderStatus;
import com.ruoyi.asset.domain.AmsAsset;
import com.ruoyi.asset.domain.AmsAssetLocation;
import com.ruoyi.asset.domain.AmsReturnAssetCandidate;
import com.ruoyi.asset.domain.AmsReturnOrder;
import com.ruoyi.asset.domain.AmsReturnOrderItem;
import com.ruoyi.asset.domain.AmsWarehouse;
import com.ruoyi.asset.domain.AssetTransitionContext;
import com.ruoyi.asset.mapper.AmsAssetMapper;
import com.ruoyi.asset.mapper.AmsReturnOrderMapper;
import com.ruoyi.asset.service.IAmsAssetLocationService;
import com.ruoyi.asset.service.IAmsReturnOrderService;
import com.ruoyi.asset.service.IAmsWarehouseService;
import com.ruoyi.asset.service.IAssetStatusTransitionService;
import com.ruoyi.common.constant.UserConstants;
import com.ruoyi.common.core.domain.entity.SysDept;
import com.ruoyi.common.core.domain.entity.SysUser;
import com.ruoyi.common.core.text.Convert;
import com.ruoyi.common.exception.ServiceException;
import com.ruoyi.common.utils.DateUtils;
import com.ruoyi.common.utils.StringUtils;
import com.ruoyi.system.service.ISysCodeRuleService;
import com.ruoyi.system.service.ISysDeptService;
import com.ruoyi.system.service.ISysUserService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;

/**
 * 退库管理Service业务层处理
 * <p>
 * 核心业务职责包括：
 * 1. 资产状态及使用归属检验：防止非“在用”或已被其他有效退库单占用的资产被加入单据；
 * 2. 退库单状态流转控制：严格遵守 草稿(DRAFT) -> 待确认(PENDING_CONFIRM) -> 已退库(RETURNED) 这一单向流转顺序；
 * 3. 核心字段篡改预防：在更新单据时，涉及单据号及申请人/部门的快照字段强制复用原始数据；
 * 4. 实物确认时的资产流转：成功确认退库后，将委托公共流转服务更新资产台账和记录资产履历，并锁定资产防止死锁。
 * </p>
 *
 * @author Yangk
 */
@Service
public class AmsReturnOrderServiceImpl implements IAmsReturnOrderService
{
    /** 退库单在 sys_code_rule 中配置的自动编码规则标识 */
    private static final String RETURN_ORDER_RULE = "RETURN_ORDER";

    private static final String ENABLED_YES = "Y";

    private static final String DEL_FLAG_NORMAL = "0";

    @Autowired
    private AmsReturnOrderMapper amsReturnOrderMapper;

    @Autowired
    private AmsAssetMapper amsAssetMapper;

    @Autowired
    private ISysCodeRuleService sysCodeRuleService;

    @Autowired
    private IAmsWarehouseService amsWarehouseService;

    @Autowired
    private IAmsAssetLocationService amsAssetLocationService;

    @Autowired
    private ISysDeptService sysDeptService;

    @Autowired
    private ISysUserService sysUserService;

    @Autowired
    private IAssetStatusTransitionService assetStatusTransitionService;

    @Override
    public AmsReturnOrder selectAmsReturnOrderByOrderId(Long orderId)
    {
        AmsReturnOrder order = amsReturnOrderMapper.selectAmsReturnOrderByOrderId(orderId);
        fillOriginReferences(order);
        return order;
    }

    @Override
    public List<AmsReturnOrder> selectAmsReturnOrderList(AmsReturnOrder order)
    {
        return amsReturnOrderMapper.selectAmsReturnOrderList(order);
    }

    @Override
    public List<AmsReturnAssetCandidate> selectAvailableReturnAssetList(AmsAsset asset, Long currentOrderId,
            Long originWarehouseId, Boolean missingOriginOnly)
    {
        return amsReturnOrderMapper.selectAvailableReturnAssetList(asset, currentOrderId,
                originWarehouseId, missingOriginOnly,
                AssetStatus.IN_USE, ReturnOrderStatus.DRAFT, ReturnOrderStatus.PENDING_CONFIRM);
    }

    /**
     * 保存退库草稿单。
     * <p>
     * 业务意图与安全设计：
     * 1. 申请人、申请部门信息进行合法性审查，并强制校验“申请人是否属于申请部门”，防范非法的业务提交；
     * 2. 调用自动编码规则 `sysCodeRuleService.nextCode` 生成全局唯一的退库单号，该调用在事务内执行锁行；
     * 3. 对单据中的每个资产明细项锁定并写入原始状态及当前使用归属的快照（作为后续确认时防脏写的版本基准）。
     * </p>
     */
    @Override
    @Transactional(rollbackFor = Exception.class)
    public int insertAmsReturnOrder(AmsReturnOrder order, Long applicantId, String applicantName,
            Long applyDeptId)
    {
        validateOrderRequest(order);
        fillApplicantSnapshots(order, applicantId, applicantName, applyDeptId);
        order.setReturnNo(sysCodeRuleService.nextCode(RETURN_ORDER_RULE));
        order.setOrderStatus(ReturnOrderStatus.DRAFT);
        order.setConfirmUserId(null);
        order.setConfirmUserName(null);
        order.setConfirmTime(null);
        order.setDelFlag(DEL_FLAG_NORMAL);
        order.setCreateTime(DateUtils.getNowDate());
        fillOrderSnapshots(order, null);

        if (amsReturnOrderMapper.insertAmsReturnOrder(order) != 1 || StringUtils.isNull(order.getOrderId()))
        {
            throw new ServiceException("退库单保存失败");
        }
        insertReturnOrderItems(order);
        return 1;
    }

    /**
     * 更新退库草稿单。
     * <p>
     * 业务意图与防篡改：
     * 1. 只有处于“草稿(DRAFT)”状态的退库单才允许修改；
     * 2. 防篡改设计：退库单号 `returnNo`、申请人/部门快照等字段具有受控特性，不允许通过前台直接修改。
     *    故在此处强制使用数据库中现存的原始值覆盖传入的值，然后再持久化。
     * 3. 修改时采取“先删明细再重新插入”的方式，确保保存后单据明细项与前台选择的状态完全一致。
     * </p>
     */
    @Override
    @Transactional(rollbackFor = Exception.class)
    public int updateAmsReturnOrder(AmsReturnOrder order)
    {
        if (StringUtils.isNull(order) || StringUtils.isNull(order.getOrderId()))
        {
            throw new ServiceException("退库单ID不能为空");
        }
        AmsReturnOrder current = requireOrderForUpdate(order.getOrderId(), ReturnOrderStatus.DRAFT,
                "仅草稿退库单允许修改");
        validateOrderRequest(order);

        // 受控字段必须使用数据库当前值，防止普通编辑绕过单据状态和申请人快照。
        order.setReturnNo(current.getReturnNo());
        order.setApplicantId(current.getApplicantId());
        order.setApplicantName(current.getApplicantName());
        order.setApplyDeptId(current.getApplyDeptId());
        order.setApplyDeptName(current.getApplyDeptName());
        order.setOrderStatus(ReturnOrderStatus.DRAFT);
        order.setConfirmUserId(null);
        order.setConfirmUserName(null);
        order.setConfirmTime(null);
        order.setUpdateTime(DateUtils.getNowDate());
        fillOrderSnapshots(order, order.getOrderId());

        amsReturnOrderMapper.deleteAmsReturnOrderItemByOrderId(order.getOrderId());
        insertReturnOrderItems(order);
        if (amsReturnOrderMapper.updateAmsReturnOrder(order) != 1)
        {
            throw new ServiceException("退库单状态已变化，保存失败");
        }
        return 1;
    }

    /**
     * 提交退库单。
     * <p>
     * 业务意图与防呆设计：
     * 1. 采用分布式/单机锁方案的前置机制：使用 `requireOrderForUpdate` 发送 `FOR UPDATE` 对退库单主行加排他锁，
     *    并校验其状态必须为“草稿(DRAFT)”。防止其它并发事务重复提交或在审批流转时篡改数据；
     * 2. 调用 `validateOrderReadyForFlow` 重新对该退库单关联的所有资产执行资产状态（必须是在用）与使用归属（不可在草稿期间改变）的强校验。
     * 3. 在单据更新时校验影响行数，以确保更新的安全一致性。
     * </p>
     */
    @Override
    @Transactional(rollbackFor = Exception.class)
    public int submitReturn(Long orderId, String operateLoginName)
    {
        AmsReturnOrder order = requireOrderForUpdate(orderId, ReturnOrderStatus.DRAFT,
                "仅草稿退库单允许提交");
        validateLoginName(operateLoginName);
        validateOrderReadyForFlow(order);

        order.setOrderStatus(ReturnOrderStatus.PENDING_CONFIRM);
        order.setUpdateBy(StringUtils.trim(operateLoginName));
        order.setUpdateTime(DateUtils.getNowDate());
        if (amsReturnOrderMapper.submitAmsReturnOrder(order) != 1)
        {
            throw new ServiceException("退库单状态已变化，提交失败");
        }
        return 1;
    }

    /**
     * 确认退库。
     * <p>
     * 业务意图与事务一致性设计：
     * 1. 使用 `requireOrderForUpdate` 对单据加排他锁，验证当前状态为“待确认(PENDING_CONFIRM)”。
     * 2. 调用 `validateOrderReadyForFlow` 进行最终状态锁定：该方法会锁住单据内所有资产并再次确认没有发生属性篡改。
     * 3. 流转委托设计：退库核心逻辑（清空使用责任人及部门，回退到目标在库仓库/位置，生成资产履历）被统一收口在公共的资产生命周期流转服务
     *    `assetStatusTransitionService.confirmReturn` 中。
     *    这样做能防止我们在不同业务模块重复手写资产状态修改 SQL 导致数据不一致（如遗漏履历记录或 RFID 绑定未处理）。
     * </p>
     */
    @Override
    @Transactional(rollbackFor = Exception.class)
    public int confirmReturn(Long orderId, Long operateUserId, String operateUserName,
             String operateLoginName)
    {
        AmsReturnOrder order = requireOrderForUpdate(orderId, ReturnOrderStatus.PENDING_CONFIRM,
                "仅待确认退库单允许确认");
        validateOperator(operateUserId, operateUserName, "确认人");
        validateLoginName(operateLoginName);
        validateOrderReadyForFlow(order);

        for (AmsReturnOrderItem item : sortedItems(order))
        {
            AssetTransitionContext context = new AssetTransitionContext();
            context.setSourceOrderId(order.getOrderId());
            context.setSourceOrderNo(order.getReturnNo());
            context.setSourceItemId(item.getItemId());
            context.setOperateUserId(operateUserId);
            context.setOperateUserName(StringUtils.trim(operateUserName));
            context.setOperateLoginName(StringUtils.trim(operateLoginName));
            context.setChangeSummary("确认资产退库");
            context.setRemark(order.getRemark());
            assetStatusTransitionService.confirmReturn(item.getAssetId(), item.getAfterWarehouseId(),
                    item.getAfterLocationId(), context);
        }

        Date now = DateUtils.getNowDate();
        order.setOrderStatus(ReturnOrderStatus.RETURNED);
        order.setConfirmUserId(operateUserId);
        order.setConfirmUserName(StringUtils.trim(operateUserName));
        order.setConfirmTime(now);
        order.setUpdateBy(StringUtils.trim(operateLoginName));
        order.setUpdateTime(now);
        if (amsReturnOrderMapper.confirmAmsReturnOrder(order) != 1)
        {
            throw new ServiceException("退库单状态已变化，确认退库失败");
        }
        return 1;
    }

    @Override
    @Transactional(rollbackFor = Exception.class)
    public int deleteAmsReturnOrderByOrderIds(String orderIds)
    {
        Long[] sortedIds = Arrays.stream(Convert.toStrArray(orderIds))
                .map(Long::valueOf).sorted().toArray(Long[]::new);
        int rows = 0;
        for (Long orderId : sortedIds)
        {
            AmsReturnOrder order = amsReturnOrderMapper.selectAmsReturnOrderByOrderIdForUpdate(orderId);
            if (StringUtils.isNull(order))
            {
                continue;
            }
            requireDraft(order);
            amsReturnOrderMapper.deleteAmsReturnOrderItemByOrderId(orderId);
            rows += amsReturnOrderMapper.deleteAmsReturnOrderByOrderId(orderId);
        }
        return rows;
    }

    @Override
    @Transactional(rollbackFor = Exception.class)
    public int deleteAmsReturnOrderByOrderId(Long orderId)
    {
        AmsReturnOrder order = amsReturnOrderMapper.selectAmsReturnOrderByOrderIdForUpdate(orderId);
        if (StringUtils.isNull(order))
        {
            return 0;
        }
        requireDraft(order);
        amsReturnOrderMapper.deleteAmsReturnOrderItemByOrderId(orderId);
        return amsReturnOrderMapper.deleteAmsReturnOrderByOrderId(orderId);
    }

    private void validateOrderRequest(AmsReturnOrder order)
    {
        if (StringUtils.isNull(order))
        {
            throw new ServiceException("退库单不能为空");
        }
        if (order.getAmsReturnOrderItemList() == null || order.getAmsReturnOrderItemList().isEmpty())
        {
            throw new ServiceException("退库单明细不能为空");
        }
        validateLength(order.getRemark(), 500, "备注");
    }

    private void fillApplicantSnapshots(AmsReturnOrder order, Long applicantId, String applicantName,
            Long applyDeptId)
    {
        validateOperator(applicantId, applicantName, "申请人");
        SysUser applicant = requireNormalUser(applicantId, "申请人");
        SysDept applyDept = requireNormalDept(applyDeptId, "申请部门");
        if (!Objects.equals(applicant.getDeptId(), applyDept.getDeptId()))
        {
            throw new ServiceException("申请人不属于当前申请部门");
        }
        order.setApplicantId(applicantId);
        order.setApplicantName(StringUtils.trim(applicantName));
        order.setApplyDeptId(applyDept.getDeptId());
        order.setApplyDeptName(applyDept.getDeptName());
    }

    /**
     * 保存草稿时按资产ID升序锁定，回填退库前使用归属和退库后仓位快照。
     */
    private void fillOrderSnapshots(AmsReturnOrder order, Long currentOrderId)
    {
        AmsWarehouse warehouse = requireEnabledWarehouse(order.getReceiveWarehouseId());
        order.setReceiveWarehouseCode(warehouse.getWarehouseCode());
        order.setReceiveWarehouseName(warehouse.getWarehouseName());

        Set<Long> assetIds = new HashSet<>();
        ReturnOriginGroup originGroup = new ReturnOriginGroup();
        List<AmsReturnOrderItem> items = sortedItems(order);
        order.setAmsReturnOrderItemList(items);
        for (AmsReturnOrderItem item : items)
        {
            AmsAsset asset = lockReturnableAsset(item.getAssetId());
            if (!assetIds.add(asset.getAssetId()))
            {
                throw new ServiceException("同一退库单不能重复选择资产");
            }
            validateNotOccupied(asset, currentOrderId);
            originGroup.accept(asset, fillOriginReference(item));
            fillAssetSnapshots(item, asset);
            fillTargetSnapshots(item, asset, warehouse);
            item.setReturnNo(order.getReturnNo());
            item.setDelFlag(DEL_FLAG_NORMAL);
            validateLength(item.getRemark(), 500, "明细备注");
        }
    }

    /**
     * 提交和确认前重新锁定资产，阻断草稿期间使用归属变化或资产被其他退库单占用。
     */
    private void validateOrderReadyForFlow(AmsReturnOrder order)
    {
        AmsWarehouse warehouse = requireEnabledWarehouse(order.getReceiveWarehouseId());
        List<AmsReturnOrderItem> items = sortedItems(order);
        if (items.isEmpty())
        {
            throw new ServiceException("退库单明细不能为空");
        }
        Set<Long> assetIds = new HashSet<>();
        ReturnOriginGroup originGroup = new ReturnOriginGroup();
        for (AmsReturnOrderItem item : items)
        {
            AmsAsset asset = lockReturnableAsset(item.getAssetId());
            if (!assetIds.add(asset.getAssetId()))
            {
                throw new ServiceException("同一退库单不能重复选择资产");
            }
            validateNotOccupied(asset, order.getOrderId());
            originGroup.accept(asset, fillOriginReference(item));
            validateBeforeSnapshot(item, asset);
            fillTargetSnapshots(item, asset, warehouse);
        }
    }

    private AmsAsset lockReturnableAsset(Long assetId)
    {
        if (StringUtils.isNull(assetId))
        {
            throw new ServiceException("退库资产不能为空");
        }
        AmsAsset asset = amsAssetMapper.selectAmsAssetByAssetIdForUpdate(assetId);
        if (StringUtils.isNull(asset))
        {
            throw new ServiceException("退库资产不存在或已删除");
        }
        if (!StringUtils.equals(AssetStatus.IN_USE, asset.getAssetStatus()))
        {
            throw new ServiceException(StringUtils.format("资产【{}】当前状态不允许退库", asset.getAssetCode()));
        }
        if (StringUtils.isNull(asset.getUseDeptId()) || StringUtils.isNull(asset.getUseUserId())
                || StringUtils.isEmpty(asset.getUseDeptName()) || StringUtils.isEmpty(asset.getUseUserName()))
        {
            throw new ServiceException(StringUtils.format("在用资产【{}】缺少当前使用部门或使用人", asset.getAssetCode()));
        }
        return asset;
    }

    private void validateNotOccupied(AmsAsset asset, Long currentOrderId)
    {
        if (amsReturnOrderMapper.countOtherActiveReturnOrderByAssetId(asset.getAssetId(), currentOrderId,
                ReturnOrderStatus.DRAFT, ReturnOrderStatus.PENDING_CONFIRM) > 0)
        {
            throw new ServiceException(StringUtils.format("资产【{}】已被其他有效退库单占用", asset.getAssetCode()));
        }
    }

    private void fillAssetSnapshots(AmsReturnOrderItem item, AmsAsset asset)
    {
        item.setAssetCode(asset.getAssetCode());
        item.setAssetName(asset.getAssetName());
        item.setCategoryId(asset.getCategoryId());
        item.setCategoryCode(asset.getCategoryCode());
        item.setCategoryName(asset.getCategoryName());
        item.setSpecModel(asset.getSpecModel());
        item.setBrand(asset.getBrand());
        item.setAssetStatus(asset.getAssetStatus());
        item.setBeforeDeptId(asset.getUseDeptId());
        item.setBeforeDeptName(asset.getUseDeptName());
        item.setBeforeUserId(asset.getUseUserId());
        item.setBeforeUserName(asset.getUseUserName());
    }

    /** 回填最近一次已完成领用的原仓位，供页面默认带出和后端单仓库规则校验。 */
    private AmsReturnAssetCandidate fillOriginReference(AmsReturnOrderItem item)
    {
        AmsReturnAssetCandidate candidate = amsReturnOrderMapper.selectReturnAssetCandidateByAssetId(item.getAssetId());
        item.setOriginWarehouseId(candidate == null ? null : candidate.getOriginWarehouseId());
        item.setOriginWarehouseCode(candidate == null ? null : candidate.getOriginWarehouseCode());
        item.setOriginWarehouseName(candidate == null ? null : candidate.getOriginWarehouseName());
        item.setOriginWarehouseEnabled(candidate == null ? null : candidate.getOriginWarehouseEnabled());
        item.setOriginLocationId(candidate == null ? null : candidate.getOriginLocationId());
        item.setOriginLocationCode(candidate == null ? null : candidate.getOriginLocationCode());
        item.setOriginLocationName(candidate == null ? null : candidate.getOriginLocationName());
        item.setOriginLocationEnabled(candidate == null ? null : candidate.getOriginLocationEnabled());
        return candidate;
    }

    private void fillOriginReferences(AmsReturnOrder order)
    {
        if (order == null || order.getAmsReturnOrderItemList() == null)
        {
            return;
        }
        for (AmsReturnOrderItem item : order.getAmsReturnOrderItemList())
        {
            fillOriginReference(item);
        }
    }

    private void fillTargetSnapshots(AmsReturnOrderItem item, AmsAsset asset, AmsWarehouse warehouse)
    {
        AmsAssetLocation location = requireEnabledLocation(item.getAfterLocationId());
        if (!Objects.equals(warehouse.getWarehouseId(), location.getWarehouseId()))
        {
            throw new ServiceException(StringUtils.format("资产【{}】的退库位置不属于接收仓库", asset.getAssetCode()));
        }
        item.setAfterWarehouseId(warehouse.getWarehouseId());
        item.setAfterWarehouseCode(warehouse.getWarehouseCode());
        item.setAfterWarehouseName(warehouse.getWarehouseName());
        item.setAfterLocationCode(location.getLocationCode());
        item.setAfterLocationName(location.getLocationName());
    }

    private void validateBeforeSnapshot(AmsReturnOrderItem item, AmsAsset asset)
    {
        if (!Objects.equals(item.getBeforeDeptId(), asset.getUseDeptId())
                || !Objects.equals(item.getBeforeUserId(), asset.getUseUserId()))
        {
            throw new ServiceException(StringUtils.format("资产【{}】当前使用归属已变化，请重新编辑并提交退库单",
                    asset.getAssetCode()));
        }
    }

    private AmsWarehouse requireEnabledWarehouse(Long warehouseId)
    {
        if (StringUtils.isNull(warehouseId))
        {
            throw new ServiceException("接收仓库不能为空");
        }
        AmsWarehouse warehouse = amsWarehouseService.selectAmsWarehouseByWarehouseId(warehouseId);
        if (StringUtils.isNull(warehouse) || !StringUtils.equals(ENABLED_YES, warehouse.getEnabled()))
        {
            throw new ServiceException("接收仓库不存在或已停用");
        }
        return warehouse;
    }

    private AmsAssetLocation requireEnabledLocation(Long locationId)
    {
        if (StringUtils.isNull(locationId))
        {
            throw new ServiceException("退库位置不能为空");
        }
        AmsAssetLocation location = amsAssetLocationService.selectAmsAssetLocationByLocationId(locationId);
        if (StringUtils.isNull(location) || !StringUtils.equals(ENABLED_YES, location.getEnabled()))
        {
            throw new ServiceException("退库位置不存在或已停用");
        }
        return location;
    }

    private SysDept requireNormalDept(Long deptId, String fieldName)
    {
        if (StringUtils.isNull(deptId))
        {
            throw new ServiceException(fieldName + "不能为空");
        }
        SysDept dept = sysDeptService.selectDeptById(deptId);
        if (StringUtils.isNull(dept)
                || !StringUtils.equals(UserConstants.DEPT_NORMAL, dept.getStatus())
                || !StringUtils.equals(DEL_FLAG_NORMAL, dept.getDelFlag()))
        {
            throw new ServiceException(fieldName + "不存在或已停用");
        }
        return dept;
    }

    private SysUser requireNormalUser(Long userId, String fieldName)
    {
        if (StringUtils.isNull(userId))
        {
            throw new ServiceException(fieldName + "不能为空");
        }
        SysUser user = sysUserService.selectUserById(userId);
        if (StringUtils.isNull(user)
                || !StringUtils.equals(UserConstants.NORMAL, user.getStatus())
                || !StringUtils.equals(DEL_FLAG_NORMAL, user.getDelFlag()))
        {
            throw new ServiceException(fieldName + "不存在或已停用");
        }
        return user;
    }

    private AmsReturnOrder requireOrderForUpdate(Long orderId, String requiredStatus, String message)
    {
        if (StringUtils.isNull(orderId))
        {
            throw new ServiceException("退库单ID不能为空");
        }
        AmsReturnOrder order = amsReturnOrderMapper.selectAmsReturnOrderByOrderIdForUpdate(orderId);
        if (StringUtils.isNull(order))
        {
            throw new ServiceException("退库单不存在或已删除");
        }
        if (!StringUtils.equals(requiredStatus, order.getOrderStatus()))
        {
            throw new ServiceException(message);
        }
        return order;
    }

    private void requireDraft(AmsReturnOrder order)
    {
        if (!StringUtils.equals(ReturnOrderStatus.DRAFT, order.getOrderStatus()))
        {
            throw new ServiceException("仅草稿退库单允许删除");
        }
    }

    /** 
     * 按资产ID升序对明细进行排序。
     * <p>
     * 避坑考量：
     * 这是典型的防死锁（Deadlock Prevention）设计。
     * 当多个并发事务同时操作包含多个资产的生命周期单据时，若获取行锁（`FOR UPDATE`）的顺序不一致：
     * 例如事务A先锁资产1再锁资产2，而事务B并发先锁资产2再锁资产1，就极易引发数据库死锁。
     * 通过在应用层使用此方法统一按资产 ID 升序排序，使所有涉及批量资产锁定的逻辑（保存、提交、确认）
     * 拥有全局一致的锁定顺序，从根本上消除了并发死锁的风险。
     * </p>
     */
    private List<AmsReturnOrderItem> sortedItems(AmsReturnOrder order)
    {
        if (order.getAmsReturnOrderItemList() == null)
        {
            return new ArrayList<>();
        }
        List<AmsReturnOrderItem> items = new ArrayList<>(order.getAmsReturnOrderItemList());
        items.sort(Comparator.comparing(AmsReturnOrderItem::getAssetId,
                Comparator.nullsFirst(Long::compareTo)));
        return items;
    }

    private void insertReturnOrderItems(AmsReturnOrder order)
    {
        List<AmsReturnOrderItem> items = new ArrayList<>();
        Date now = DateUtils.getNowDate();
        for (AmsReturnOrderItem item : order.getAmsReturnOrderItemList())
        {
            item.setItemId(null);
            item.setOrderId(order.getOrderId());
            item.setCreateBy(StringUtils.isNotEmpty(order.getCreateBy()) ? order.getCreateBy() : order.getUpdateBy());
            item.setCreateTime(now);
            item.setUpdateBy(null);
            item.setUpdateTime(null);
            items.add(item);
        }
        if (amsReturnOrderMapper.batchAmsReturnOrderItem(items) != items.size())
        {
            throw new ServiceException("退库单明细保存失败");
        }
    }

    private void validateOperator(Long userId, String userName, String fieldName)
    {
        if (StringUtils.isNull(userId) || StringUtils.isEmpty(StringUtils.trim(userName)))
        {
            throw new ServiceException(fieldName + "ID和名称不能为空");
        }
        validateLength(StringUtils.trim(userName), 100, fieldName + "名称");
    }

    private void validateLoginName(String loginName)
    {
        if (StringUtils.isEmpty(StringUtils.trim(loginName)))
        {
            throw new ServiceException("操作登录账号不能为空");
        }
        validateLength(StringUtils.trim(loginName), 64, "操作登录账号");
    }

    private void validateLength(String value, int maxLength, String fieldName)
    {
        if (StringUtils.isNotEmpty(value) && value.length() > maxLength)
        {
            throw new ServiceException(fieldName + "长度不能超过" + maxLength + "个字符");
        }
    }

    /**
     * 退库单原仓库一致性校验分组辅助类。
     * <p>
     * 业务意图与管控：
     * 1. “一单一仓库原则”：在一张退库单中，原则上不允许将来自不同领用仓库（即不同的 `originWarehouseId`）的资产混在一起退库。
     *    因为现场交接和收料一般发生在特定的仓库，混杂多个原仓库会增加清点和确认流程的混乱度。
     * 2. 来源可追溯性一致：系统严格限制“可追溯领用来源资产”（曾经过领用单流程）与“无明确来源资产”混单提交。
     *    无来源资产由于没有历史领用仓位记录，其退库不具备自动原仓对齐校验。
     * </p>
     */
    private static class ReturnOriginGroup
    {
        private boolean initialized;

        private boolean traceable;

        private Long originWarehouseId;

        /**
         * 接收并核对资产的来源一致性。
         *
         * @param asset 待核查资产
         * @param candidate 来源资产候选快照
         * @throws ServiceException 若来源仓库类型或仓库 ID 不一致时，直接抛出业务异常阻断
         */
        void accept(AmsAsset asset, AmsReturnAssetCandidate candidate)
        {
            boolean currentTraceable = candidate != null && candidate.isOriginTraceable();
            Long currentWarehouseId = currentTraceable ? candidate.getOriginWarehouseId() : null;
            if (!initialized)
            {
                initialized = true;
                traceable = currentTraceable;
                originWarehouseId = currentWarehouseId;
                return;
            }
            if (traceable != currentTraceable)
            {
                throw new ServiceException(StringUtils.format(
                        "资产【{}】与当前退库单的原领用仓位来源类型不一致", asset.getAssetCode()));
            }
            if (traceable && !Objects.equals(originWarehouseId, currentWarehouseId))
            {
                throw new ServiceException(StringUtils.format(
                        "资产【{}】的原领用仓库与当前退库单不一致", asset.getAssetCode()));
            }
        }
    }
}
