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.common.security.utils.SecurityUtils;
import com.usky.vpp.constant.VppDrEventStatus;
import com.usky.vpp.domain.VppBiddingConfig;
import com.usky.vpp.domain.VppCustomer;
import com.usky.vpp.domain.VppDevice;
import com.usky.vpp.domain.VppDrEvent;
import com.usky.vpp.domain.VppDrInvitation;
import com.usky.vpp.domain.VppResourcePoint;
import com.usky.vpp.domain.VppResponseDeviationPrice;
import com.usky.vpp.domain.VppSite;
import com.usky.vpp.mapper.VppCustomerMapper;
import com.usky.vpp.mapper.VppDeviceMapper;
import com.usky.vpp.mapper.VppDrEventMapper;
import com.usky.vpp.mapper.VppDrInvitationMapper;
import com.usky.vpp.mapper.VppResourcePointMapper;
import com.usky.vpp.mapper.VppSiteMapper;
import com.usky.vpp.service.VppBaselineService;
import com.usky.vpp.service.VppDrMonitorService;
import com.usky.vpp.service.VppSubsidyPredictionCalculator;
import com.usky.vpp.service.VppTsdbQueryService;
import com.usky.vpp.service.vo.BaselinePointVO;
import com.usky.vpp.service.vo.DrMonitorCurveVO;
import com.usky.vpp.service.vo.DrMonitorRecordVO;
import com.usky.vpp.service.vo.DrMonitorSiteResourceVO;
import com.usky.vpp.service.vo.DrMonitorSummaryVO;
import com.usky.vpp.service.vo.SiteBaselineVO;
import com.usky.vpp.service.vo.SubsidyPredictionCalcResult;
import com.usky.vpp.util.VppAuditHelper;
import com.usky.vpp.util.VppBaselineHelper;
import com.usky.vpp.util.VppDrParticipationHelper;
import com.usky.vpp.util.VppPageHelper;
import com.usky.vpp.util.VppResourceTypeHelper;
import org.springframework.beans.BeanUtils;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import org.springframework.util.CollectionUtils;
import org.springframework.util.StringUtils;

import java.math.BigDecimal;
import java.math.RoundingMode;
import java.time.LocalDate;
import java.time.LocalDateTime;
import java.time.LocalTime;
import java.time.format.DateTimeFormatter;
import java.time.format.DateTimeParseException;
import java.util.ArrayList;
import java.util.Collections;
import java.util.Comparator;
import java.util.LinkedHashMap;
import java.util.LinkedHashSet;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import java.util.Set;
import java.util.stream.Collectors;

/**
 * 需求响应监测（响应记录）
 * <p>统一口径：时间区间内执行中/已结束事件 → 参与且执行中/已结束的未删除邀约。</p>
 */
@Service
public class VppDrMonitorServiceImpl implements VppDrMonitorService {

    private static final int EVENT_STATUS_EXECUTING = VppDrEventStatus.EXECUTING;
    private static final int EVENT_STATUS_ENDED = VppDrEventStatus.ENDED;
    private static final int REPLY_ACCEPT = 1;
    private static final int SUPPORT_PEAK = 1;
    private static final int SCALE = 4;
    private static final int CURVE_BEFORE_HOURS = 2;
    private static final int CURVE_AFTER_HOURS = 1;
    private static final int CURVE_AFTER_MINUTES = 1;
    private static final DateTimeFormatter DATE_FMT = DateTimeFormatter.ofPattern("yyyy-MM-dd");
    private static final DateTimeFormatter DATE_TIME_FMT = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss");
    private static final DateTimeFormatter TIME_FMT = DateTimeFormatter.ofPattern("HH:mm");
    private static final BigDecimal ONE = BigDecimal.ONE;
    private static final BigDecimal HUNDRED = new BigDecimal("100");

