package com.usky.vpp.service.impl;

import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
import com.usky.common.core.bean.CommonPage;
import com.usky.common.core.exception.BusinessException;
import com.usky.vpp.config.VppSmsProperties;
import com.usky.vpp.constant.VppDrEventStatus;
import com.usky.vpp.domain.VppCustomer;
import com.usky.vpp.domain.VppCustomerContact;
import com.usky.vpp.domain.VppDrEvent;
import com.usky.vpp.domain.VppDrInvitation;
import com.usky.vpp.domain.VppDrParticipation;
import com.usky.vpp.domain.VppResourcePoint;
import com.usky.vpp.mapper.VppCustomerContactMapper;
import com.usky.vpp.mapper.VppCustomerMapper;
import com.usky.vpp.mapper.VppDrEventMapper;
import com.usky.vpp.mapper.VppDrInvitationMapper;
import com.usky.vpp.mapper.VppDrParticipationMapper;
import com.usky.vpp.mapper.VppResourcePointMapper;
import com.usky.vpp.service.VppAliyunSmsService;
import com.usky.vpp.service.VppDrInvitationService;
import com.usky.vpp.service.VppUnIntegrationService;
import com.usky.vpp.service.vo.DrInvitationNotifyRequest;
import com.usky.vpp.service.vo.DrInvitationReplyRequest;
import com.usky.vpp.service.vo.DrInvitationRequest;
import com.usky.vpp.service.vo.DrInvitationVO;
import com.usky.vpp.util.VppAuditHelper;
import com.usky.vpp.util.VppDrParticipationHelper;
import com.usky.vpp.util.VppPageHelper;
import com.usky.vpp.util.VppSmsTemplateHelper;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.BeanUtils;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import org.springframework.util.CollectionUtils;
import org.springframework.util.StringUtils;

import java.math.BigDecimal;
import java.time.LocalDate;
import java.time.LocalDateTime;
import java.time.LocalTime;
import java.util.Collections;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import java.util.Set;
import java.util.stream.Collectors;

@Service
public class VppDrInvitationServiceImpl implements VppDrInvitationService {

    private static final Logger log = LoggerFactory.getLogger(VppDrInvitationServiceImpl.class);

    private static final int REPLY_PENDING = 0;
    private static final int REPLY_ACCEPT = 1;
    private static final int REPLY_REJECT = 2;
    private static final int REPLY_TIMEOUT = 3;

    /** 与事件状态一致：0待参与 1已申报 2申报完成 3执行中 4已结束 5已取消 */
    private static final int RESPONSE_PENDING = VppDrEventStatus.PENDING;
    private static final int RESPONSE_DECLARED = VppDrEventStatus.DECLARED;
    private static final int RESPONSE_CANCELLED = VppDrEventStatus.CANCELLED;

    private static final int SMS_NOT_SENT = 0;
    private static final int SMS_SENT = 1;
    private static final int SMS_FAILED = 2;

    private static final int PARTICIPATE_ACCEPT = 1;

    @Autowired
    private VppDrInvitationMapper invitationMapper;
    @Autowired
    private VppDrEventMapper eventMapper;
    @Autowired
    private VppCustomerMapper customerMapper;
    @Autowired
    private VppCustomerContactMapper contactMapper;
    @Autowired
    private VppDrParticipationMapper participationMapper;
    @Autowired
    private VppResourcePointMapper resourcePointMapper;
    @Autowired
    private VppUnIntegrationService unIntegrationService;
    @Autowired
    private VppAliyunSmsService aliyunSmsService;
    @Autowired
    private VppSmsProperties smsProperties;

    @Override
    public CommonPage<DrInvitationVO> pageInvitation(Map<String, Object> params) {
        Page<VppDrInvitation> page = VppPageHelper.of(params);
        LambdaQueryWrapper<VppDrInvitation> wrapper = new LambdaQueryWrapper<VppDrInvitation>()
                .eq(VppDrInvitation::getDeleteFlag, VppAuditHelper.NOT_DELETED)
                .orderByDesc(VppDrInvitation::getIssueTime);

        applyListFilters(wrapper, params);

        Page<VppDrInvitation> result = invitationMapper.selectPage(page, wrapper);
        List<DrInvitationVO> records = toVoList(result.getRecords());
        return new CommonPage<>(records, result.getTotal(), result.getCurrent(), result.getSize());
    }

