package com.ruoyi.asset.service.impl;

import java.util.Date;
import com.ruoyi.asset.constant.AssetLifecycleBusinessType;
import com.ruoyi.asset.constant.AssetStatus;
import com.ruoyi.asset.constant.RfidBindStatus;
import com.ruoyi.asset.constant.RfidTagStatus;
import com.ruoyi.asset.domain.AmsAsset;
import com.ruoyi.asset.domain.AmsAssetLifecycleLog;
import com.ruoyi.asset.domain.AmsRfidTag;
import com.ruoyi.asset.domain.RfidBindingContext;
import com.ruoyi.asset.domain.RfidBindingResult;
import com.ruoyi.asset.mapper.AmsAssetMapper;
import com.ruoyi.asset.mapper.AmsRfidTagMapper;
import com.ruoyi.asset.service.IAssetLifecycleService;
import com.ruoyi.asset.service.IRfidBindingService;
import com.ruoyi.common.exception.ServiceException;
import com.ruoyi.common.utils.DateUtils;
import com.ruoyi.common.utils.StringUtils;
import com.ruoyi.common.utils.bean.BeanUtils;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;

/**
 * RFID绑定公共服务实现
 * <p>
 * 所有RFID绑定与解绑入口统一通过本服务维护资产和标签两侧当前关系，并写入资产履历。
 * 固定按“标签 → 资产”顺序加锁，避免不同入口使用不同锁顺序增加死锁风险。
 * </p>
 *
 * @author Yangk
 */
@Service
public class RfidBindingServiceImpl implements IRfidBindingService
{
    @Autowired
    private AmsRfidTagMapper amsRfidTagMapper;

    @Autowired
    private AmsAssetMapper amsAssetMapper;

    @Autowired
    private IAssetLifecycleService assetLifecycleService;

    /**
     * 按资产编码+EPC 建立 RFID 绑定关系。
     * <p>加锁顺序：先锁标签（EPC唯一索引定位），再锁资产（资产编码唯一索引定位），
     * 与 unbind 保持一致的"标签->资产"锁序，消除死锁风险。</p>
     */
    @Override
    @Transactional(rollbackFor = Exception.class)
    public RfidBindingResult bind(String assetCode, String epcCode, RfidBindingContext context)
    {
        String normalizedAssetCode = StringUtils.trim(assetCode);
        String normalizedEpcCode = StringUtils.trim(epcCode);
        validateContext(context);
        validateRequired(normalizedAssetCode, 64, "资产编码");
        validateRequired(normalizedEpcCode, 128, "EPC编码");

        // 第一步加锁：按 EPC 行锁标签，保证并发绑定互斥
        AmsRfidTag lockedTag = amsRfidTagMapper.selectAmsRfidTagByEpcForUpdate(normalizedEpcCode);
        if (StringUtils.isNull(lockedTag))
        {
            throw new ServiceException("RFID标签不存在或已删除");
        }
        validateTagForBind(lockedTag);

        // 第二步加锁：按资产编码行锁资产，保证同一资产不会被并发绑定
        AmsAsset lockedAsset = amsAssetMapper.selectAmsAssetByAssetCodeForUpdate(normalizedAssetCode);
        if (StringUtils.isNull(lockedAsset))
        {
            throw new ServiceException("资产不存在或已删除");
        }
        validateAssetForBind(lockedAsset);

        // 构造变更前后快照（深拷贝），供履历记录和返回值使用，不影响锁定的原始对象
        Date operateTime = DateUtils.getNowDate();
        AmsRfidTag beforeTag = copyTag(lockedTag);
        AmsRfidTag afterTag = copyTag(lockedTag);
        fillBoundTag(afterTag, lockedAsset, context, operateTime);
        AmsAsset beforeAsset = copyAsset(lockedAsset);
        AmsAsset afterAsset = copyAsset(lockedAsset);
        fillBoundAsset(afterAsset, lockedTag, context, operateTime);

        // 先写标签侧：SQL WHERE 包含 bind_status='UNBOUND' AND asset_id IS NULL 的 CAS 守卫
        if (amsRfidTagMapper.bindAmsRfidTag(afterTag) != 1)
        {
            throw new ServiceException("RFID标签当前状态已变化，绑定失败");
        }
        // 再写资产侧：SQL WHERE 包含 tag_id IS NULL AND asset_status!=DISPOSED 的 CAS 守卫
        if (amsAssetMapper.bindAssetRfid(afterAsset.getAssetId(), afterAsset.getTagId(),
                afterAsset.getTagCode(), afterAsset.getEpcCode(), AssetStatus.DISPOSED,
                afterAsset.getUpdateBy(), afterAsset.getUpdateTime()) != 1)
        {
            throw new ServiceException("资产当前RFID绑定信息已变化，绑定失败");
        }
        assetLifecycleService.recordLifecycle(beforeAsset, afterAsset,
                buildLifecycleLog(afterTag, context, AssetLifecycleBusinessType.RFID_BIND,
                        buildBindSummary(afterAsset, afterTag), operateTime));
        return new RfidBindingResult(beforeAsset, copyAsset(afterAsset), beforeTag, copyTag(afterTag));
    }