    @Autowired
    private VppDrEventMapper eventMapper;
    @Autowired
    private VppDrInvitationMapper invitationMapper;
    @Autowired
    private VppCustomerMapper customerMapper;
    @Autowired
    private VppResourcePointMapper resourcePointMapper;
    @Autowired
    private VppDeviceMapper deviceMapper;
    @Autowired
    private VppSiteMapper siteMapper;
    @Autowired
    private VppBaselineService baselineService;
    @Autowired
    private VppTsdbQueryService tsdbQueryService;
    @Autowired
    private VppSubsidyPredictionCalculator subsidyPredictionCalculator;

    @Override
    public DrMonitorSummaryVO getSummary(String startDate, String endDate, List<Long> siteIds) {
        requireSiteIds(siteIds);
        Integer tenantId = SecurityUtils.getTenantId();
        List<VppDrInvitation> invitations = listMonitorInvitations(
                parseStart(startDate), parseEnd(endDate), siteIds, tenantId);

        DrMonitorSummaryVO vo = new DrMonitorSummaryVO();
        if (invitations.isEmpty()) {
            return vo;
        }
        int total = invitations.size();
        BigDecimal capacity = BigDecimal.ZERO;
        long qualified = 0L;
        for (VppDrInvitation invitation : invitations) {
            capacity = capacity.add(nz(invitation.getActualResponseCapacityKw()));
            if (isCompletionQualified(resolveEstimatedCompletionRate(invitation))) {
                qualified++;
            }
        }
        vo.setResponseCount((long) total);
        vo.setResponseCapacityKw(capacity.setScale(SCALE, RoundingMode.HALF_UP));
        vo.setHistoricalQualifiedRate(BigDecimal.valueOf(qualified)
                .divide(BigDecimal.valueOf(total), SCALE, RoundingMode.HALF_UP));
        return vo;
    }

    @Override
    public CommonPage<DrMonitorRecordVO> pageRecords(String startDate, String endDate,
                                                     List<Long> siteIds, Map<String, Object> params) {
        requireSiteIds(siteIds);
        Integer tenantId = SecurityUtils.getTenantId();
        List<VppDrInvitation> invitations = listMonitorInvitations(
                parseStart(startDate), parseEnd(endDate), siteIds, tenantId);
        Page<?> page = VppPageHelper.of(params);
        if (invitations.isEmpty()) {
            return new CommonPage<>(Collections.emptyList(), 0L, page.getCurrent(), page.getSize());
        }

        Set<Long> eventIds = invitations.stream()
                .map(VppDrInvitation::getDrEventId)
                .filter(Objects::nonNull)
                .collect(Collectors.toSet());
        Map<Long, VppDrEvent> eventMap = loadEventMapByIds(eventIds, tenantId);
        Map<Long, VppCustomer> customerMap = loadCustomerMap(invitations);
        Map<Long, VppSite> siteMap = loadSiteMap(invitations, tenantId);
        List<VppResponseDeviationPrice> adjustments =
                subsidyPredictionCalculator.loadAdjustments(tenantId);
        List<VppBiddingConfig> biddingConfigs =
                subsidyPredictionCalculator.loadBiddingConfigs(tenantId);

        List<DrMonitorRecordVO> records = invitations.stream()
                .sorted(Comparator.comparing(
                        i -> eventMap.get(i.getDrEventId()) == null
                                ? null : eventMap.get(i.getDrEventId()).getStartTime(),
                        Comparator.nullsLast(Comparator.reverseOrder())))
                .map(inv -> toRecordVo(inv, eventMap.get(inv.getDrEventId()),
                        customerMap.get(inv.getCustomerId()), siteMap.get(inv.getSiteId()),
                        adjustments, biddingConfigs))
                .collect(Collectors.toList());

        long current = page.getCurrent();
        long size = page.getSize();
        int from = (int) Math.min((current - 1) * size, records.size());
        int to = (int) Math.min(from + size, records.size());
        List<DrMonitorRecordVO> pageRecords = from >= records.size()
                ? Collections.emptyList()
                : records.subList(from, to);
        return new CommonPage<>(pageRecords, (long) records.size(), current, size);
    }

