package com.nbport.zgwl.contractlaunch.service;

import com.alibaba.fastjson2.JSON;
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.nbport.zgwl.common.PageResult;
import com.nbport.zgwl.contractlaunch.dto.ContractLaunchApproveDto;
import com.nbport.zgwl.contractlaunch.dto.ContractLaunchArchiveDto;
import com.nbport.zgwl.contractlaunch.dto.ContractLaunchDto;
import com.nbport.zgwl.contractlaunch.dto.ContractLaunchFileDto;
import com.nbport.zgwl.contractlaunch.dto.ContractLaunchLogDto;
import com.nbport.zgwl.contractlaunch.dto.ContractLaunchPartyDto;
import com.nbport.zgwl.contractlaunch.dto.ContractLaunchPlanDto;
import com.nbport.zgwl.contractlaunch.dto.ContractLaunchQueryDto;
import com.nbport.zgwl.contractlaunch.dto.ContractLaunchSealDto;
import com.nbport.zgwl.contractlaunch.service.ContractLaunchLegalFileBridgeService;
import com.nbport.zgwl.contractlaunch.mapper.ContractLaunchMapper;
import com.nbport.zgwl.contractlaunch.utils.ContractLaunchCodeUtils;
import com.nbport.zgwl.contractlaunch.utils.ContractLaunchConstants;
import com.nbport.zgwl.contractlaunch.utils.JsonUtils;
import com.nbport.zgwl.contractpush.dto.PushPayloadDTO;
import com.nbport.zgwl.contractpush.service.DataCollectionPushService;
import com.nbport.zgwl.contractpush.service.SettlementPushService;
import com.nbport.zgwl.seal.service.SealTokenService;
import com.nbport.zgwl.seal.config.SealConfig;
import com.nbport.zgwl.seal.utils.SealHttpClient;
import com.alibaba.fastjson2.JSON;
import com.alibaba.fastjson2.JSONObject;
import com.nbport.zgwl.service.LocalFileService;
import com.nbport.zgwl.entity.SysUserEntity;
import com.nbport.zgwl.exception.ServiceException;
import com.nbport.zgwl.utils.AdminSecurityManage;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;

import java.time.LocalDateTime;
import java.util.ArrayList;
import java.util.Collections;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import java.util.stream.Collectors;

/**
 * 合同发起主服务
 *
 * 这里尽量把核心流程都写在一个 Service 里，
 * 方便你后续自己看和改，不额外再拆很多 handler。
 */
@Slf4j
@Service
@RequiredArgsConstructor
public class ContractLaunchServiceImpl implements ContractLaunchService {

    private final ContractLaunchMapper contractLaunchMapper;
    private final AdminSecurityManage adminSecurityManage;
    private final SettlementPushService settlementPushService;
    private final DataCollectionPushService dataCollectionPushService;
    private final ContractLaunchLegalFileBridgeService legalFileBridgeService;
    private final LocalFileService localFileService;
    private final SealTokenService sealTokenService;
    private final SealConfig sealConfig;

    /** 分页查询，附带合同方/文件/履行计划（列表页不查日志） */
    @Override
    public PageResult<ContractLaunchDto> queryPage(ContractLaunchQueryDto queryDto) {
        Page<ContractLaunchDto> page = new Page<>(queryDto.getPageNo(), queryDto.getPageSize());
        Page<ContractLaunchDto> resultPage = (Page<ContractLaunchDto>) contractLaunchMapper.selectContractPage(page, queryDto);
        PageResult<ContractLaunchDto> pageResult = new PageResult<>(resultPage);
        fillChildren(pageResult.getData(), false);
        return pageResult;
    }

    /** 查询详情，含日志 */
    @Override
    public ContractLaunchDto getDetail(Long id) {
        ContractLaunchDto contract = contractLaunchMapper.selectContractById(id);
        if (contract == null) {
            throw new ServiceException("合同不存在");
        }
        fillChildren(Collections.singletonList(contract), true);
        return contract;
    }

    /**
     * 创建合同草稿。
     *
     * 完整流程：
     *   1. 获取当前登录用户信息
     *   2. 数据清洗（normalizeContract：去空格、补默认值）
     *   3. 数据校验（validateDraft：必填项检查）
     *   4. 生成主键ID 和 contractSysId（CLS-时间戳）、contractCode（类型编码-日期-随机数）
     *   5. 填充创建人/创建时间/公司/部门/状态 draft
     *   6. 保存主表 + 子表（合同方/文件/履行计划）
     *   7. 记录操作日志
     *
     * 注意：此时仅在本系统保存，未推送到法务系统。
     */
    @Override
    @Transactional(rollbackFor = Exception.class)
    public ContractLaunchDto createDraft(ContractLaunchDto contract) {
        SysUserEntity currentUser = adminSecurityManage.getUser();
        normalizeContract(contract);
        validateDraft(contract);
        return persistDraft(contract, currentUser, false, ContractLaunchConstants.ACTION_SAVE, "保存草稿", "合同草稿创建成功");
    }

    /**
     * 编辑合同草稿。
     *
     * 仅 draft（草稿）或 rejected（已驳回）可编辑。
     * 采用替换式更新：先删子表（合同方/文件/履行计划）旧数据，再以当前数据重新插入。
     * 保留以下字段不变：contractSysId、contractCode、lawContractCode、lawContractId、
     * 状态、审批状态、发起人、公司、orgId、创建人/创建时间。
     */
    @Override
    @Transactional(rollbackFor = Exception.class)
    public ContractLaunchDto updateDraft(ContractLaunchDto contract) {
        if (contract.getId() == null) {
            throw new ServiceException("修改时 id 不能为空");
        }
        ContractLaunchDto dbContract = getDetail(contract.getId());
        if (!canEdit(dbContract.getStatus())) {
            throw new ServiceException("当前状态不允许编辑");
        }

        SysUserEntity currentUser = adminSecurityManage.getUser();
        normalizeContract(contract);
        validateDraft(contract);
        contract.setContractSysId(dbContract.getContractSysId());
        contract.setContractCode(dbContract.getContractCode());
        contract.setLawContractCode(dbContract.getLawContractCode());
        contract.setLawContractId(dbContract.getLawContractId());
        contract.setStatus(dbContract.getStatus());
        contract.setApproveStatus(dbContract.getApproveStatus());
        contract.setInitiator(dbContract.getInitiator());
        contract.setInitiatorDept(dbContract.getInitiatorDept());
        contract.setCompanyName(dbContract.getCompanyName());
        contract.setOrgId(dbContract.getOrgId());
        contract.setCreatorPhone(dbContract.getCreatorPhone());
        contract.setCreateBy(dbContract.getCreateBy());
        contract.setCreateTime(dbContract.getCreateTime());
        contract.setUpdateBy(currentUser.getUsername());
        contract.setUpdateTime(LocalDateTime.now());
        contract.setIsDeleted(ContractLaunchConstants.IS_DELETED_NO);
        contractLaunchMapper.updateContract(contract);
        replaceChildren(contract, currentUser.getUsername());
        insertLog(contract.getId(), ContractLaunchConstants.ACTION_SAVE, "编辑草稿", ContractLaunchConstants.ACTION_STATUS_SUCCESS, "合同草稿修改成功", null, currentUser);
        return getDetail(contract.getId());
    }

    /**
     * 复制合同并生成新草稿。
     *
     * 复制策略：
     *   1. 复制主信息、合同方、正文/签订依据/其他附件、履行计划
     *   2. 重置系统主键、合同系统号、合同编号、流程状态、审批信息、签署信息、归档信息
     *   3. 不复制日志，也不复制 signed/archive 分类文件
     */
    @Override
    @Transactional(rollbackFor = Exception.class)
    public ContractLaunchDto copyDraft(Long id) {
        ContractLaunchDto source = getDetail(id);
        SysUserEntity currentUser = adminSecurityManage.getUser();
        ContractLaunchDto copiedContract = buildCopiedDraft(source);
        normalizeContract(copiedContract);
        validateDraft(copiedContract);
        return persistDraft(copiedContract, currentUser, true, ContractLaunchConstants.ACTION_COPY, "复制合同", "合同复制成功，已生成新草稿");
    }

    @Override
    @Transactional(rollbackFor = Exception.class)
    public ContractLaunchDto createChangeDraft(Long id) {
        ContractLaunchDto source = getDetail(id);
        SysUserEntity currentUser = adminSecurityManage.getUser();
        ContractLaunchDto changedContract = buildCopiedDraft(source);
        changedContract.setMainContractCode(source.getContractCode());
        changedContract.setContractName(buildChangedContractName(source.getContractName()));
        changedContract.setIsAddit(1);
        normalizeContract(changedContract);
        validateDraft(changedContract);
        return persistDraft(changedContract, currentUser, true, ContractLaunchConstants.ACTION_CHANGE, "发起变更合同", "已基于原合同生成变更草稿");
    }