    @Override
    public DrInvitationVO getInvitation(Long id) {
        return toVo(requireInvitation(id));
    }

    @Override
    @Transactional(rollbackFor = Exception.class)
    public DrInvitationVO createInvitation(DrInvitationRequest request) {
        validateCreateRequest(request);

        VppDrEvent event = requireEvent(request.getDrEventId());
        VppCustomer customer = requireCustomer(request.getCustomerId());
        ensureInvitationUnique(request, null);

        VppDrInvitation invitation = new VppDrInvitation();
        invitation.setInvitationNo(VppAuditHelper.nextInvitationNo());
        invitation.setDrEventId(request.getDrEventId());
        invitation.setCustomerId(request.getCustomerId());
        invitation.setExecuteStartDate(request.getExecuteStartDate());
        invitation.setExecuteEndDate(request.getExecuteEndDate());
        invitation.setReplyDeadline(request.getReplyDeadline());
        invitation.setIssueTime(LocalDateTime.now());
        invitation.setTransactionType(request.getTransactionType() != null ? request.getTransactionType() : event.getEventType());
        invitation.setResponseType(request.getResponseType() != null ? request.getResponseType() : event.getResponseType());
        invitation.setDemandCapacityKw(request.getDemandCapacityKw());
        invitation.setDeclaredCapacityKw(request.getDeclaredCapacityKw() != null ? request.getDeclaredCapacityKw() : BigDecimal.ZERO);
        invitation.setActualClearedCapacityKw(request.getActualClearedCapacityKw());
        invitation.setEstimatedResponseCapacityKw(request.getEstimatedResponseCapacityKw());
        invitation.setActualResponseCapacityKw(request.getActualResponseCapacityKw());
        invitation.setIsWinningBid(request.getIsWinningBid());
        invitation.setResponseCompletionRate(resolveCompletionRate(request, invitation));
        invitation.setEstimatedCompletionRate(resolveEstimatedCompletionRate(request, invitation));
        invitation.setSiteId(request.getSiteId());
        invitation.setReplyStatus(REPLY_PENDING);
        invitation.setResponseStatus(RESPONSE_PENDING);
        invitation.setSmsNotifyStatus(SMS_NOT_SENT);
        invitation.setSmsContactId(resolveSmsContactId(request.getSmsContactId(), customer.getId()));
        invitation.setRemark(request.getRemark());
        VppAuditHelper.fillCreate(invitation);
        if (invitation.getTenantId() == null && event.getTenantId() != null) {
            invitation.setTenantId(event.getTenantId());
        }
        invitationMapper.insert(invitation);
        return toVo(invitation);
    }