    @Override
    public DrMonitorCurveVO getCurve(Long invitationId) {
        if (invitationId == null) {
            throw new BusinessException("邀约ID不能为空");
        }
        Integer tenantId = SecurityUtils.getTenantId();
        VppDrInvitation invitation = invitationMapper.selectById(invitationId);
        if (invitation == null || VppAuditHelper.isDeleted(invitation.getDeleteFlag())) {
            throw new BusinessException("邀约记录不存在");
        }
        if (tenantId != null && invitation.getTenantId() != null && !tenantId.equals(invitation.getTenantId())) {
            throw new BusinessException("无权操作其他租户的邀约");
        }
        if (invitation.getSiteId() == null || invitation.getSiteId() <= 0) {
            throw new BusinessException("邀约未关联站点");
        }

        VppDrEvent event = invitation.getDrEventId() != null
                ? eventMapper.selectById(invitation.getDrEventId()) : null;
        if (event != null && VppAuditHelper.isDeleted(event.getDeleteFlag())) {
            event = null;
        }
        LocalDateTime responseStart = resolveInvitationResponseStart(invitation, event);
        LocalDateTime responseEnd = resolveInvitationResponseEnd(invitation, event);
        LocalDateTime queryStart = responseStart.minusHours(CURVE_BEFORE_HOURS);
        LocalDateTime queryEnd = responseEnd.plusHours(CURVE_AFTER_HOURS).plusMinutes(CURVE_AFTER_MINUTES);
        if (queryEnd.isBefore(queryStart)) {
            throw new BusinessException("响应时段无效");
        }

        SiteBaselineVO baseline = baselineService.getSiteBaseline(
                invitation.getSiteId(), responseStart.format(DATE_TIME_FMT));
        BigDecimal actualKw = nz(invitation.getActualResponseCapacityKw()).setScale(SCALE, RoundingMode.HALF_UP);
        Map<String, BigDecimal> actualLoadByTime = queryActualLoadByTime(
                invitation.getSiteId(), tenantId, queryStart, queryEnd);

        DrMonitorCurveVO curve = new DrMonitorCurveVO();
        curve.setId(invitation.getId());
        curve.setSiteId(invitation.getSiteId());
        curve.setActualResponseCapacityKw(actualKw);
        if (invitation.getExecuteStartDate() != null) {
            curve.setResponseDate(invitation.getExecuteStartDate().format(DATE_FMT));
        } else if (event != null && event.getStartTime() != null) {
            curve.setResponseDate(event.getStartTime().toLocalDate().format(DATE_FMT));
        }
        if (event != null) {
            curve.setEventId(event.getEventId());
        }
        if (baseline != null && StringUtils.hasText(baseline.getDataSource())) {
            curve.setDataSource(baseline.getDataSource());
        } else if (!actualLoadByTime.isEmpty()) {
            curve.setDataSource("tsdb");
        }

        Map<String, BigDecimal> baselineByTime = toBaselineByTime(baseline);
        int interval = VppBaselineHelper.INTERVAL_MINUTES;
        LocalDateTime cursor = alignToInterval(queryStart, interval);
        while (!cursor.isAfter(queryEnd)) {
            String timeKey = cursor.toLocalTime().format(TIME_FMT);
            DrMonitorCurveVO.Point curvePoint = new DrMonitorCurveVO.Point();
            curvePoint.setTime(timeKey);
            curvePoint.setBaselineKw(scaleKw(baselineByTime.get(timeKey)));
            curvePoint.setActualLoadKw(scaleKw(actualLoadByTime.get(timeKey)));
            curvePoint.setActualResponseCapacityKw(actualKw);
            curve.getPoints().add(curvePoint);
            cursor = cursor.plusMinutes(interval);
        }
        return curve;
    }