    /**
     * 提交法务审批。
     *
     * 状态流转：draft/rejected → reviewing（待审批）
     *
     * 主要做两件事：
     *   1. 更新合同状态为 reviewing
     *   2. 构建结算平台 pushContract 接口的完整报文（含合同方、履行计划、附件等）
     *   3. 记录提交日志（日志中保存了完整的推送报文，方便排查）
     *
     * TODO（真实联调时打开）：
     *   PushPayloadDTO requestDTO = new PushPayloadDTO();
     *   requestDTO.setPayload(settlementPayload);
     *   settlementPushService.pushContract(requestDTO);
     *
     * 法务审批完成后会通过 pushUrl 回调本系统的 /callback/law/approve 接口。
     */
    @Override
    @Transactional(rollbackFor = Exception.class)
    public ContractLaunchDto submitToLaw(Long id) {

        ContractLaunchDto contract = getDetail(id);
        if (!canSubmit(contract.getStatus())) {
            throw new ServiceException("当前状态不允许提交法务");
        }
        SysUserEntity currentUser = adminSecurityManage.getUser();
        contract.setStatus(ContractLaunchConstants.STATUS_REVIEWING);
        contract.setApproveStatus(ContractLaunchConstants.APPROVE_STATUS_PENDING);
        contract.setApproveMessage("已提交法务，等待审批");
        contract.setUpdateBy(currentUser.getUsername());
        contract.setUpdateTime(LocalDateTime.now());
        contractLaunchMapper.updateContract(contract);

        Map<String, Object> settlementPayload = buildSettlementPushPayload(contract);
        log.info("结算参数 JSON：{}", JsonUtils.toJson(settlementPayload));
        insertLog(id, ContractLaunchConstants.ACTION_SUBMIT, "提交法务", ContractLaunchConstants.ACTION_STATUS_PENDING, "合同已提交法务接口，当前先保留 mock 流程", settlementPayload, currentUser);

        // TODO: 法务接口当前不可用，真实联调时打开下面调用即可
        // PushPayloadDTO requestDTO = new PushPayloadDTO();
        // requestDTO.setPayload(settlementPayload);
        // Map<String, Object> response = settlementPushService.pushContract(requestDTO);

        return getDetail(id);
    }

    /**
     * 【测试按钮】模拟法务审批通过。
     *
     * 状态流转：reviewing → pending_sign（待签署）
     *
     * 模拟法务系统返回 lawContractCode 和 lawContractId，供后续签署流程使用。
     * 如果 approveDto 中传入了值则使用传入值，否则自动生成模拟值：
     *   - lawContractCode = "LAW-" + 合同类型编码-日期-随机数
     *   - lawContractId = "LAWID-" + 合同ID
     *
     * 真实场景下此逻辑由法务回调接口 handleLawApproveCallback 替代。
     */
    @Override
    @Transactional(rollbackFor = Exception.class)
    public ContractLaunchDto mockApprove(Long id, ContractLaunchApproveDto approveDto) {
        ContractLaunchDto contract = getDetail(id);
        if (!ContractLaunchConstants.STATUS_REVIEWING.equals(contract.getStatus())) {
            throw new ServiceException("仅审批中的合同可模拟通过");
        }
        SysUserEntity currentUser = adminSecurityManage.getUser();
        contract.setStatus(ContractLaunchConstants.STATUS_PENDING_SIGN);
        contract.setApproveStatus(ContractLaunchConstants.APPROVE_STATUS_APPROVED);
        contract.setApproveMessage(approveDto == null || approveDto.getApproveMessage() == null || approveDto.getApproveMessage().isBlank()
                ? "法务审批通过（测试按钮模拟）"
                : approveDto.getApproveMessage());
        contract.setLawContractCode(approveDto == null || approveDto.getLawContractCode() == null || approveDto.getLawContractCode().isBlank()
                ? "LAW-" + ContractLaunchCodeUtils.buildContractCode(contract.getContractTypeCode())
                : approveDto.getLawContractCode());
        contract.setLawContractId(approveDto == null || approveDto.getLawContractId() == null || approveDto.getLawContractId().isBlank()
                ? "LAWID-" + id
                : approveDto.getLawContractId());
        contract.setUpdateBy(currentUser.getUsername());
        contract.setUpdateTime(LocalDateTime.now());
        contractLaunchMapper.updateContract(contract);
        insertLog(id, ContractLaunchConstants.ACTION_APPROVE, "模拟审批通过", ContractLaunchConstants.ACTION_STATUS_SUCCESS, contract.getApproveMessage(), approveDto, currentUser);
        return getDetail(id);
    }

    /**
     * 【测试按钮】模拟法务审批驳回。
     *
     * 状态流转：reviewing → rejected（已驳回）
     * 驳回后合同回到草稿状态，可以再次编辑后重新提交。
     */
    @Override
    @Transactional(rollbackFor = Exception.class)
    public ContractLaunchDto mockReject(Long id, ContractLaunchApproveDto approveDto) {
        ContractLaunchDto contract = getDetail(id);
        if (!ContractLaunchConstants.STATUS_REVIEWING.equals(contract.getStatus())) {
            throw new ServiceException("仅审批中的合同可模拟驳回");
        }
        SysUserEntity currentUser = adminSecurityManage.getUser();
        contract.setStatus(ContractLaunchConstants.STATUS_REJECTED);
        contract.setApproveStatus(ContractLaunchConstants.APPROVE_STATUS_REJECTED);
        contract.setApproveMessage(approveDto == null || approveDto.getApproveMessage() == null || approveDto.getApproveMessage().isBlank()
                ? "法务审批驳回（测试按钮模拟）"
                : approveDto.getApproveMessage());
        contract.setUpdateBy(currentUser.getUsername());
        contract.setUpdateTime(LocalDateTime.now());
        contractLaunchMapper.updateContract(contract);
        insertLog(id, ContractLaunchConstants.ACTION_REJECT, "模拟审批驳回", ContractLaunchConstants.ACTION_STATUS_SUCCESS, contract.getApproveMessage(), approveDto, currentUser);
        return getDetail(id);
    }

    /**
     * 接收法务审批回调。
     *
     * 法务系统审批完成后，通过 pushUrl POST 审批结果到本接口。
     *
     * 处理逻辑：
     *   1. 从回调报文中提取 contractSysId 定位到本系统合同
     *   2. 判断审批结果（通过/驳回）
     *      - 通过：status → pending_sign（待签署），记录法务返回的合同编号和合同ID
     *      - 驳回：status → rejected（已驳回），记录驳回原因
     *   3. 记录操作日志
     *
     * 兼容设计：
     *   - 使用 getFirstText 按多个 key 依次尝试提取字段（如 approveStatus/status/result）
     *   - 审批通过状态兼容：1/true/success/approved/pass/agree/审批通过
     *   - 合同编号兼容：lawContractCode/contractCode/htCode/code
     *   - 合同ID兼容：lawContractId/contractId/id/businessId
     */
    @Override
    @Transactional(rollbackFor = Exception.class)
    public ContractLaunchDto handleLawApproveCallback(Map<String, Object> callbackPayload) {
        ContractLaunchDto contract = getContractByLawCallback(callbackPayload);
        SysUserEntity currentUser = adminSecurityManage.getUser();

        boolean approved = isLawCallbackApproved(callbackPayload);
        contract.setApproveStatus(approved
                ? ContractLaunchConstants.APPROVE_STATUS_APPROVED
                : ContractLaunchConstants.APPROVE_STATUS_REJECTED);
        contract.setStatus(approved
                ? ContractLaunchConstants.STATUS_PENDING_SIGN
                : ContractLaunchConstants.STATUS_REJECTED);
        contract.setApproveMessage(getFirstText(callbackPayload,
                "approveMessage", "message", "msg", "opinion", "remark"));

        String lawContractCode = getFirstText(callbackPayload,
                "lawContractCode", "contractCode", "htCode", "code");
        String lawContractId = getFirstText(callbackPayload,
                "lawContractId", "contractId", "id", "businessId");
        if (lawContractCode != null && !lawContractCode.isBlank()) {
            contract.setLawContractCode(lawContractCode);
        }
        if (lawContractId != null && !lawContractId.isBlank()) {
            contract.setLawContractId(lawContractId);
        }

        contract.setUpdateBy(currentUser.getUsername());
        contract.setUpdateTime(LocalDateTime.now());
        contractLaunchMapper.updateContract(contract);
        insertLog(
                contract.getId(),
                approved ? ContractLaunchConstants.ACTION_APPROVE : ContractLaunchConstants.ACTION_REJECT,
                approved ? "法务审批回调通过" : "法务审批回调驳回",
                ContractLaunchConstants.ACTION_STATUS_SUCCESS,
                contract.getApproveMessage(),
                callbackPayload,
                currentUser
        );
        return getDetail(contract.getId());
    }

    /**
     * 回写签章信息（签署发起时调用）。
     *
     * 浙江CA安全签平台创建签署合同后，回调此方法回写 sealContractId。
     * 状态流转：pending_sign → signing（签署中）
     *
     * sealContractId 由浙江CA安全签平台生成，后续通过此 ID 查询签署进度。
     */
    @Override
    @Transactional(rollbackFor = Exception.class)
    public ContractLaunchDto saveSealInfo(Long id, ContractLaunchSealDto sealDto) {
        ContractLaunchDto contract = getDetail(id);
        SysUserEntity currentUser = adminSecurityManage.getUser();
        contract.setSealContractId(sealDto.getSealContractId());
        contract.setSealContractStatus(sealDto.getSealContractStatus());
        contract.setStatus(ContractLaunchConstants.STATUS_SIGNING);
        contract.setUpdateBy(currentUser.getUsername());
        contract.setUpdateTime(LocalDateTime.now());
        contractLaunchMapper.updateContract(contract);
        insertLog(id, ContractLaunchConstants.ACTION_SEAL_START, "开始签署", ContractLaunchConstants.ACTION_STATUS_SUCCESS, "签章平台合同已创建", sealDto, currentUser);
        return getDetail(id);
    }