    @Override
    @Transactional(rollbackFor = Exception.class)
    public void updateInvitation(Long id, DrInvitationRequest request) {
        VppDrInvitation invitation = requireInvitation(id);
        if (invitation.getReplyStatus() != REPLY_PENDING || invitation.getResponseStatus() != RESPONSE_PENDING) {
            throw new BusinessException("仅待回复状态的邀约可修改");
        }
        if (request == null) {
            throw new BusinessException("请求不能为空");
        }
        if (request.getExecuteStartDate() == null || request.getExecuteEndDate() == null) {
            throw new BusinessException("执行日期不能为空");
        }
        if (request.getReplyDeadline() == null) {
            throw new BusinessException("邀约回复截止日期不能为空");
        }
        if (request.getDemandCapacityKw() == null || request.getDemandCapacityKw().compareTo(BigDecimal.ZERO) <= 0) {
            throw new BusinessException("需求容量必须大于0");
        }
        validateDateRange(request.getExecuteStartDate(), request.getExecuteEndDate());

        invitation.setExecuteStartDate(request.getExecuteStartDate());
        invitation.setExecuteEndDate(request.getExecuteEndDate());
        invitation.setReplyDeadline(request.getReplyDeadline());
        invitation.setDemandCapacityKw(request.getDemandCapacityKw());
        if (request.getDeclaredCapacityKw() != null) {
            invitation.setDeclaredCapacityKw(request.getDeclaredCapacityKw());
        }
        if (request.getActualClearedCapacityKw() != null) {
            invitation.setActualClearedCapacityKw(request.getActualClearedCapacityKw());
        }
        if (request.getEstimatedResponseCapacityKw() != null) {
            invitation.setEstimatedResponseCapacityKw(request.getEstimatedResponseCapacityKw());
        }
        if (request.getActualResponseCapacityKw() != null) {
            invitation.setActualResponseCapacityKw(request.getActualResponseCapacityKw());
        }
        if (request.getIsWinningBid() != null) {
            invitation.setIsWinningBid(request.getIsWinningBid());
        }
        if (request.getSiteId() != null) {
            invitation.setSiteId(request.getSiteId());
        }
        invitation.setResponseCompletionRate(resolveCompletionRate(request, invitation));
        invitation.setEstimatedCompletionRate(resolveEstimatedCompletionRate(request, invitation));
        if (request.getTransactionType() != null) {
            invitation.setTransactionType(request.getTransactionType());
        }
        if (request.getResponseType() != null) {
            invitation.setResponseType(request.getResponseType());
        }
        if (request.getSmsContactId() != null) {
            requireContact(request.getSmsContactId(), invitation.getCustomerId());
            invitation.setSmsContactId(request.getSmsContactId());
        }
        if (request.getRemark() != null) {
            invitation.setRemark(request.getRemark());
        }
        VppAuditHelper.fillUpdate(invitation);
        invitationMapper.updateById(invitation);
    }

    @Override
    @Transactional(rollbackFor = Exception.class)
    public void deleteInvitation(Long id) {
        VppDrInvitation invitation = requireInvitation(id);
        if (invitation.getReplyStatus() == REPLY_ACCEPT) {
            throw new BusinessException("已参与的邀约不可删除");
        }
        if (invitation.getResponseStatus() != RESPONSE_PENDING) {
            throw new BusinessException("仅待参与状态的邀约可删除");
        }
        if (invitation.getParticipationId() != null) {
            throw new BusinessException("邀约已关联参与记录，不可删除");
        }

        VppAuditHelper.fillSoftDelete(invitation);
        invitationMapper.updateById(invitation);
    }

    @Override
    @Transactional(rollbackFor = Exception.class)
    public DrInvitationVO notifySms(Long id, DrInvitationNotifyRequest request) {
        VppDrInvitation invitation = requireInvitation(id);
        Long contactId = request != null && request.getContactId() != null
                ? request.getContactId()
                : invitation.getSmsContactId();
        if (contactId == null) {
            contactId = resolveSmsContactId(null, invitation.getCustomerId());
            invitation.setSmsContactId(contactId);
        }
        VppCustomerContact contact = requireContact(contactId, invitation.getCustomerId());
        if (!StringUtils.hasText(contact.getContactPhone())) {
            throw new BusinessException("联系人手机号为空，无法发送短信");
        }

        VppDrEvent event = requireEvent(invitation.getDrEventId());
        VppCustomer customer = requireCustomer(invitation.getCustomerId());
        String templateCode = resolveTemplateCode(request);
        String templateParam = VppSmsTemplateHelper.buildInvitationParam(invitation, event, customer, contact);

        try {
            aliyunSmsService.sendTemplateSms(contact.getContactPhone(), templateCode, templateParam);
            invitation.setSmsNotifyStatus(SMS_SENT);
            invitation.setSmsNotifyAt(LocalDateTime.now());
        } catch (BusinessException ex) {
            invitation.setSmsNotifyStatus(SMS_FAILED);
            VppAuditHelper.fillUpdate(invitation);
            invitationMapper.updateById(invitation);
            throw ex;
        } catch (Exception ex) {
            log.warn("邀约短信发送失败 invitationId={}: {}", id, ex.getMessage());
            invitation.setSmsNotifyStatus(SMS_FAILED);
            VppAuditHelper.fillUpdate(invitation);
            invitationMapper.updateById(invitation);
            throw new BusinessException("短信发送失败: " + ex.getMessage());
        }

        VppAuditHelper.fillUpdate(invitation);
        invitationMapper.updateById(invitation);
        return toVo(invitation);
    }