    @Override
    public List<DrMonitorSiteResourceVO> listEventSitePeakResources(Long eventId) {
        if (eventId == null) {
            throw new BusinessException("事件ID不能为空");
        }
        Integer tenantId = SecurityUtils.getTenantId();
        VppDrEvent event = eventMapper.selectById(eventId);
        if (event == null || VppAuditHelper.isDeleted(event.getDeleteFlag())) {
            throw new BusinessException("需求响应事件不存在");
        }
        if (tenantId != null && event.getTenantId() != null && !tenantId.equals(event.getTenantId())) {
            throw new BusinessException("无权操作其他租户的事件");
        }

        List<VppDrInvitation> invitations = invitationMapper.selectList(new LambdaQueryWrapper<VppDrInvitation>()
                .eq(VppDrInvitation::getDrEventId, eventId)
                .eq(VppDrInvitation::getDeleteFlag, VppAuditHelper.NOT_DELETED)
                .eq(tenantId != null, VppDrInvitation::getTenantId, tenantId));
        Set<Long> siteIds = invitations.stream()
                .map(VppDrInvitation::getSiteId)
                .filter(id -> id != null && id > 0)
                .collect(Collectors.toCollection(LinkedHashSet::new));
        if (siteIds.isEmpty()) {
            return Collections.emptyList();
        }

        Map<Long, VppSite> siteMap = siteMapper.selectList(new LambdaQueryWrapper<VppSite>()
                        .in(VppSite::getId, siteIds)
                        .eq(VppSite::getDeleteFlag, VppAuditHelper.NOT_DELETED)
                        .eq(tenantId != null, VppSite::getTenantId, tenantId))
                .stream()
                .collect(Collectors.toMap(VppSite::getId, s -> s, (a, b) -> a));

        List<VppResourcePoint> peakResources = listPeakResources(new ArrayList<>(siteIds), tenantId);
        Map<Long, List<VppResourcePoint>> resourcesBySite = peakResources.stream()
                .filter(r -> r.getSiteId() != null)
                .collect(Collectors.groupingBy(VppResourcePoint::getSiteId));

        List<DrMonitorSiteResourceVO> result = new ArrayList<>();
        for (Long siteId : siteIds) {
            VppSite site = siteMap.get(siteId);
            if (site == null) {
                continue;
            }
            DrMonitorSiteResourceVO vo = new DrMonitorSiteResourceVO();
            vo.setSiteId(site.getId());
            vo.setSiteCode(site.getSiteCode());
            vo.setSiteName(site.getSiteName());
            List<VppResourcePoint> siteResources = resourcesBySite.getOrDefault(siteId, Collections.emptyList())
                    .stream()
                    .sorted(Comparator.comparing(VppResourcePoint::getId, Comparator.nullsLast(Long::compareTo)))
                    .collect(Collectors.toList());
            for (VppResourcePoint resource : siteResources) {
                vo.getResources().add(toResourceItem(resource));
            }
            result.add(vo);
        }
        return result;
    }

    private static DrMonitorSiteResourceVO.ResourceItem toResourceItem(VppResourcePoint resource) {
        DrMonitorSiteResourceVO.ResourceItem item = new DrMonitorSiteResourceVO.ResourceItem();
        item.setResourceId(resource.getId());
        item.setResourceCode(resource.getResourceCode());
        item.setResourceName(resource.getResourceName());
        item.setResourceType(resource.getResourceType());
        item.setResourceTypeLabel(VppResourceTypeHelper.resourceTypeLabel(resource.getResourceType()));
        item.setCapacityKw(resource.getCapacityKw());
        item.setIsControl(resource.getIsControl());
        item.setIsSupportPeak(resource.getIsSupportPeak());
        item.setMaxUpKw(resource.getMaxUpKw());
        item.setMinDownKw(resource.getMinDownKw());
        item.setIsSupportFm(resource.getIsSupportFm());
        item.setRemark(resource.getRemark());
        return item;
    }

    // ---------- loaders ----------

    private void requireSiteIds(List<Long> siteIds) {
        if (CollectionUtils.isEmpty(siteIds)) {
            throw new BusinessException("站点ID不能为空");
        }
    }