    /**
     * 签署完成回调。
     *
     * 所有签署方签署完成后（或在浙江CA签署完成、或线下签署后手动上传），
     * 回写签署完成的合同文件信息。
     *
     * 状态流转：signing → pending_collect（待采集）
     *
     * 执行步骤：
     *   1. 回写 signedFileName/signedFilePath/signedFileType/signedFileSize
     *   2. 如果有线下签署原因（signedOfflineReason），一并保存
     *   3. 将签署文件保存到 file 子表（fileCategory = "signed"）
     *   4. 记录操作日志
     */
    @Override
    @Transactional(rollbackFor = Exception.class)
    public ContractLaunchDto markSealCompleted(Long id, ContractLaunchSealDto sealDto) {
        ContractLaunchDto contract = getDetail(id);
        SysUserEntity currentUser = adminSecurityManage.getUser();
        LocalDateTime completedTime = LocalDateTime.now();
        contract.setSealContractId(sealDto.getSealContractId() == null || sealDto.getSealContractId().isBlank() ? contract.getSealContractId() : sealDto.getSealContractId());
        contract.setSealContractStatus(sealDto.getSealContractStatus());
        contract.setSignedFileName(sealDto.getSignedFileName());
        contract.setSignedFilePath(resolveSignedFilePath(sealDto));
        contract.setSignedFileUrl(sealDto.getSignedFileUrl() != null && !sealDto.getSignedFileUrl().isBlank()
                ? sealDto.getSignedFileUrl()
                : (isExternalUrl(sealDto.getSignedFilePath()) ? sealDto.getSignedFilePath() : null));
        contract.setSignedFileType(sealDto.getSignedFileType());
        contract.setSignedFileSize(sealDto.getSignedFileSize());
        contract.setSignedOfflineReason(sealDto.getSignedOfflineReason());
        contract.setSignDate(firstNonNull(sealDto.getSignDate(), contract.getSignDate(), completedTime));
        contract.setSealDate(firstNonNull(sealDto.getSealDate(), contract.getSealDate(), contract.getSignDate()));
        contract.setActiveDate(firstNonNull(sealDto.getActiveDate(), contract.getActiveDate()));
        validateSignCompletion(contract);
        contract.setStatus(ContractLaunchConstants.STATUS_PENDING_COLLECT);
        contract.setUpdateBy(currentUser.getUsername());
        contract.setUpdateTime(completedTime);
        contractLaunchMapper.updateContract(contract);
        saveSignedFile(id, sealDto, currentUser.getUsername());
        insertLog(id, ContractLaunchConstants.ACTION_SEAL_FINISH, "签署完成", ContractLaunchConstants.ACTION_STATUS_SUCCESS, "合同签署已完成，待采集", sealDto, currentUser);
        return getDetail(id);
    }

    /**
     * 【测试按钮】模拟合同采集。
     *
     * 合同签署完成后，将合同最新信息（含用印信息和签订信息）推送到法务系统。
     *
     * 状态流转：pending_collect → pending_archive（待归档）
     *
     * 构建数据采集 V3.0 collectContract 接口的报文，包含：
     *   - 合同基本字段（c_name, c_code, c_type 等）
     *   - 用印信息 c_seal（签章类型、签章日期等）
     *   - 签订信息 c_sign（签署日期、生效日期、签署附件等）
     *   - 履行计划 c_lxPlan
     *   - 附件（正文 c_zw / 其他 c_attachs / 签署文件 c_signAttachs）
     *
     * TODO（真实联调时打开）：
     *   PushPayloadDTO requestDTO = new PushPayloadDTO();
     *   requestDTO.setPayload(payload);
     *   dataCollectionPushService.collectContract(requestDTO);
     */
    @Override
    @Transactional(rollbackFor = Exception.class)
    public ContractLaunchDto mockCollect(Long id) {
        ContractLaunchDto contract = getDetail(id);
        if (!ContractLaunchConstants.STATUS_PENDING_COLLECT.equals(contract.getStatus())) {
            throw new ServiceException("当前状态不允许执行采集");
        }
        validateCollectionReady(contract);
        SysUserEntity currentUser = adminSecurityManage.getUser();
        Map<String, Object> payload = buildDataCollectionContractPayload(contract);
        insertLog(id, ContractLaunchConstants.ACTION_COLLECT, "模拟合同采集", ContractLaunchConstants.ACTION_STATUS_PENDING, "合同采集接口当前先注释，使用测试按钮流转", payload, currentUser);
        log.info("测试采集提交参数：{}", JsonUtils.toJson(payload));
        // TODO: 法务接口当前不可用，真实联调时打开下面调用即可
        // PushPayloadDTO requestDTO = new PushPayloadDTO();
        // requestDTO.setPayload(payload);
        // dataCollectionPushService.collectContract(requestDTO);

        contract.setStatus(ContractLaunchConstants.STATUS_PENDING_ARCHIVE);
        contract.setUpdateBy(currentUser.getUsername());
        contract.setUpdateTime(LocalDateTime.now());
        contractLaunchMapper.updateContract(contract);
        return getDetail(id);
    }

    /**
     * 合同归档。
     *
     * 合同全生命周期最后一步。将归档信息（保管人、保管单位、归档附件）保存到合同，
     * 同时推送到法务系统存档。
     *
     * 状态流转：pending_archive → completed（已完成）
     *
     * 执行步骤：
     *   1. 填写归档日期、保管人信息
     *   2. 保存归档附件到 file 子表（fileCategory = "archive"）
     *   3. 构建归档报文 pushArchive 接口的 payload
     *   4. 记录操作日志
     *
     * TODO（真实联调时打开）：
     *   settlementPushService.pushArchive(requestDTO)
     *   dataCollectionPushService.collectContractArchive(requestDTO)
     */
    @Override
    @Transactional(rollbackFor = Exception.class)
    public ContractLaunchDto archive(Long id, ContractLaunchArchiveDto archiveDto) {
        ContractLaunchDto contract = getDetail(id);
        if (!ContractLaunchConstants.STATUS_PENDING_ARCHIVE.equals(contract.getStatus())) {
            throw new ServiceException("当前状态不允许归档");
        }
        SysUserEntity currentUser = adminSecurityManage.getUser();
        contract.setArchiveDate(LocalDateTime.now());
        contract.setArchiveKeeper(archiveDto == null ? null : archiveDto.getArchiveKeeper());
        contract.setArchiveKeeperUnit(archiveDto == null ? null : archiveDto.getArchiveKeeperUnit());
        contract.setArchiveKeeperUnitCode(archiveDto == null ? null : archiveDto.getArchiveKeeperUnitCode());
        contract.setStatus(ContractLaunchConstants.STATUS_COMPLETED);
        contract.setUpdateBy(currentUser.getUsername());
        contract.setUpdateTime(LocalDateTime.now());
        contractLaunchMapper.updateContract(contract);

        if (archiveDto != null && archiveDto.getArchiveFiles() != null && !archiveDto.getArchiveFiles().isEmpty()) {
            for (ContractLaunchFileDto fileDto : archiveDto.getArchiveFiles()) {
                fileDto.setId(contractLaunchMapper.selectFileSeqNextVal());
                fileDto.setContractId(id);
                fileDto.setFileCategory(ContractLaunchConstants.FILE_CATEGORY_ARCHIVE);
            }
            contractLaunchMapper.batchInsertFile(archiveDto.getArchiveFiles(), currentUser.getUsername(), currentUser.getUsername());
        }

        Map<String, Object> payload = buildArchivePayload(contract, archiveDto);
        log.info("合同归档参数：{}", JsonUtils.toJson(payload));
        insertLog(id, ContractLaunchConstants.ACTION_ARCHIVE, "合同归档", ContractLaunchConstants.ACTION_STATUS_PENDING, "合同归档接口当前先注释，使用测试按钮流转", payload, currentUser);

        // TODO: 法务接口当前不可用，真实联调时打开下面调用即可
        // PushPayloadDTO requestDTO = new PushPayloadDTO();
        // requestDTO.setPayload(payload);
        // settlementPushService.pushArchive(requestDTO);
        // dataCollectionPushService.collectContractArchive(requestDTO);

        return getDetail(id);
    }