    @Override
    @Transactional(rollbackFor = Exception.class)
    public DrInvitationVO replyInvitation(Long id, DrInvitationReplyRequest request) {
        VppDrInvitation invitation = requireInvitation(id);
//        if (invitation.getReplyStatus() != REPLY_PENDING) {
//            throw new BusinessException("当前邀约已回复");
//        }
        if (invitation.getReplyDeadline() != null && LocalDateTime.now().isAfter(invitation.getReplyDeadline())) {
            throw new BusinessException("已超过邀约回复截止时间");
        }
        if (request == null || request.getParticipate() == null) {
            throw new BusinessException("回复结果不能为空");
        }

        if (Boolean.TRUE.equals(request.getParticipate())) {
            BigDecimal declared = request.getDeclaredCapacityKw() != null
                    ? request.getDeclaredCapacityKw()
                    : invitation.getDeclaredCapacityKw();
            if (declared == null || declared.compareTo(BigDecimal.ZERO) <= 0) {
                throw new BusinessException("申报出清容量必须大于0");
            }

            VppDrParticipation participation = findOrCreateParticipation(invitation, declared);
            invitation.setParticipationId(participation.getId());
            invitation.setDeclaredCapacityKw(declared);
            invitation.setReplyStatus(REPLY_ACCEPT);
            invitation.setResponseStatus(RESPONSE_DECLARED);
//            syncParticipationToUn(invitation.getDrEventId(), true, Collections.singletonList(participation));

        } else {
            if (!StringUtils.hasText(request.getReason())) {
                throw new BusinessException("拒绝原因不能为空");
            }
            invitation.setReplyStatus(REPLY_REJECT);
            invitation.setResponseStatus(RESPONSE_CANCELLED);
            invitation.setRemark(appendRemark(invitation.getRemark(), "拒绝原因: " + request.getReason()));
//            syncParticipationToUn(invitation.getDrEventId(), false, null);
        }

        VppAuditHelper.fillUpdate(invitation);
        invitationMapper.updateById(invitation);
        return toVo(invitation);
    }

    private void applyListFilters(LambdaQueryWrapper<VppDrInvitation> wrapper, Map<String, Object> params) {
        if (params == null) {
            return;
        }
        if (params.get("drEventId") != null) {
            wrapper.eq(VppDrInvitation::getDrEventId, Long.parseLong(params.get("drEventId").toString()));
        }
        if (params.get("eventId") != null && StringUtils.hasText(params.get("eventId").toString())) {
            List<Long> eventPks = eventMapper.selectList(new LambdaQueryWrapper<VppDrEvent>()
                            .eq(VppDrEvent::getDeleteFlag, VppAuditHelper.NOT_DELETED)
                            .like(VppDrEvent::getEventId, params.get("eventId").toString().trim()))
                    .stream()
                    .map(VppDrEvent::getId)
                    .collect(Collectors.toList());
            if (eventPks.isEmpty()) {
                wrapper.eq(VppDrInvitation::getDrEventId, -1L);
            } else {
                wrapper.in(VppDrInvitation::getDrEventId, eventPks);
            }
        }
        if (params.get("responseType") != null) {
            wrapper.eq(VppDrInvitation::getResponseType, Integer.parseInt(params.get("responseType").toString()));
        }
        if (params.get("replyStatus") != null) {
            wrapper.eq(VppDrInvitation::getReplyStatus, Integer.parseInt(params.get("replyStatus").toString()));
        }
        if (params.get("responseStatus") != null) {
            wrapper.eq(VppDrInvitation::getResponseStatus, Integer.parseInt(params.get("responseStatus").toString()));
        }
        if (params.get("customerId") != null) {
            wrapper.eq(VppDrInvitation::getCustomerId, Long.parseLong(params.get("customerId").toString()));
        }
        if (params.get("customerName") != null && StringUtils.hasText(params.get("customerName").toString())) {
            List<Long> customerIds = customerMapper.selectList(new LambdaQueryWrapper<VppCustomer>()
                            .eq(VppCustomer::getDeleteFlag, VppAuditHelper.NOT_DELETED)
                            .like(VppCustomer::getCustomerName, params.get("customerName").toString()))
                    .stream()
                    .map(VppCustomer::getId)
                    .collect(Collectors.toList());
            if (customerIds.isEmpty()) {
                wrapper.eq(VppDrInvitation::getCustomerId, -1L);
            } else {
                wrapper.in(VppDrInvitation::getCustomerId, customerIds);
            }
        }
        if (params.get("issueDateStart") != null && StringUtils.hasText(params.get("issueDateStart").toString())) {
            LocalDate start = LocalDate.parse(params.get("issueDateStart").toString());
            wrapper.ge(VppDrInvitation::getIssueTime, start.atStartOfDay());
        }
        if (params.get("issueDateEnd") != null && StringUtils.hasText(params.get("issueDateEnd").toString())) {
            LocalDate end = LocalDate.parse(params.get("issueDateEnd").toString());
            wrapper.le(VppDrInvitation::getIssueTime, end.atTime(LocalTime.MAX));
        }
    }