    /**
     * 时间区间内事件开始、结束均落在区间，且状态为执行中/已结束；再关联参与且执行中/已结束的未删除邀约。
     */
    private List<VppDrInvitation> listMonitorInvitations(LocalDateTime rangeStart, LocalDateTime rangeEnd,
                                                         List<Long> siteIds, Integer tenantId) {
        List<VppDrEvent> events = eventMapper.selectList(new LambdaQueryWrapper<VppDrEvent>()
                .eq(VppDrEvent::getDeleteFlag, VppAuditHelper.NOT_DELETED)
                .in(VppDrEvent::getEventStatus, EVENT_STATUS_EXECUTING, EVENT_STATUS_ENDED)
                .ge(VppDrEvent::getStartTime, rangeStart)
                .le(VppDrEvent::getEndTime, rangeEnd)
                .isNotNull(VppDrEvent::getStartTime)
                .isNotNull(VppDrEvent::getEndTime)
                .eq(tenantId != null, VppDrEvent::getTenantId, tenantId));
        if (events.isEmpty()) {
            return Collections.emptyList();
        }
        Set<Long> eventIds = events.stream().map(VppDrEvent::getId).collect(Collectors.toSet());
        return invitationMapper.selectList(new LambdaQueryWrapper<VppDrInvitation>()
                .eq(VppDrInvitation::getDeleteFlag, VppAuditHelper.NOT_DELETED)
                .in(VppDrInvitation::getDrEventId, eventIds)
                .in(VppDrInvitation::getSiteId, siteIds)
                .eq(VppDrInvitation::getReplyStatus, REPLY_ACCEPT)
                .in(VppDrInvitation::getResponseStatus, EVENT_STATUS_EXECUTING, EVENT_STATUS_ENDED)
                .eq(tenantId != null, VppDrInvitation::getTenantId, tenantId));
    }

    private Map<Long, VppDrEvent> loadEventMapByIds(Set<Long> eventIds, Integer tenantId) {
        if (CollectionUtils.isEmpty(eventIds)) {
            return Collections.emptyMap();
        }
        return eventMapper.selectList(new LambdaQueryWrapper<VppDrEvent>()
                        .in(VppDrEvent::getId, eventIds)
                        .eq(VppDrEvent::getDeleteFlag, VppAuditHelper.NOT_DELETED)
                        .eq(tenantId != null, VppDrEvent::getTenantId, tenantId))
                .stream()
                .collect(Collectors.toMap(VppDrEvent::getId, e -> e, (a, b) -> a));
    }

    private Map<Long, VppCustomer> loadCustomerMap(List<VppDrInvitation> invitations) {
        Set<Long> customerIds = invitations.stream()
                .map(VppDrInvitation::getCustomerId)
                .filter(Objects::nonNull)
                .collect(Collectors.toSet());
        if (customerIds.isEmpty()) {
            return Collections.emptyMap();
        }
        return customerMapper.selectList(new LambdaQueryWrapper<VppCustomer>()
                        .in(VppCustomer::getId, customerIds)
                        .eq(VppCustomer::getDeleteFlag, VppAuditHelper.NOT_DELETED))
                .stream()
                .collect(Collectors.toMap(VppCustomer::getId, c -> c, (a, b) -> a));
    }

    private Map<Long, VppSite> loadSiteMap(List<VppDrInvitation> invitations, Integer tenantId) {
        Set<Long> siteIds = invitations.stream()
                .map(VppDrInvitation::getSiteId)
                .filter(id -> id != null && id > 0)
                .collect(Collectors.toSet());
        if (siteIds.isEmpty()) {
            return Collections.emptyMap();
        }
        return siteMapper.selectList(new LambdaQueryWrapper<VppSite>()
                        .in(VppSite::getId, siteIds)
                        .eq(VppSite::getDeleteFlag, VppAuditHelper.NOT_DELETED)
                        .eq(tenantId != null, VppSite::getTenantId, tenantId))
                .stream()
                .collect(Collectors.toMap(VppSite::getId, s -> s, (a, b) -> a));
    }