    @Override
    @Transactional(rollbackFor = Exception.class)
    public ContractLaunchDto terminate(Long id, String terminateReason) {
        ContractLaunchDto contract = getDetail(id);
        if (ContractLaunchConstants.STATUS_COMPLETED.equals(contract.getStatus())) {
            throw new ServiceException("已完成合同不允许终止");
        }
        if (ContractLaunchConstants.STATUS_TERMINATED.equals(contract.getStatus())) {
            throw new ServiceException("合同已终止，请勿重复操作");
        }
        SysUserEntity currentUser = adminSecurityManage.getUser();
        contract.setStatus(ContractLaunchConstants.STATUS_TERMINATED);
        contract.setIsTerminated("Y");
        contract.setApproveMessage(terminateReason == null || terminateReason.isBlank() ? "合同已终止" : terminateReason);
        contract.setUpdateBy(currentUser.getUsername());
        contract.setUpdateTime(LocalDateTime.now());
        contractLaunchMapper.updateContract(contract);
        insertLog(id,
                ContractLaunchConstants.ACTION_TERMINATE,
                "终止合同",
                ContractLaunchConstants.ACTION_STATUS_SUCCESS,
                contract.getApproveMessage(),
                null,
                currentUser);
        return getDetail(id);
    }

    /**
     * 删除合同草稿（逻辑删除）。
     *
     * 仅 draft（草稿）或 rejected（已驳回）状态允许删除。
     * 设置 isDeleted = 'Y'，不物理删除数据。
     * 已提交法务审批或进行中的合同不允许删除。
     */
    @Override
    @Transactional(rollbackFor = Exception.class)
    public void deleteDraft(Long id) {
        ContractLaunchDto contract = getDetail(id);
        if (!canDelete(contract.getStatus())) {
            throw new ServiceException("仅草稿或驳回状态允许删除");
        }
        SysUserEntity currentUser = adminSecurityManage.getUser();
        contractLaunchMapper.deleteContract(id, currentUser.getUsername());
        insertLog(id, ContractLaunchConstants.ACTION_DELETE, "删除合同", ContractLaunchConstants.ACTION_STATUS_SUCCESS, "合同已逻辑删除", null, currentUser);
    }

    private ContractLaunchDto persistDraft(ContractLaunchDto contract,
                                           SysUserEntity currentUser,
                                           boolean forceGenerateContractCode,
                                           String actionType,
                                           String actionName,
                                           String actionContent) {
        LocalDateTime now = LocalDateTime.now();
        contract.setId(contractLaunchMapper.selectMainSeqNextVal());
        contract.setContractSysId(ContractLaunchCodeUtils.buildContractSysId());
        if (forceGenerateContractCode || contract.getContractCode() == null || contract.getContractCode().isBlank()) {
            contract.setContractCode(ContractLaunchCodeUtils.buildContractCode(contract.getContractTypeCode()));
        }
        contract.setContractSource(contract.getContractSource() == null || contract.getContractSource().isBlank() ? "ECP" : contract.getContractSource());
        contract.setStatus(ContractLaunchConstants.STATUS_DRAFT);
        contract.setApproveStatus(ContractLaunchConstants.APPROVE_STATUS_PENDING);
        contract.setApproveMessage(null);
        contract.setInitiator(currentUser.getUsername());
        contract.setInitiatorDept(currentUser.getSysOrgEntity() == null ? "" : currentUser.getSysOrgEntity().getOrgName());
        contract.setCompanyName(currentUser.getSysOrgEntity() == null ? "" : currentUser.getSysOrgEntity().getOrgName());
        contract.setOrgId(currentUser.getSysOrgEntity() == null ? "" : currentUser.getSysOrgEntity().getId());
        contract.setCreatorPhone(currentUser.getMobilePhone());
        contract.setCreateBy(currentUser.getUsername());
        contract.setCreateTime(now);
        contract.setUpdateBy(currentUser.getUsername());
        contract.setUpdateTime(now);
        contract.setIsDeleted(ContractLaunchConstants.IS_DELETED_NO);
        contract.setQueryScope("personal");
        contractLaunchMapper.insertContract(contract);
        saveChildren(contract, currentUser.getUsername());
        insertLog(contract.getId(), actionType, actionName, ContractLaunchConstants.ACTION_STATUS_SUCCESS, actionContent, null, currentUser);
        return getDetail(contract.getId());
    }

    private ContractLaunchDto buildCopiedDraft(ContractLaunchDto source) {
        ContractLaunchDto target = new ContractLaunchDto();
        target.setContractName(buildCopiedContractName(source.getContractName()));
        target.setContractType(source.getContractType());
        target.setContractTypeCode(source.getContractTypeCode());
        target.setContractSource(source.getContractSource());
        target.setCreationMode(source.getCreationMode());
        target.setTemplateId(source.getTemplateId());
        target.setTemplateCode(source.getTemplateCode());
        target.setTemplateName(source.getTemplateName());
        target.setContractAmount(source.getContractAmount());
        target.setCurrencyName(source.getCurrencyName());
        target.setContractPeriodType(source.getContractPeriodType());
        target.setPeriodExplain(source.getPeriodExplain());
        target.setEffectiveStart(source.getEffectiveStart());
        target.setEffectiveEnd(source.getEffectiveEnd());
        target.setPaymentMethod(source.getPaymentMethod());
        target.setPrimaryContent(source.getPrimaryContent());
        target.setAmountExplain(source.getAmountExplain());
        target.setValuationMode(source.getValuationMode());
        target.setNeedAmendments(source.getNeedAmendments());
        target.setEstimatedAmount(source.getEstimatedAmount());
        target.setSealType(source.getSealType());
        target.setSealQuantity(source.getSealQuantity());
        target.setTextSource(source.getTextSource());
        target.setMandateType(source.getMandateType());
        target.setCargoType(source.getCargoType());
        target.setAssistDepartmentName(source.getAssistDepartmentName());
        target.setExecutorAccount(source.getExecutorAccount());
        target.setSelfCode(source.getSelfCode());
        target.setMainContractCode(source.getMainContractCode());
        target.setSigningSubjectCode(source.getSigningSubjectCode());
        target.setDateType(source.getDateType());
        target.setIsAddit(source.getIsAddit());
        target.setIsElectron(source.getIsElectron());
        target.setPaymentDirection(source.getPaymentDirection());
        target.setPushUrl(source.getPushUrl());
        target.setIsMajorContract(source.getIsMajorContract());
        target.setIsOpenBidding(source.getIsOpenBidding());
        target.setIsYearBudget(source.getIsYearBudget());
        target.setIsShare(source.getIsShare());
        target.setContractNature(source.getContractNature());
        target.setIsSgat(source.getIsSgat());
        target.setIsNormal(source.getIsNormal());
        target.setIsTerminated(null);
        target.setIsSingleAgreement(source.getIsSingleAgreement());
        target.setBiddingNo(source.getBiddingNo());
        target.setOpenBiddingRemark(source.getOpenBiddingRemark());
        target.setContractGistRemark(source.getContractGistRemark());
        target.setEwAmountType(source.getEwAmountType());
        target.setDjAmount(source.getDjAmount());
        target.setYjAmount(source.getYjAmount());
        target.setYfkAmount(source.getYfkAmount());
        target.setBzjAmount(source.getBzjAmount());
        target.setBodySummary(source.getBodySummary());
        target.setTemplateFieldValuesJson(source.getTemplateFieldValuesJson());
        target.setContractTextFileName(source.getContractTextFileName());
        target.setContractTextFilePath(source.getContractTextFilePath());
        target.setContractTextFileType(source.getContractTextFileType());
        target.setContractTextFileSize(source.getContractTextFileSize());
        target.setSignDate(source.getSignDate());
        target.setSealDate(source.getSealDate());
        target.setActiveDate(source.getActiveDate());
        target.setInvoiceDate(source.getInvoiceDate());
        target.setReceiveDate(source.getReceiveDate());
        target.setRemindDays(source.getRemindDays());
        target.setRemark(source.getRemark());
        target.setPartyList(copyPartyList(source.getPartyList()));
        target.setFileList(copyFileList(source.getFileList()));
        target.setPerformancePlans(copyPlanList(source.getPerformancePlans()));
        return target;
    }

    private String buildCopiedContractName(String sourceName) {
        String baseName = trim(sourceName);
        if (baseName == null || baseName.isBlank()) {
            return "复制合同";
        }
        if (baseName.endsWith("-副本")) {
            return baseName;
        }
        return baseName + "-副本";
    }

    private String buildChangedContractName(String sourceName) {
        String baseName = trim(sourceName);
        if (baseName == null || baseName.isBlank()) {
            return "变更合同";
        }
        if (baseName.endsWith("-变更")) {
            return baseName;
        }
        return baseName + "-变更";
    }

    private List<ContractLaunchPartyDto> copyPartyList(List<ContractLaunchPartyDto> sourceList) {
        if (sourceList == null || sourceList.isEmpty()) {
            return new ArrayList<>();
        }
        List<ContractLaunchPartyDto> targetList = new ArrayList<>();
        for (ContractLaunchPartyDto source : sourceList) {
            ContractLaunchPartyDto target = new ContractLaunchPartyDto();
            target.setRoleCode(source.getRoleCode());
            target.setRoleName(source.getRoleName());
            target.setPartnerId(source.getPartnerId());
            target.setOppositeId(source.getOppositeId());
            target.setCompanyName(source.getCompanyName());
            target.setCompanyType(source.getCompanyType());
            target.setOppositeCharacter(source.getOppositeCharacter());
            target.setContactName(source.getContactName());
            target.setContactPhone(source.getContactPhone());
            target.setContactEmail(source.getContactEmail());
            target.setContactAddress(source.getContactAddress());
            target.setRegisteredAddress(source.getRegisteredAddress());
            target.setLegalRepresentative(source.getLegalRepresentative());
            target.setCreditCode(source.getCreditCode());
            target.setBankName(source.getBankName());
            target.setBankAccount(source.getBankAccount());
            target.setNaturalPersonIdType(source.getNaturalPersonIdType());
            target.setNaturalPersonIdNumber(source.getNaturalPersonIdNumber());
            target.setIsGroupInternal(source.getIsGroupInternal());
            target.setIsHongKongMacaoTaiwan(source.getIsHongKongMacaoTaiwan());
            target.setRemark(source.getRemark());
            targetList.add(target);
        }
        return targetList;
    }