    private VppDrParticipation findOrCreateParticipation(VppDrInvitation invitation, BigDecimal declaredCapacityKw) {
        Long resourceId = resolveResourceIdBySiteId(invitation.getSiteId());
        LambdaQueryWrapper<VppDrParticipation> wrapper = new LambdaQueryWrapper<VppDrParticipation>()
                .eq(VppDrParticipation::getEventId, invitation.getDrEventId())
                .eq(VppDrParticipation::getCustomerId, invitation.getCustomerId())
                .eq(VppDrParticipation::getDeleteFlag, VppAuditHelper.NOT_DELETED);
        if (resourceId != null) {
            wrapper.eq(VppDrParticipation::getResourceId, resourceId);
        } else {
            wrapper.isNull(VppDrParticipation::getResourceId);
        }
        VppDrParticipation existing = participationMapper.selectOne(wrapper.last("LIMIT 1"));
        if (existing != null) {
            existing.setResourceId(resourceId);
            existing.setParticipateStatus(PARTICIPATE_ACCEPT);
            existing.setDeclaredCapacityKw(declaredCapacityKw);
            existing.setDeclaredAt(LocalDateTime.now());
            VppDrParticipationHelper.refreshCompletionRate(existing);
            VppAuditHelper.fillUpdate(existing);
            participationMapper.updateById(existing);
            return existing;
        }

        VppDrParticipation participation = new VppDrParticipation();
        participation.setEventId(invitation.getDrEventId());
        participation.setCustomerId(invitation.getCustomerId());
        participation.setResourceId(resourceId);
        participation.setParticipateStatus(PARTICIPATE_ACCEPT);
        participation.setDeclaredCapacityKw(declaredCapacityKw);
        participation.setDeclaredAt(LocalDateTime.now());
        VppDrParticipationHelper.refreshCompletionRate(participation);
        VppAuditHelper.fillCreate(participation);
        if (participation.getTenantId() == null && invitation.getTenantId() != null) {
            participation.setTenantId(invitation.getTenantId());
        }
        participationMapper.insert(participation);
        return participation;
    }

    private Long resolveResourceIdBySiteId(Long siteId) {
        if (siteId == null) {
            throw new BusinessException("邀约未关联站点，无法创建参与记录");
        }
        VppResourcePoint resourcePoint = resourcePointMapper.selectOne(new LambdaQueryWrapper<VppResourcePoint>()
                .eq(VppResourcePoint::getSiteId, siteId)
                .eq(VppResourcePoint::getDeleteFlag, VppAuditHelper.NOT_DELETED)
                .last("LIMIT 1"));
        if (resourcePoint == null) {
            throw new BusinessException("站点未配置资源点，无法参与");
        }
        return resourcePoint.getId();
    }

    private void syncParticipationToUn(Long eventId, boolean participate, List<VppDrParticipation> participations) {
        try {
            unIntegrationService.submitParticipation(eventId, participate, participations);
        } catch (Exception ex) {
            log.warn("同步 UN 邀约回复失败 eventId={}: {}", eventId, ex.getMessage());
        }
    }

    private VppDrInvitation requireInvitation(Long id) {
        VppDrInvitation invitation = invitationMapper.selectById(id);
        if (invitation == null || VppAuditHelper.isDeleted(invitation.getDeleteFlag())) {
            throw new BusinessException("邀约记录不存在");
        }
        return invitation;
    }