    private DrMonitorRecordVO toRecordVo(VppDrInvitation invitation, VppDrEvent event,
                                         VppCustomer customer, VppSite site,
                                         List<VppResponseDeviationPrice> adjustments,
                                         List<VppBiddingConfig> biddingConfigs) {
        DrMonitorRecordVO vo = new DrMonitorRecordVO();
        if (event != null) {
            BeanUtils.copyProperties(event, vo);
        }
        vo.setInvitationId(invitation.getId());
        vo.setCustomerId(invitation.getCustomerId());
        if (customer != null) {
            vo.setCustomerName(customer.getCustomerName());
        }
        vo.setSiteId(invitation.getSiteId());
        if (site != null) {
            vo.setSiteName(site.getSiteName());
        }
        vo.setTransactionType(invitation.getTransactionType());
        vo.setEstimatedResponseCapacityKw(invitation.getEstimatedResponseCapacityKw());
        vo.setClearedCapacityKw(invitation.getActualClearedCapacityKw());
        BigDecimal estimatedCompletionRate = resolveEstimatedCompletionRate(invitation);
        vo.setEstimatedCompletionRate(estimatedCompletionRate);
        vo.setQualified(isCompletionQualified(estimatedCompletionRate));
        SubsidyPredictionCalcResult subsidy = subsidyPredictionCalculator.calculate(
                invitation, event, adjustments, biddingConfigs);
        vo.setEstimatedSubsidyAmount(subsidy.getEstimatedSubsidyAmount());
        vo.setSubsidyPrice(subsidy.getSubsidyPrice());
        return vo;
    }

    private LocalDateTime resolveInvitationResponseStart(VppDrInvitation invitation, VppDrEvent event) {
        if (invitation.getExecuteStartDate() != null) {
            LocalTime timeOfDay = (event != null && event.getStartTime() != null)
                    ? event.getStartTime().toLocalTime() : LocalTime.MIN;
            return LocalDateTime.of(invitation.getExecuteStartDate(), timeOfDay);
        }
        if (event != null && event.getStartTime() != null) {
            return event.getStartTime();
        }
        throw new BusinessException("邀约缺少响应时间");
    }

    private LocalDateTime resolveInvitationResponseEnd(VppDrInvitation invitation, VppDrEvent event) {
        if (invitation.getExecuteEndDate() != null) {
            LocalTime timeOfDay = (event != null && event.getEndTime() != null)
                    ? event.getEndTime().toLocalTime() : LocalTime.of(23, 59, 59);
            return LocalDateTime.of(invitation.getExecuteEndDate(), timeOfDay);
        }
        if (event != null && event.getEndTime() != null) {
            return event.getEndTime();
        }
        throw new BusinessException("邀约缺少响应结束时间");
    }

    /**
     * 按站点可控设备查询实际负荷，时间范围为响应开始前 2 小时至结束后 1 小时零 1 分钟。
     * <p>调用方式对齐 {@link VppTsdbQueryService#queryBaselineAvgByTimePoint}：
     * 设备 UUID + 时段 + {@link VppBaselineHelper#POWER_METRICS}，再按 interval 聚合成 HH:mm。</p>
     */
    private Map<String, BigDecimal> queryActualLoadByTime(Long siteId, Integer tenantId,
                                                          LocalDateTime queryStart, LocalDateTime queryEnd) {
        List<String> deviceUuids = listSiteDeviceUuids(siteId, tenantId);
        if (deviceUuids.isEmpty()) {
            return Collections.emptyMap();
        }
        return VppBaselineHelper.buildActualLoadByTimePoint(
                tsdbQueryService.queryDeviceMetricHistory(
                        deviceUuids, queryStart, queryEnd, VppBaselineHelper.POWER_METRICS),
                VppBaselineHelper.INTERVAL_MINUTES);
    }