    private List<ContractLaunchFileDto> copyFileList(List<ContractLaunchFileDto> sourceList) {
        if (sourceList == null || sourceList.isEmpty()) {
            return new ArrayList<>();
        }
        List<ContractLaunchFileDto> targetList = new ArrayList<>();
        for (ContractLaunchFileDto source : sourceList) {
            if (ContractLaunchConstants.FILE_CATEGORY_SIGNED.equals(source.getFileCategory())
                    || ContractLaunchConstants.FILE_CATEGORY_ARCHIVE.equals(source.getFileCategory())) {
                continue;
            }
            ContractLaunchFileDto target = new ContractLaunchFileDto();
            target.setFileCategory(source.getFileCategory());
            target.setFileName(source.getFileName());
            target.setFilePath(source.getFilePath());
            target.setFileType(source.getFileType());
            target.setFileSize(source.getFileSize());
            target.setRemark(source.getRemark());
            targetList.add(target);
        }
        return targetList;
    }

    private List<ContractLaunchPlanDto> copyPlanList(List<ContractLaunchPlanDto> sourceList) {
        if (sourceList == null || sourceList.isEmpty()) {
            return new ArrayList<>();
        }
        List<ContractLaunchPlanDto> targetList = new ArrayList<>();
        for (ContractLaunchPlanDto source : sourceList) {
            ContractLaunchPlanDto target = new ContractLaunchPlanDto();
            target.setPlanMatter(source.getPlanMatter());
            target.setPlanWhere(source.getPlanWhere());
            target.setPlanTime(source.getPlanTime());
            target.setPlanDay(source.getPlanDay());
            target.setPlanMoney(source.getPlanMoney());
            target.setPlanSubjectCode(source.getPlanSubjectCode());
            target.setPlanSubjectName(source.getPlanSubjectName());
            target.setPlanDetail(source.getPlanDetail());
            target.setPlanStatus("0");
            target.setRemark(source.getRemark());
            targetList.add(target);
        }
        return targetList;
    }

    /**
     * 批量填充子表数据（合同方/文件/履行计划/日志）。
     *
     * 一次性查完所有子表再按 contractId 分组赋值，避免 N+1 查询。
     * includeLogs: 列表页不查日志（日志数据量大），详情页才查。
     */
    private void fillChildren(List<ContractLaunchDto> list, boolean includeLogs) {
        if (list == null || list.isEmpty()) {
            return;
        }
        List<Long> ids = list.stream().map(ContractLaunchDto::getId).filter(Objects::nonNull).collect(Collectors.toList());
        if (ids.isEmpty()) {
            return;
        }
        Map<Long, List<ContractLaunchPartyDto>> partyMap = contractLaunchMapper.selectPartyByContractIds(ids).stream()
                .collect(Collectors.groupingBy(ContractLaunchPartyDto::getContractId));
        Map<Long, List<ContractLaunchFileDto>> fileMap = contractLaunchMapper.selectFileByContractIds(ids).stream()
                .collect(Collectors.groupingBy(ContractLaunchFileDto::getContractId));
        Map<Long, List<ContractLaunchPlanDto>> planMap = contractLaunchMapper.selectPlanByContractIds(ids).stream()
                .collect(Collectors.groupingBy(ContractLaunchPlanDto::getContractId));
        Map<Long, List<ContractLaunchLogDto>> logMap = includeLogs
                ? contractLaunchMapper.selectLogByContractIds(ids).stream().collect(Collectors.groupingBy(ContractLaunchLogDto::getContractId))
                : Collections.emptyMap();

        list.forEach(item -> {
            item.setPartyList(new ArrayList<>(partyMap.getOrDefault(item.getId(), Collections.emptyList())));
            item.setFileList(new ArrayList<>(fileMap.getOrDefault(item.getId(), Collections.emptyList())));
            item.setPerformancePlans(new ArrayList<>(planMap.getOrDefault(item.getId(), Collections.emptyList())));
            item.setLogList(new ArrayList<>(logMap.getOrDefault(item.getId(), Collections.emptyList())));
            item.setCounterpartySummary(
                    item.getPartyList().stream()
                            .filter(party -> !"A".equalsIgnoreCase(party.getRoleCode()))
                            .map(ContractLaunchPartyDto::getCompanyName)
                            .filter(name -> name != null && !name.isBlank())
                            .collect(Collectors.joining("；"))
            );
        });
    }

    /** 保存子表数据（合同方/文件/履行计划，插入前生成主键 ID） */
    private void saveChildren(ContractLaunchDto contract, String operator) {
        if (contract.getPartyList() != null) {
            for (int i = 0; i < contract.getPartyList().size(); i++) {
                ContractLaunchPartyDto party = contract.getPartyList().get(i);
                party.setId(contractLaunchMapper.selectPartySeqNextVal());
                party.setContractId(contract.getId());
                party.setSortOrder(i + 1);
            }
            if (!contract.getPartyList().isEmpty()) {
                contractLaunchMapper.batchInsertParty(contract.getPartyList(), operator, operator);
            }
        }
        if (contract.getFileList() != null) {
            for (int i = 0; i < contract.getFileList().size(); i++) {
                ContractLaunchFileDto file = contract.getFileList().get(i);
                file.setId(contractLaunchMapper.selectFileSeqNextVal());
                file.setContractId(contract.getId());
                file.setSortOrder(i + 1);
            }
            if (!contract.getFileList().isEmpty()) {
                contractLaunchMapper.batchInsertFile(contract.getFileList(), operator, operator);
            }
        }
        if (contract.getPerformancePlans() != null) {
            for (ContractLaunchPlanDto plan : contract.getPerformancePlans()) {
                plan.setId(contractLaunchMapper.selectPlanSeqNextVal());
                plan.setContractId(contract.getId());
                if (plan.getPlanStatus() == null || plan.getPlanStatus().isBlank()) {
                    plan.setPlanStatus("0");
                }
            }
            if (!contract.getPerformancePlans().isEmpty()) {
                contractLaunchMapper.batchInsertPlan(contract.getPerformancePlans(), operator, operator);
            }
        }
    }

    /** 替换子表数据（先删旧数据，再保存新数据） */
    private void replaceChildren(ContractLaunchDto contract, String operator) {
        contractLaunchMapper.deletePartyByContractId(contract.getId(), operator);
        contractLaunchMapper.deleteFileByContractId(contract.getId(), operator);
        contractLaunchMapper.deletePlanByContractId(contract.getId(), operator);
        saveChildren(contract, operator);
    }

    /** 保存签署文件到 file 子表（先删旧签署文件，再插入新的） */
    private void saveSignedFile(Long contractId, ContractLaunchSealDto sealDto, String operator) {
        if (sealDto.getSignedFilePath() == null || sealDto.getSignedFilePath().isBlank()) {
            return;
        }
        contractLaunchMapper.deleteFileByContractIdAndCategory(contractId, ContractLaunchConstants.FILE_CATEGORY_SIGNED, operator);
        ContractLaunchFileDto fileDto = new ContractLaunchFileDto();
        fileDto.setId(contractLaunchMapper.selectFileSeqNextVal());
        fileDto.setContractId(contractId);
        fileDto.setFileCategory(ContractLaunchConstants.FILE_CATEGORY_SIGNED);
        fileDto.setFileName(sealDto.getSignedFileName() == null || sealDto.getSignedFileName().isBlank()
                ? "已签署合同文件"
                : sealDto.getSignedFileName());
        fileDto.setFilePath(sealDto.getSignedFilePath());
        fileDto.setFileType(sealDto.getSignedFileType());
        fileDto.setFileSize(sealDto.getSignedFileSize());
        fileDto.setSortOrder(1);
        contractLaunchMapper.batchInsertFile(Collections.singletonList(fileDto), operator, operator);
    }

    /** 写入操作日志 */
    private void insertLog(Long contractId, String actionType, String actionName, String actionStatus, String actionContent, Object payload, SysUserEntity currentUser) {
        ContractLaunchLogDto logDto = new ContractLaunchLogDto();
        logDto.setId(contractLaunchMapper.selectLogSeqNextVal());
        logDto.setContractId(contractId);
        logDto.setActionType(actionType);
        logDto.setActionName(actionName);
        logDto.setActionStatus(actionStatus);
        logDto.setOperatorName(currentUser.getUsername());
        logDto.setOperatorPhone(currentUser.getMobilePhone());
        logDto.setActionTime(LocalDateTime.now());
        logDto.setActionContent(actionContent);
        logDto.setCallbackPayload(payload == null ? null : JSON.toJSONString(payload));
        contractLaunchMapper.insertLog(logDto);
    }