    private VppDrEvent requireEvent(Long id) {
        VppDrEvent event = eventMapper.selectById(id);
        if (event == null || VppAuditHelper.isDeleted(event.getDeleteFlag())) {
            throw new BusinessException("需求响应事件不存在");
        }
        return event;
    }

    private VppCustomer requireCustomer(Long id) {
        VppCustomer customer = customerMapper.selectById(id);
        if (customer == null || VppAuditHelper.isDeleted(customer.getDeleteFlag())) {
            throw new BusinessException("客户不存在");
        }
        return customer;
    }

    private VppCustomerContact requireContact(Long contactId, Long customerId) {
        VppCustomerContact contact = contactMapper.selectById(contactId);
        if (contact == null || VppAuditHelper.isDeleted(contact.getDeleteFlag())) {
            throw new BusinessException("联系人不存在");
        }
        if (!Objects.equals(contact.getCustomerId(), customerId)) {
            throw new BusinessException("联系人不属于该客户");
        }
        return contact;
    }

    private Long resolveSmsContactId(Long contactId, Long customerId) {
        if (contactId != null) {
            return requireContact(contactId, customerId).getId();
        }
        VppCustomerContact primary = contactMapper.selectOne(new LambdaQueryWrapper<VppCustomerContact>()
                .eq(VppCustomerContact::getCustomerId, customerId)
                .eq(VppCustomerContact::getIsPrimary, 1)
                .eq(VppCustomerContact::getDeleteFlag, VppAuditHelper.NOT_DELETED)
                .last("LIMIT 1"));
        if (primary != null) {
            return primary.getId();
        }
        VppCustomerContact any = contactMapper.selectOne(new LambdaQueryWrapper<VppCustomerContact>()
                .eq(VppCustomerContact::getCustomerId, customerId)
                .eq(VppCustomerContact::getDeleteFlag, VppAuditHelper.NOT_DELETED)
                .last("LIMIT 1"));
        return any != null ? any.getId() : null;
    }

    private void ensureInvitationUnique(DrInvitationRequest request, Long excludeId) {
        if (request == null || request.getDrEventId() == null) {
            return;
        }
        LambdaQueryWrapper<VppDrInvitation> wrapper = new LambdaQueryWrapper<VppDrInvitation>()
                .eq(VppDrInvitation::getDrEventId, request.getDrEventId())
                .eq(VppDrInvitation::getDeleteFlag, VppAuditHelper.NOT_DELETED);
        if (request.getSiteId() != null) {
            wrapper.eq(VppDrInvitation::getSiteId, request.getSiteId());
        } else if (request.getCustomerId() != null) {
            wrapper.eq(VppDrInvitation::getCustomerId, request.getCustomerId());
        }
        if (excludeId != null) {
            wrapper.ne(VppDrInvitation::getId, excludeId);
        }
        if (invitationMapper.selectCount(wrapper) > 0) {
            throw new BusinessException(request.getSiteId() != null
                    ? "该事件已存在该站点的邀约记录"
                    : "该事件已存在该企业的邀约记录");
        }
    }

    private void validateCreateRequest(DrInvitationRequest request) {
        if (request == null) {
            throw new BusinessException("请求不能为空");
        }
        if (request.getDrEventId() == null) {
            throw new BusinessException("需求响应事件不能为空");
        }
        if (request.getCustomerId() == null) {
            throw new BusinessException("企业不能为空");
        }
        if (request.getExecuteStartDate() == null || request.getExecuteEndDate() == null) {
            throw new BusinessException("执行日期不能为空");
        }
        if (request.getReplyDeadline() == null) {
            throw new BusinessException("邀约回复截止日期不能为空");
        }
        if (request.getDemandCapacityKw() == null || request.getDemandCapacityKw().compareTo(BigDecimal.ZERO) <= 0) {
            throw new BusinessException("需求容量必须大于0");
        }
        validateDateRange(request.getExecuteStartDate(), request.getExecuteEndDate());
    }

    private void validateDateRange(LocalDate start, LocalDate end) {
        if (start.isAfter(end)) {
            throw new BusinessException("执行开始日期不能晚于结束日期");
        }
    }