    /**
     * 按标签ID解绑 RFID 与资产的绑定关系。
     * <p>加锁顺序：先锁标签（tagId），再锁标签指向的资产（tag.assetId），
     * 保持与 bind 一致的"标签->资产"锁序。</p>
     */
    @Override
    @Transactional(rollbackFor = Exception.class)
    public RfidBindingResult unbind(Long tagId, RfidBindingContext context)
    {
        if (StringUtils.isNull(tagId))
        {
            throw new ServiceException("RFID标签ID不能为空");
        }
        validateContext(context);

        AmsRfidTag lockedTag = amsRfidTagMapper.selectAmsRfidTagByTagIdForUpdate(tagId);
        if (StringUtils.isNull(lockedTag))
        {
            throw new ServiceException("RFID标签不存在或已删除");
        }
        validateTagForUnbind(lockedTag);

        AmsAsset lockedAsset = amsAssetMapper.selectAmsAssetByAssetIdForUpdate(lockedTag.getAssetId());
        if (StringUtils.isNull(lockedAsset))
        {
            throw new ServiceException("RFID标签绑定资产不存在或已删除，无法解绑");
        }
        validateBindingMatches(lockedAsset, lockedTag);

        Date operateTime = DateUtils.getNowDate();
        AmsRfidTag beforeTag = copyTag(lockedTag);
        AmsRfidTag afterTag = copyTag(lockedTag);
        clearTagBinding(afterTag, context, operateTime);
        AmsAsset beforeAsset = copyAsset(lockedAsset);
        AmsAsset afterAsset = copyAsset(lockedAsset);
        clearAssetBinding(afterAsset, context, operateTime);

        // 先解标签侧：SQL WHERE 包含 bind_status='BOUND' AND asset_id=#{assetId} 的 CAS 守卫
        if (amsRfidTagMapper.unbindAmsRfidTagByTagId(lockedTag.getTagId(), lockedAsset.getAssetId(),
                context.getOperateLoginName(), operateTime) != 1)
        {
            throw new ServiceException("RFID标签当前绑定信息已变化，解绑失败");
        }
        // 再清资产侧：SQL WHERE 精确匹配 tag_id + tag_code + epc_code 三字段
        if (amsAssetMapper.clearAssetRfidBinding(lockedAsset.getAssetId(), lockedTag.getTagId(),
                lockedTag.getTagCode(), lockedTag.getEpcCode(), context.getOperateLoginName(), operateTime) != 1)
        {
            throw new ServiceException("资产台账当前RFID信息已变化，解绑失败");
        }
        // 解绑履历使用变更前的标签快照（beforeTag）构造，因为解绑后标签已无资产信息
        assetLifecycleService.recordLifecycle(beforeAsset, afterAsset,
                buildLifecycleLog(beforeTag, context, AssetLifecycleBusinessType.RFID_UNBIND,
                        buildUnbindSummary(beforeAsset, beforeTag), operateTime));
        return new RfidBindingResult(beforeAsset, copyAsset(afterAsset), beforeTag, copyTag(afterTag));
    }