    /** 数据清洗：去空格、补默认值 */
    private void normalizeContract(ContractLaunchDto contract) {
        if (contract == null) {
            throw new ServiceException("合同数据不能为空");
        }
        contract.setContractName(trim(contract.getContractName()));
        contract.setContractCode(trim(contract.getContractCode()));
        contract.setContractType(trim(contract.getContractType()));
        contract.setContractTypeCode(trim(contract.getContractTypeCode()));
        contract.setPaymentMethod(trim(contract.getPaymentMethod()));
        contract.setPrimaryContent(trim(contract.getPrimaryContent()));
        contract.setAmountExplain(trim(contract.getAmountExplain()));
        contract.setNeedAmendments(trim(contract.getNeedAmendments()));
        contract.setAssistDepartmentName(trim(contract.getAssistDepartmentName()));
        contract.setExecutorAccount(trim(contract.getExecutorAccount()));
        contract.setPeriodExplain(trim(contract.getPeriodExplain()));
        contract.setContractGistRemark(trim(contract.getContractGistRemark()));
        contract.setBodySummary(trim(contract.getBodySummary()));
        contract.setTemplateFieldValuesJson(trim(contract.getTemplateFieldValuesJson()));
        contract.setSigningSubjectCode(trim(contract.getSigningSubjectCode()));
        contract.setDateType(trim(contract.getDateType()));
        contract.setPushUrl(trim(contract.getPushUrl()));
        contract.setEwAmountType(trim(contract.getEwAmountType()));
        contract.setBiddingNo(trim(contract.getBiddingNo()));
        if (contract.getDateType() == null || contract.getDateType().isBlank()) {
            contract.setDateType("0");
        }
        if (contract.getTextSource() == null || contract.getTextSource().isBlank()) {
            contract.setTextSource("standardText");
        }
        if (contract.getIsElectron() == null || contract.getIsElectron().isBlank()) {
            contract.setIsElectron("1");
        }
        if (contract.getPaymentDirection() == null || contract.getPaymentDirection().isBlank()) {
            contract.setPaymentDirection("0");
        }
        if (contract.getNeedAmendments() == null || contract.getNeedAmendments().isBlank()) {
            contract.setNeedAmendments("0");
        }
        if (contract.getPushUrl() == null || contract.getPushUrl().isBlank()) {
            contract.setPushUrl("/contract/launch/callback/law/approve");
        }
        if (contract.getSealQuantity() == null) {
            contract.setSealQuantity(1);
        }
        if (contract.getPartyList() == null) {
            contract.setPartyList(new ArrayList<>());
        }
        if (contract.getFileList() == null) {
            contract.setFileList(new ArrayList<>());
        }
        if (contract.getPerformancePlans() == null) {
            contract.setPerformancePlans(new ArrayList<>());
        }
    }

    /** 校验草稿必填项 */
    private void validateDraft(ContractLaunchDto contract) {
        if (contract.getContractName() == null || contract.getContractName().isBlank()) {
            throw new ServiceException("合同名称不能为空");
        }
        if (contract.getContractTypeCode() == null || contract.getContractTypeCode().isBlank()) {
            throw new ServiceException("合同类型编码不能为空");
        }
        if (contract.getContractType() == null || contract.getContractType().isBlank()) {
            throw new ServiceException("合同类型不能为空");
        }
        if (contract.getValuationMode() == null || contract.getValuationMode().isBlank()) {
            throw new ServiceException("计价方式不能为空");
        }
        if (contract.getCurrencyName() == null || contract.getCurrencyName().isBlank()) {
            throw new ServiceException("币种不能为空");
        }
        if (contract.getSealType() == null || contract.getSealType().isBlank()) {
            throw new ServiceException("签订方式不能为空");
        }
        if (contract.getSealQuantity() != null && contract.getSealQuantity() < 1) {
            throw new ServiceException("用印数量必须大于等于1");
        }
        if (contract.getContractPeriodType() == null || contract.getContractPeriodType().isBlank()) {
            throw new ServiceException("合同期限类型不能为空");
        }
        if ("0".equals(contract.getContractPeriodType())
                && (contract.getEffectiveStart() == null || contract.getEffectiveEnd() == null)) {
            throw new ServiceException("固定期限合同必须填写开始和结束时间");
        }
        if ("1".equals(contract.getContractPeriodType())
                && (contract.getPeriodExplain() == null || contract.getPeriodExplain().isBlank())) {
            throw new ServiceException("无固定期限合同必须填写期限说明");
        }
        if (contract.getActiveDate() == null) {
            throw new ServiceException("合同生效日期不能为空");
        }
        validateActiveDateWithinTerm(contract);
        if (contract.getPartyList() == null || contract.getPartyList().isEmpty()) {
            throw new ServiceException("至少需要维护一个合同方");
        }
        if ("2".equals(contract.getValuationMode()) && contract.getEstimatedAmount() == null) {
            throw new ServiceException("预估总价合同必须填写预估金额");
        }
        boolean hasPartyA = contract.getPartyList().stream().anyMatch(item -> "A".equalsIgnoreCase(item.getRoleCode()));
        boolean hasCounterparty = contract.getPartyList().stream().anyMatch(item -> !"A".equalsIgnoreCase(item.getRoleCode()));
        if (!hasPartyA) {
            throw new ServiceException("必须维护我方信息");
        }
        if (!hasCounterparty) {
            throw new ServiceException("至少维护一个相对方");
        }
        if (Integer.valueOf(1).equals(contract.getIsOpenBidding())
                && (contract.getBiddingNo() == null || contract.getBiddingNo().isBlank())) {
            throw new ServiceException("公开招标时必须填写关联招标号");
        }
        if (requiresAuthAttachment(contract.getSealType())
                && (contract.getFileList() == null || contract.getFileList().stream().noneMatch(item ->
                ContractLaunchConstants.FILE_CATEGORY_AUTH.equals(item.getFileCategory())
                        && item.getFilePath() != null
                        && !item.getFilePath().isBlank()))) {
            throw new ServiceException("当前签订方式必须上传授权委托书附件");
        }
        validatePerformancePlans(contract, false);
    }

    private boolean canEdit(String status) {
        return ContractLaunchConstants.STATUS_DRAFT.equals(status) || ContractLaunchConstants.STATUS_REJECTED.equals(status);
    }

    private boolean canSubmit(String status) {
        return ContractLaunchConstants.STATUS_DRAFT.equals(status) || ContractLaunchConstants.STATUS_REJECTED.equals(status);
    }

    private boolean canDelete(String status) {
        return ContractLaunchConstants.STATUS_DRAFT.equals(status) || ContractLaunchConstants.STATUS_REJECTED.equals(status);
    }

    /** 通过法务回调报文查找对应合同（先取 contractSysId 再查库） */
    private ContractLaunchDto getContractByLawCallback(Map<String, Object> payload) {
        if (payload == null || payload.isEmpty()) {
            throw new ServiceException("法务回调报文不能为空");
        }
        String contractSysId = getFirstText(payload, "contractSysId", "businessNo", "thirdContractId");
        if (contractSysId == null || contractSysId.isBlank()) {
            throw new ServiceException("法务回调缺少 contractSysId，无法定位合同");
        }
        ContractLaunchDto contract = contractLaunchMapper.selectContractByContractSysId(contractSysId);
        if (contract == null) {
            throw new ServiceException("未找到法务回调对应的合同：" + contractSysId);
        }
        fillChildren(Collections.singletonList(contract), true);
        return contract;
    }

    /** 判断法务回调是否审批通过（兼容多种字段名和值） */
    private boolean isLawCallbackApproved(Map<String, Object> payload) {
        String status = getFirstText(payload, "approveStatus", "status", "result", "auditStatus");
        if (status == null || status.isBlank()) {
            return true;
        }
        String normalized = status.trim().toLowerCase();
        return normalized.equals("1")
                || normalized.equals("true")
                || normalized.equals("success")
                || normalized.equals("approved")
                || normalized.equals("pass")
                || normalized.equals("agree")
                || normalized.equals("通过")
                || normalized.equals("审批通过");
    }

    /** 从回调报文中提取指定 key 的文本值（多个 key 依次尝试，取第一个非空） */
    private String getFirstText(Map<String, Object> payload, String... keys) {
        if (payload == null) {
            return null;
        }
        for (String key : keys) {
            Object value = payload.get(key);
            if (value == null) {
                continue;
            }
            String text = String.valueOf(value).trim();
            if (!text.isBlank()) {
                return text;
            }
        }
        return null;
    }