    private String appendRemark(String original, String addition) {
        if (!StringUtils.hasText(original)) {
            return addition;
        }
        return original + "; " + addition;
    }

    private List<DrInvitationVO> toVoList(List<VppDrInvitation> invitations) {
        if (CollectionUtils.isEmpty(invitations)) {
            return Collections.emptyList();
        }
        Set<Long> customerIds = invitations.stream()
                .map(VppDrInvitation::getCustomerId)
                .filter(Objects::nonNull)
                .collect(Collectors.toSet());
        Map<Long, VppCustomer> customerMap = customerIds.isEmpty()
                ? Collections.emptyMap()
                : customerMapper.selectBatchIds(customerIds).stream()
                .collect(Collectors.toMap(VppCustomer::getId, c -> c, (a, b) -> a));

        Set<Long> eventPks = invitations.stream()
                .map(VppDrInvitation::getDrEventId)
                .filter(Objects::nonNull)
                .collect(Collectors.toSet());
        Map<Long, VppDrEvent> eventMap = eventPks.isEmpty()
                ? Collections.emptyMap()
                : eventMapper.selectBatchIds(eventPks).stream()
                .filter(e -> !VppAuditHelper.isDeleted(e.getDeleteFlag()))
                .collect(Collectors.toMap(VppDrEvent::getId, e -> e, (a, b) -> a));

        return invitations.stream()
                .map(inv -> toVo(inv, customerMap.get(inv.getCustomerId()), null, eventMap.get(inv.getDrEventId())))
                .collect(Collectors.toList());
    }

    private DrInvitationVO toVo(VppDrInvitation invitation) {
        VppCustomer customer = customerMapper.selectById(invitation.getCustomerId());
        VppCustomerContact contact = invitation.getSmsContactId() != null
                ? contactMapper.selectById(invitation.getSmsContactId())
                : null;
        VppDrEvent event = invitation.getDrEventId() != null
                ? eventMapper.selectById(invitation.getDrEventId())
                : null;
        if (event != null && VppAuditHelper.isDeleted(event.getDeleteFlag())) {
            event = null;
        }
        return toVo(invitation, customer, contact, event);
    }

    private DrInvitationVO toVo(VppDrInvitation invitation, VppCustomer customer, VppCustomerContact contact,
                                VppDrEvent event) {
        DrInvitationVO vo = new DrInvitationVO();
        BeanUtils.copyProperties(invitation, vo);
        if (customer != null) {
            vo.setCustomerName(customer.getCustomerName());
        }
        if (contact != null) {
            vo.setSmsContactName(contact.getContactName());
            vo.setSmsContactPhone(contact.getContactPhone());
        }
        if (event != null) {
            vo.setEventId(event.getEventId());
        }
        return vo;
    }

    /**
     * 优先用请求显式响应完成率；否则按 实际响应容量 / 实际出清 计算。
     */
    private BigDecimal resolveCompletionRate(DrInvitationRequest request, VppDrInvitation invitation) {
        if (request != null && request.getResponseCompletionRate() != null) {
            return request.getResponseCompletionRate();
        }
        return VppDrParticipationHelper.calcResponseCompletionRate(
                invitation.getActualResponseCapacityKw(), invitation.getActualClearedCapacityKw());
    }

    /**
     * 优先用请求显式预估完成率；否则按 预估响应容量 / 实际出清 计算。
     */
    private BigDecimal resolveEstimatedCompletionRate(DrInvitationRequest request, VppDrInvitation invitation) {
        if (request != null && request.getEstimatedCompletionRate() != null) {
            return request.getEstimatedCompletionRate();
        }
        return VppDrParticipationHelper.calcEstimatedCompletionRate(
                invitation.getEstimatedResponseCapacityKw(), invitation.getActualClearedCapacityKw());
    }

    private String resolveTemplateCode(DrInvitationNotifyRequest request) {
        if (request != null && StringUtils.hasText(request.getTemplateCode())) {
            return request.getTemplateCode().trim();
        }
        if (StringUtils.hasText(smsProperties.getInvitationTemplateCode())) {
            return smsProperties.getInvitationTemplateCode().trim();
        }
        throw new BusinessException("短信模板未配置，请设置 vpp.sms.invitation-template-code 或在请求中传入 templateCode");
    }
}