    private void validateContext(RfidBindingContext context)
    {
        if (StringUtils.isNull(context))
        {
            throw new ServiceException("RFID绑定操作上下文不能为空");
        }
        context.setOperateUserName(StringUtils.trim(context.getOperateUserName()));
        context.setOperateLoginName(StringUtils.trim(context.getOperateLoginName()));
        context.setRemark(StringUtils.trim(context.getRemark()));
        if (StringUtils.isNull(context.getOperateUserId())
                || StringUtils.isEmpty(context.getOperateUserName())
                || StringUtils.isEmpty(context.getOperateLoginName()))
        {
            throw new ServiceException("操作人ID、名称和登录账号不能为空");
        }
        validateLength(context.getOperateUserName(), 100, "操作用户名称");
        validateLength(context.getOperateLoginName(), 64, "操作登录账号");
        validateLength(context.getRemark(), 500, "备注");
    }

    private void validateRequired(String value, int maxLength, String fieldName)
    {
        if (StringUtils.isEmpty(value))
        {
            throw new ServiceException(fieldName + "不能为空");
        }
        validateLength(value, maxLength, fieldName);
    }

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

    private void validateTagForBind(AmsRfidTag tag)
    {
        if (!StringUtils.equals(RfidTagStatus.NORMAL, tag.getTagStatus()))
        {
            throw new ServiceException("RFID标签已作废，不能绑定资产");
        }
        if (!StringUtils.equals(RfidBindStatus.UNBOUND, tag.getBindStatus()))
        {
            throw new ServiceException("RFID标签已绑定资产，不能重复绑定");
        }
        // 未绑定标签不应残留任何绑定元数据，避免在脏数据上继续建立新关系。
        if (StringUtils.isNotNull(tag.getAssetId())
                || StringUtils.isNotEmpty(tag.getAssetCode())
                || StringUtils.isNotEmpty(tag.getAssetName())
                || StringUtils.isNotNull(tag.getBindTime())
                || StringUtils.isNotNull(tag.getBindUserId())
                || StringUtils.isNotEmpty(tag.getBindUserName()))
        {
            throw new ServiceException("RFID标签绑定状态与绑定信息不一致，请先修正");
        }
    }

    private void validateAssetForBind(AmsAsset asset)
    {
        if (StringUtils.equals(AssetStatus.DISPOSED, asset.getAssetStatus()))
        {
            throw new ServiceException("已报废资产不能绑定RFID标签");
        }
        if (StringUtils.isNotNull(asset.getTagId())
                || StringUtils.isNotEmpty(asset.getTagCode())
                || StringUtils.isNotEmpty(asset.getEpcCode()))
        {
            throw new ServiceException("资产已绑定RFID标签或绑定信息不完整，不能重复绑定");
        }
    }

    private void validateTagForUnbind(AmsRfidTag tag)
    {
        if (!StringUtils.equals(RfidBindStatus.BOUND, tag.getBindStatus())
                || StringUtils.isNull(tag.getAssetId()))
        {
            throw new ServiceException("RFID标签未绑定资产，无需解绑");
        }
        if (StringUtils.isEmpty(tag.getTagCode()) || StringUtils.isEmpty(tag.getEpcCode())
                || StringUtils.isEmpty(tag.getAssetCode()) || StringUtils.isEmpty(tag.getAssetName())
                || StringUtils.isNull(tag.getBindTime()) || StringUtils.isNull(tag.getBindUserId())
                || StringUtils.isEmpty(tag.getBindUserName()))
        {
            throw new ServiceException("RFID标签当前绑定信息不完整，无法解绑");
        }
    }

    private void validateBindingMatches(AmsAsset asset, AmsRfidTag tag)
    {
        if (!tag.getAssetId().equals(asset.getAssetId())
                || !StringUtils.equals(tag.getAssetCode(), asset.getAssetCode())
                || !StringUtils.equals(tag.getAssetName(), asset.getAssetName())
                || !tag.getTagId().equals(asset.getTagId())
                || !StringUtils.equals(tag.getTagCode(), asset.getTagCode())
                || !StringUtils.equals(tag.getEpcCode(), asset.getEpcCode()))
        {
            throw new ServiceException("资产台账当前RFID信息与标签绑定信息不一致，无法解绑");
        }
    }

    private void fillBoundTag(AmsRfidTag tag, AmsAsset asset, RfidBindingContext context, Date operateTime)
    {
        tag.setBindStatus(RfidBindStatus.BOUND);
        tag.setAssetId(asset.getAssetId());
        tag.setAssetCode(asset.getAssetCode());
        tag.setAssetName(asset.getAssetName());
        tag.setBindTime(operateTime);
        tag.setBindUserId(context.getOperateUserId());
        tag.setBindUserName(context.getOperateUserName());
        tag.setUpdateBy(context.getOperateLoginName());
        tag.setUpdateTime(operateTime);
    }