    /** 构建结算平台推送合同报文（pushContract 接口的 payload） */
    private Map<String, Object> buildSettlementPushPayload(ContractLaunchDto contract) {
        Map<String, Object> payload = new LinkedHashMap<>();
        payload.put("contractSource", contract.getContractSource());
        payload.put("isAddit", contract.getIsAddit() == null ? 2 : contract.getIsAddit());
        String operate = (ContractLaunchConstants.STATUS_REJECTED.equals(contract.getStatus())
                || (contract.getLawContractCode() != null && !contract.getLawContractCode().isBlank()))
                ? "update" : "create";
        payload.put("operate", operate);
        payload.put("mainContractCode", contract.getMainContractCode());
        payload.put("periodExplain", contract.getPeriodExplain());
        payload.put("valuationMode", contract.getValuationMode());
        payload.put("isMajorContract", contract.getIsMajorContract() == null ? 0 : contract.getIsMajorContract());
        payload.put("sealType", contract.getSealType());
        payload.put("creatorPhone", contract.getCreatorPhone());
        payload.put("dateType", contract.getDateType());
        payload.put("currencyName", contract.getCurrencyName());
        payload.put("contractGistRemark", contract.getContractGistRemark());
        payload.put("contractSysId", contract.getContractSysId());
        payload.put("selfCode", contract.getSelfCode());
        payload.put("contractName", contract.getContractName());
        payload.put("startTime", contract.getEffectiveStart());
        payload.put("endTime", contract.getEffectiveEnd());
        payload.put("amountExplain", contract.getAmountExplain());
        payload.put("isText", contract.getTextSource());
        payload.put("primaryContent", contract.getPrimaryContent());
        payload.put("isElectron", contract.getIsElectron());
        payload.put("paymentDirection", contract.getPaymentDirection());
        payload.put("contractTypeCode", contract.getContractTypeCode());
        payload.put("bodySummary", contract.getBodySummary());
        payload.put("signingSubjectCode",
                contract.getSigningSubjectCode() == null || contract.getSigningSubjectCode().isBlank()
                        ? contract.getOrgId()
                        : contract.getSigningSubjectCode());
        payload.put("executorAccount", contract.getExecutorAccount());
        payload.put("contractPeriod", contract.getContractPeriodType());
        payload.put("contractAmount", contract.getContractAmount());
        payload.put("isSgat", contract.getIsSgat());
        payload.put("isOpenBidding", contract.getIsOpenBidding() == null ? 0 : contract.getIsOpenBidding());
        payload.put("pushUrl", contract.getPushUrl());
        payload.put("isYearBudget", contract.getIsYearBudget() == null ? 0 : contract.getIsYearBudget());
        payload.put("isShare", contract.getIsShare());
        payload.put("assistDepartmentName", contract.getAssistDepartmentName());
        payload.put("contractNature", contract.getContractNature());
        payload.put("contractCode", contract.getContractCode());
        payload.put("ewAmountType", contract.getEwAmountType());
        payload.put("djAmount", contract.getDjAmount());
        payload.put("yjAmount", contract.getYjAmount());
        payload.put("yfkAmount", contract.getYfkAmount());
        payload.put("bzjAmount", contract.getBzjAmount());
        payload.put("contractOpposites", contract.getPartyList().stream()
                .filter(item -> !"A".equalsIgnoreCase(item.getRoleCode()))
                .map(item -> {
                    Map<String, Object> map = new LinkedHashMap<>();
                    map.put("oppCharacter", "2".equals(item.getOppositeCharacter()) ? "ZZR" : "QY");
                    map.put("credirCode", item.getCreditCode());
                    map.put("idCard", item.getNaturalPersonIdNumber());
                    map.put("oppositeName", item.getCompanyName());
                    return map;
                }).collect(Collectors.toList()));
        payload.put("contractKxPlans", contract.getPerformancePlans().stream().map(plan -> {
            Map<String, Object> map = new LinkedHashMap<>();
            map.put("planWhere", plan.getPlanWhere());
            map.put("planMoney", plan.getPlanMoney());
            map.put("planDetail", plan.getPlanDetail());
            map.put("planTime", plan.getPlanTime());
            map.put("planMatter", plan.getPlanMatter());
            map.put("planDay", plan.getPlanDay());
            map.put("planSubjectCode", plan.getPlanSubjectCode());
            map.put("planSubjectName", plan.getPlanSubjectName());
            map.put("executorUnitId", plan.getExecutorUnitId());
            return map;
        }).collect(Collectors.toList()));
//        payload.put("contractText", buildSettlementFilePayloadList(contract, ContractLaunchConstants.FILE_CATEGORY_BODY));
//        payload.put("otherAttach", buildSettlementFilePayloadList(contract, ContractLaunchConstants.FILE_CATEGORY_OTHER));
//        payload.put("contractGistFile", buildSettlementFilePayloadList(contract, ContractLaunchConstants.FILE_CATEGORY_GIST));
        return payload;
    }

    /** 构建数据采集合同报文（collectContract 接口的 payload） */
    private Map<String, Object> buildDataCollectionContractPayload(ContractLaunchDto contract) {
        Map<String, Object> payload = new LinkedHashMap<>();
        payload.put("c_name", contract.getContractName());
        payload.put("c_code", contract.getLawContractCode() == null || contract.getLawContractCode().isBlank() ? contract.getContractCode() : contract.getLawContractCode());
        // TODO: c_weNames 传机构编码，当前用 signingSubjectCode（签约主体编码），若为空则用 orgId
        payload.put("c_weNames", contract.getSigningSubjectCode() != null && !contract.getSigningSubjectCode().isBlank()
                ? contract.getSigningSubjectCode()
                : contract.getOrgId());
        payload.put("c_oppositeNames", contract.getPartyList().stream()
                .filter(item -> !"A".equalsIgnoreCase(item.getRoleCode()))
                .map(item -> item.getOppositeId() != null && !item.getOppositeId().isBlank()
                        ? item.getOppositeId()
                        : item.getPartnerId() == null ? null : String.valueOf(item.getPartnerId()))
                .filter(Objects::nonNull)
                .collect(Collectors.joining(",")));
        payload.put("c_textSource", contract.getTextSource());
        payload.put("c_needAmendments", contract.getNeedAmendments());
        payload.put("c_mainContent", contract.getPrimaryContent());
        payload.put("c_direction", contract.getPaymentDirection());
        payload.put("c_method", contract.getValuationMode());
        // 合同标的额：计价方式=固定总价时必填
        if ("1".equals(contract.getValuationMode()) && contract.getContractAmount() != null) {
            payload.put("c_subjectAMT", contract.getContractAmount());
        } else {
            payload.put("c_subjectAMT", null);
        }
        payload.put("c_currency", contract.getCurrencyName());
        payload.put("c_estimatedAMT", "2".equals(contract.getValuationMode()) ? contract.getEstimatedAmount() : null);
        payload.put("c_amountExplain", contract.getAmountExplain());
        // 额外金额
        payload.put("c_additional", contract.getEwAmountType());
        if (contract.getDjAmount() != null) payload.put("c_dAMT", contract.getDjAmount());
        if (contract.getYjAmount() != null) payload.put("c_yAMT", contract.getYjAmount());
        if (contract.getYfkAmount() != null) payload.put("c_yfkAMT", contract.getYfkAmount());
        if (contract.getBzjAmount() != null) payload.put("c_bzjAMT", contract.getBzjAmount());
        payload.put("c_type", contract.getContractTypeCode());
        payload.put("c_termType", contract.getContractPeriodType());
        payload.put("c_termExplain", contract.getPeriodExplain());
        payload.put("c_termStart", contract.getEffectiveStart());
        payload.put("c_termEnd", contract.getEffectiveEnd());
        payload.put("c_qyztGAT", contract.getIsSgat());
        payload.put("c_qyztSW", contract.getContractNature());
        payload.put("c_executor", contract.getExecutorAccount());
        payload.put("c_openBidding", contract.getIsOpenBidding());
        // 关联招标号：公开招标选是时必填
        if (Integer.valueOf(1).equals(contract.getIsOpenBidding()) && contract.getBiddingNo() != null) {
            payload.put("c_biddingNo", contract.getBiddingNo());
        } else {
            payload.put("c_biddingNo", null);
        }
        payload.put("c_currentBudget", contract.getIsYearBudget());
        payload.put("c_major", contract.getIsMajorContract());
        payload.put("c_signMethod", contract.getSealType());
//        payload.put("c_authAttachs", buildCollectionFilePayloadList(contract, ContractLaunchConstants.FILE_CATEGORY_AUTH));
        payload.put("c_creator", contract.getInitiator());
        payload.put("c_creatorPhone", contract.getCreatorPhone());
        payload.put("c_createDept", contract.getInitiatorDept());
        payload.put("c_creationTime", contract.getCreateTime());
        payload.put("c_signExplain", contract.getContractGistRemark());
//        payload.put("c_zw", buildCollectionFilePayloadList(contract, ContractLaunchConstants.FILE_CATEGORY_BODY));
//        payload.put("c_attachs", buildCollectionFilePayloadList(contract, ContractLaunchConstants.FILE_CATEGORY_OTHER));
//        payload.put("c_signAttachs", buildCollectionFilePayloadList(contract, ContractLaunchConstants.FILE_CATEGORY_GIST));
        payload.put("c_lxPlan", buildCollectionPlanPayloadList(contract));
        // 用印信息
        Map<String, Object> seal = new LinkedHashMap<>();
        seal.put("seal_isSeal", "1");
        seal.put("seal_approveSame", "1");
        seal.put("seal_type", contract.getSealType());
        seal.put("seal_quantity", contract.getSealQuantity() == null ? "1" : String.valueOf(contract.getSealQuantity()));
        seal.put("seal_date", contract.getSealDate());
        payload.put("c_seal", seal);
        // 签订信息
        Map<String, Object> sign = new LinkedHashMap<>();
        sign.put("sign_isSign", "1");
        sign.put("sign_date", contract.getSignDate());
        sign.put("sign_activeDate", contract.getActiveDate());
//        sign.put("sign_attachs", buildCollectionFilePayloadList(contract, ContractLaunchConstants.FILE_CATEGORY_SIGNED));
        sign.put("sign_executor", contract.getExecutorAccount());
        payload.put("c_sign", sign);
        // 时间戳
        payload.put("timeStamp", String.valueOf(System.currentTimeMillis()));
        return payload;
    }