    private List<String> listSiteDeviceUuids(Long siteId, Integer tenantId) {
        List<VppResourcePoint> resources = listPeakResources(Collections.singletonList(siteId), tenantId);
        if (CollectionUtils.isEmpty(resources)) {
            return Collections.emptyList();
        }
        Set<Long> deviceIds = resources.stream()
                .map(VppResourcePoint::getDeviceId)
                .filter(Objects::nonNull)
                .collect(Collectors.toSet());
        if (deviceIds.isEmpty()) {
            return Collections.emptyList();
        }
        Set<String> uuids = new LinkedHashSet<>();
        for (VppDevice device : deviceMapper.selectBatchIds(deviceIds)) {
            if (device == null || VppAuditHelper.isDeleted(device.getDeleteFlag())) {
                continue;
            }
            if (StringUtils.hasText(device.getDeviceUuid())) {
                uuids.add(device.getDeviceUuid().trim());
            }
        }
        return new ArrayList<>(uuids);
    }

    private static Map<String, BigDecimal> toBaselineByTime(SiteBaselineVO baseline) {
        if (baseline == null || CollectionUtils.isEmpty(baseline.getPoints())) {
            return Collections.emptyMap();
        }
        Map<String, BigDecimal> result = new LinkedHashMap<>();
        for (BaselinePointVO point : baseline.getPoints()) {
            if (!StringUtils.hasText(point.getTime())) {
                continue;
            }
            result.put(point.getTime(), point.getPredictedBaselineKw());
        }
        return result;
    }

    private static LocalDateTime alignToInterval(LocalDateTime time, int intervalMinutes) {
        int minute = time.getMinute() / intervalMinutes * intervalMinutes;
        return time.withMinute(minute).withSecond(0).withNano(0);
    }

    private static BigDecimal scaleKw(BigDecimal value) {
        return value == null ? null : value.setScale(SCALE, RoundingMode.HALF_UP);
    }

    private List<VppResourcePoint> listPeakResources(List<Long> siteIds, Integer tenantId) {
        return resourcePointMapper.selectList(new LambdaQueryWrapper<VppResourcePoint>()
                .eq(VppResourcePoint::getDeleteFlag, VppAuditHelper.NOT_DELETED)
                .eq(VppResourcePoint::getIsSupportPeak, SUPPORT_PEAK)
                .in(VppResourcePoint::getSiteId, siteIds)
                .eq(tenantId != null, VppResourcePoint::getTenantId, tenantId)
                .isNotNull(VppResourcePoint::getSiteId));
    }

    static boolean isCompletionQualified(BigDecimal rate) {
        if (rate == null) {
            return false;
        }
        BigDecimal scaled = rate.setScale(2, RoundingMode.HALF_UP);
        if (scaled.compareTo(ONE) > 0) {
            return scaled.compareTo(HUNDRED) >= 0;
        }
        return scaled.compareTo(ONE) >= 0;
    }

    private static BigDecimal resolveEstimatedCompletionRate(VppDrInvitation invitation) {
        if (invitation == null) {
            return null;
        }
        if (invitation.getEstimatedCompletionRate() != null) {
            return invitation.getEstimatedCompletionRate();
        }
        return VppDrParticipationHelper.calcEstimatedCompletionRate(
                invitation.getEstimatedResponseCapacityKw(), invitation.getActualClearedCapacityKw());
    }

    private static LocalDateTime parseStart(String date) {
        if (!StringUtils.hasText(date)) {
            throw new BusinessException("开始日期不能为空");
        }
        try {
            return LocalDate.parse(date.trim(), DATE_FMT).atStartOfDay();
        } catch (DateTimeParseException ex) {
            throw new BusinessException("开始日期格式无效，应为 yyyy-MM-dd");
        }
    }

    private static LocalDateTime parseEnd(String date) {
        if (!StringUtils.hasText(date)) {
            throw new BusinessException("结束日期不能为空");
        }
        try {
            return LocalDate.parse(date.trim(), DATE_FMT).atTime(LocalTime.MAX);
        } catch (DateTimeParseException ex) {
            throw new BusinessException("结束日期格式无效，应为 yyyy-MM-dd");
        }
    }

    private static BigDecimal nz(BigDecimal value) {
        return value == null ? BigDecimal.ZERO : value;
    }
}