    private void fillBoundAsset(AmsAsset asset, AmsRfidTag tag, RfidBindingContext context, Date operateTime)
    {
        asset.setTagId(tag.getTagId());
        asset.setTagCode(tag.getTagCode());
        asset.setEpcCode(tag.getEpcCode());
        asset.setUpdateBy(context.getOperateLoginName());
        asset.setUpdateTime(operateTime);
    }

    private void clearTagBinding(AmsRfidTag tag, RfidBindingContext context, Date operateTime)
    {
        tag.setBindStatus(RfidBindStatus.UNBOUND);
        tag.setAssetId(null);
        tag.setAssetCode(null);
        tag.setAssetName(null);
        tag.setBindTime(null);
        tag.setBindUserId(null);
        tag.setBindUserName(null);
        tag.setUpdateBy(context.getOperateLoginName());
        tag.setUpdateTime(operateTime);
    }

    private void clearAssetBinding(AmsAsset asset, RfidBindingContext context, Date operateTime)
    {
        asset.setTagId(null);
        asset.setTagCode(null);
        asset.setEpcCode(null);
        asset.setUpdateBy(context.getOperateLoginName());
        asset.setUpdateTime(operateTime);
    }

    private AmsAssetLifecycleLog buildLifecycleLog(AmsRfidTag tag, RfidBindingContext context,
            String businessType, String summary, Date operateTime)
    {
        AmsAssetLifecycleLog lifecycleLog = new AmsAssetLifecycleLog();
        lifecycleLog.setBusinessType(businessType);
        lifecycleLog.setSourceOrderId(tag.getTagId());
        lifecycleLog.setSourceOrderNo(tag.getTagCode());
        lifecycleLog.setOperateUserId(context.getOperateUserId());
        lifecycleLog.setOperateUserName(context.getOperateUserName());
        lifecycleLog.setOperateTime(operateTime);
        lifecycleLog.setChangeSummary(summary);
        lifecycleLog.setCreateBy(context.getOperateLoginName());
        lifecycleLog.setRemark(context.getRemark());
        return lifecycleLog;
    }

    private String buildBindSummary(AmsAsset asset, AmsRfidTag tag)
    {
        return StringUtils.format("RFID标签绑定：标签编码【{}】，EPC【{}】绑定资产【{}】",
                tag.getTagCode(), tag.getEpcCode(), asset.getAssetCode());
    }

    private String buildUnbindSummary(AmsAsset asset, AmsRfidTag tag)
    {
        return StringUtils.format("RFID标签解绑：标签编码【{}】，EPC【{}】从资产【{}】解绑",
                tag.getTagCode(), tag.getEpcCode(), asset.getAssetCode());
    }

    /** 
     * 浅拷贝资产快照，当前字段均为不可变类型或基本类型包装类，满足履历快照需求。
     * <p>注意：当前基于 BeanUtils.copyProperties 实现浅拷贝。若实体类后续引入可变对象（如 Date、自定义引用类型等），
     * 浅拷贝只复制引用，如果未来修改快照内部的可变属性可能会污染锁定状态下的原实体对象。若未来有深层修改需求，必须改为深拷贝或进行防御性拷贝。</p>
     */
    private AmsAsset copyAsset(AmsAsset source)
    {
        AmsAsset target = new AmsAsset();
        BeanUtils.copyProperties(source, target);
        return target;
    }

    /** 
     * 浅拷贝标签快照，当前字段均为不可变类型或基本类型包装类，满足履历快照需求。
     * <p>注意：当前基于 BeanUtils.copyProperties 实现浅拷贝。若实体类后续引入可变对象（如 Date、自定义引用类型等），
     * 浅拷贝只复制引用，如果未来修改快照内部的可变属性可能会污染锁定状态下的原实体对象。若未来有深层修改需求，必须改为深拷贝或进行防御性拷贝。</p>
     */
    private AmsRfidTag copyTag(AmsRfidTag source)
    {
        AmsRfidTag target = new AmsRfidTag();
        BeanUtils.copyProperties(source, target);
        return target;
    }
}