    private List<Map<String, Object>> buildCollectionPlanPayloadList(ContractLaunchDto contract) {
        boolean amountRequired = isPlanAmountRequired(contract.getValuationMode());
        return contract.getPerformancePlans().stream().map(plan -> {
            Map<String, Object> map = new LinkedHashMap<>();
            map.put("lx_id", plan.getId() == null ? null : String.valueOf(plan.getId()));
            map.put("lx_item", plan.getPlanMatter());
            map.put("lx_condition", plan.getPlanWhere());
            map.put("lx_nodeDate", plan.getPlanTime());
            map.put("lx_remindDays", plan.getPlanDay() == null ? null : String.valueOf(plan.getPlanDay()));
            if (amountRequired && plan.getPlanMoney() != null) {
                map.put("lx_amount", plan.getPlanMoney());
            }
            map.put("lx_remarks", plan.getPlanDetail());
            return map;
        }).collect(Collectors.toList());
    }

    private boolean requiresAuthAttachment(String sealType) {
        return "GZBSQRQZ".equals(sealType) || "HTZYZBSQRQZ".equals(sealType);
    }

    /** 构建归档报文（pushArchive 接口的 payload） */
    private Map<String, Object> buildArchivePayload(ContractLaunchDto contract, ContractLaunchArchiveDto archiveDto) {
        Map<String, Object> payload = new LinkedHashMap<>();
        payload.put("contractCode", contract.getLawContractCode());
        payload.put("contractSysId", contract.getContractSysId());
        payload.put("Keeper", archiveDto == null ? null : archiveDto.getArchiveKeeper());
        payload.put("KeeperUnit", archiveDto == null ? null : archiveDto.getArchiveKeeperUnit());
        payload.put("KeeperUnitCode", archiveDto == null ? null : archiveDto.getArchiveKeeperUnitCode());
        payload.put("archivePapers", List.of());
        payload.put("archiveEles", buildSettlementArchiveEleList(archiveDto));
        return payload;
    }

    private List<Map<String, Object>> buildSettlementFilePayloadList(ContractLaunchDto contract, String fileCategory) {
        return contract.getFileList().stream()
                .filter(item -> fileCategory.equals(item.getFileCategory()))
                .map(item -> {
                    Map<String, Object> map = new LinkedHashMap<>();
                    map.put("fileName", item.getFileName());
                    map.put("filePath", legalFileBridgeService.ensureLegalFilePath(item));
                    map.put("fileSize", item.getFileSize());
                    map.put("fileType", item.getFileType());
                    return map;
                })
                .filter(item -> item.get("filePath") != null)
                .collect(Collectors.toList());
    }

    private List<Map<String, Object>> buildCollectionFilePayloadList(ContractLaunchDto contract, String fileCategory) {
        return contract.getFileList().stream()
                .filter(item -> fileCategory.equals(item.getFileCategory()))
                .map(item -> legalFileBridgeService.buildCollectionPayload(item))
                .filter(item -> item != null && item.get("path") != null)
                .collect(Collectors.toList());
    }

    private List<Map<String, Object>> buildSettlementArchiveEleList(ContractLaunchArchiveDto archiveDto) {
        if (archiveDto == null || archiveDto.getArchiveFiles() == null || archiveDto.getArchiveFiles().isEmpty()) {
            return List.of();
        }
        return archiveDto.getArchiveFiles().stream()
                .map(item -> {
                    Map<String, Object> map = new LinkedHashMap<>();
                    map.put("archivesName", item.getFileName());
                    map.put("approvalAttachs", List.of(legalFileBridgeService.buildSettlementPayload(item)));
                    return map;
                })
                .collect(Collectors.toList());
    }

    /**
     * 如果 signedFilePath 是外部URL，则下载到本地并返回本地路径；
     * 如果已是本地路径则直接返回。
     */
    private String resolveSignedFilePath(ContractLaunchSealDto sealDto) {
        String filePath = sealDto.getSignedFilePath();
        if (filePath == null || filePath.isBlank()) {
            return filePath;
        }
        if (isExternalUrl(filePath)) {
            log.info("签章文件为外部URL，先通过签章API获取新下载地址: {}", filePath);
            String token = sealTokenService.getAccessToken();
            // 通过签章API获取服务端可用的下载URL
            String freshUrl = fetchSealDownloadUrl(sealDto.getSealContractId(), token, filePath);
            log.info("获取到签章下载地址: {}", freshUrl);
            String localPath = localFileService.downloadFromUrl(freshUrl, token);
            log.info("签章文件已下载到本地: {}", localPath);
            return localPath;
        }
        return filePath;
    }

    /**
     * 通过签章平台API获取下载URL（服务端IP绑定的新URL）
     */
    private String fetchSealDownloadUrl(String sealContractId, String token, String fallbackUrl) {
        try {
            String apiUrl = sealConfig.getBaseUrl() + "/saas/api/contract/download";
            JSONObject body = new JSONObject();
            body.put("contractId", sealContractId);
            String response = SealHttpClient.post(apiUrl, body.toJSONString(), token, "application/json");
            log.info("签章平台下载API响应: {}", response);
            JSONObject result = JSON.parseObject(response);
            if (result != null && result.getBooleanValue("isSuccess")) {
                Object dataObj = result.get("data");
                log.info("签章平台data类型: {}, 值: {}", dataObj != null ? dataObj.getClass().getSimpleName() : "null", dataObj);
                if (dataObj instanceof JSONObject) {
                    String url = ((JSONObject) dataObj).getString("downloadUrl");
                    if (url != null && !url.isBlank()) return url;
                } else if (dataObj instanceof String && !((String) dataObj).isBlank()) {
                    return (String) dataObj;
                }
            } else {
                log.warn("签章平台下载API返回失败: {}", response);
            }
        } catch (Exception e) {
            log.warn("通过签章API获取下载URL失败，使用原始URL: {}", e.getMessage());
        }
        return fallbackUrl;
    }

    private boolean isExternalUrl(String path) {
        return path != null && (path.startsWith("http://") || path.startsWith("https://"));
    }

    private void validateSignCompletion(ContractLaunchDto contract) {
        if (contract.getSignDate() == null) {
            throw new ServiceException("签订日期不能为空");
        }
        if (contract.getSealDate() == null) {
            throw new ServiceException("用印时间不能为空");
        }
        if (contract.getActiveDate() == null) {
            throw new ServiceException("合同生效日期不能为空");
        }
        validateActiveDateWithinTerm(contract);
    }

    private void validateCollectionReady(ContractLaunchDto contract) {
        validateSignCompletion(contract);
        validatePerformancePlans(contract, true);
    }

    private void validatePerformancePlans(ContractLaunchDto contract, boolean requirePlanId) {
        if (contract.getPerformancePlans() == null || contract.getPerformancePlans().isEmpty()) {
            throw new ServiceException("履行计划不能为空");
        }
        boolean amountRequired = isPlanAmountRequired(contract.getValuationMode());
        for (ContractLaunchPlanDto plan : contract.getPerformancePlans()) {
            if (requirePlanId && plan.getId() == null) {
                throw new ServiceException("履行计划ID不能为空");
            }
            if (plan.getPlanMatter() == null || plan.getPlanMatter().isBlank()) {
                throw new ServiceException("履行计划的履行事项不能为空");
            }
            if (plan.getPlanWhere() == null || plan.getPlanWhere().isBlank()) {
                throw new ServiceException("履行计划的履约条件不能为空");
            }
            if (plan.getPlanTime() == null) {
                throw new ServiceException("履行计划的节点日期不能为空");
            }
            if (plan.getPlanDay() == null) {
                throw new ServiceException("履行计划的提前提醒天数不能为空");
            }
            if (amountRequired && plan.getPlanMoney() == null) {
                throw new ServiceException("履行计划的款项金额不能为空");
            }
        }
    }

    private boolean isPlanAmountRequired(String valuationMode) {
        return !"0".equals(valuationMode) && !"3".equals(valuationMode);
    }

    private void validateActiveDateWithinTerm(ContractLaunchDto contract) {
        if (contract.getActiveDate() == null || !"0".equals(contract.getContractPeriodType())) {
            return;
        }
        if (contract.getEffectiveStart() != null && contract.getActiveDate().isBefore(contract.getEffectiveStart())) {
            throw new ServiceException("合同生效日期不能早于合同开始日期");
        }
        if (contract.getEffectiveEnd() != null && contract.getActiveDate().isAfter(contract.getEffectiveEnd())) {
            throw new ServiceException("合同生效日期不能晚于合同结束日期");
        }
    }

    @SafeVarargs
    private final <T> T firstNonNull(T... values) {
        if (values == null) {
            return null;
        }
        for (T value : values) {
            if (value != null) {
                return value;
            }
        }
        return null;
    }

    private String trim(String value) {
        return value == null ? null : value.trim();
    }
}
