package com.usky.vpp.service.impl;

import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
import com.usky.common.security.utils.SecurityUtils;
import com.usky.vpp.constant.VppDrEventStatus;
import com.usky.vpp.constant.VppSocialBenefitConstants;
import com.usky.vpp.domain.DmpDeviceStatus;
import com.usky.vpp.domain.VppCustomer;
import com.usky.vpp.domain.VppDevice;
import com.usky.vpp.domain.VppDrEvaluation;
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.domain.VppSite;
import com.usky.vpp.mapper.DmpDeviceStatusMapper;
import com.usky.vpp.mapper.VppCustomerMapper;
import com.usky.vpp.mapper.VppDeviceMapper;
import com.usky.vpp.mapper.VppDrEvaluationMapper;
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.mapper.VppSiteMapper;
import com.usky.vpp.service.VppCapabilityEvalService;
import com.usky.vpp.service.VppDashboardService;
import com.usky.vpp.service.vo.CapabilityEvalSummaryVO;
import com.usky.vpp.service.vo.CapabilityLoadCurveVO;
import com.usky.vpp.service.vo.CapabilityLoadPointVO;
import com.usky.vpp.service.vo.DashboardCurveVO;
import com.usky.vpp.service.vo.DashboardMapVO;
import com.usky.vpp.service.vo.DashboardSummaryVO;
import com.usky.vpp.util.VppAuditHelper;
import com.usky.vpp.util.VppCapabilityEvalHelper;
import com.usky.vpp.util.VppDrParticipationHelper;
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.Year;
import java.time.temporal.ChronoUnit;
import java.util.ArrayList;
import java.util.Collections;
import java.util.Comparator;
import java.util.HashMap;
import java.util.HashSet;
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>租户隔离；DR 相关指标仅统计 event_status=已结束 的历史事件。</p>
 */
@Service
public class VppDashboardServiceImpl implements VppDashboardService {

    private static final int EVENT_STATUS_ENDED = VppDrEventStatus.ENDED;
    private static final int EVENT_TYPE_PEAK = 1;
    private static final int EVENT_TYPE_VALLEY = 2;
    private static final int REPLY_ACCEPT = 1;
    /** dmp_device_status.device_status：1 在线，2 离线 */
    private static final int DMP_STATUS_ONLINE = 1;
    private static final int IS_CONTROL = 1;
    private static final int IS_WINNING = 1;
    private static final int SCALE = 4;
    private static final int MONEY_SCALE = 2;
    private static final BigDecimal KW_PER_MW = new BigDecimal("1000");

    @Autowired
    private VppSiteMapper siteMapper;
    @Autowired
    private VppCustomerMapper customerMapper;
    @Autowired
    private VppResourcePointMapper resourcePointMapper;
    @Autowired
    private VppDeviceMapper deviceMapper;
    @Autowired
    private DmpDeviceStatusMapper dmpDeviceStatusMapper;
    @Autowired
    private VppDrEventMapper eventMapper;
    @Autowired
    private VppDrEvaluationMapper evaluationMapper;
    @Autowired
    private VppDrInvitationMapper invitationMapper;
    @Autowired
    private VppDrParticipationMapper participationMapper;
    @Autowired
    private VppCapabilityEvalService capabilityEvalService;

    @Override
    public DashboardSummaryVO getSummary() {
        Integer tenantId = SecurityUtils.getTenantId();
        DashboardSummaryVO vo = new DashboardSummaryVO();

        List<VppSite> sites = listSites(tenantId);
        List<VppResourcePoint> resources = listResources(tenantId);
        List<VppDevice> devices = listDevices(tenantId);
        List<VppDrEvent> endedEvents = listEndedEvents(tenantId);
        Map<Long, VppDrEvaluation> evaluationMap = loadEvaluationMap(endedEvents);
        List<VppDrInvitation> endedInvitations = listInvitationsByEvents(endedEvents, tenantId);
        Map<Long, List<VppDrInvitation>> invitationsByEvent = endedInvitations.stream()
                .filter(i -> i.getDrEventId() != null)
                .collect(Collectors.groupingBy(VppDrInvitation::getDrEventId));

        fillAccess(vo.getAccess(), sites, resources);
        fillBasic(vo.getBasic(), tenantId, resources, endedEvents, evaluationMap, invitationsByEvent);
        fillDeviceStatus(vo.getDeviceStatus(), devices);
        fillSubsidy(vo.getSubsidy(), evaluationMap);
        fillYearRegulation(vo.getYearRegulation(), tenantId, endedEvents, evaluationMap, invitationsByEvent);
        fillSocialBenefit(vo.getSocialBenefit(), endedEvents, evaluationMap, invitationsByEvent);
        fillEarningsRank(vo, sites, resources, endedEvents, evaluationMap, endedInvitations);
        return vo;
    }

    @Override
    public DashboardCurveVO getCurve(String date) {
        CapabilityLoadCurveVO source = capabilityEvalService.getLoadCurve(null, date);
        DashboardCurveVO curve = new DashboardCurveVO();
        curve.setStatDate(source.getDate());
        curve.setDataSource(source.getDataSource());
        if (!CollectionUtils.isEmpty(source.getPoints())) {
            List<DashboardCurveVO.Point> points = new ArrayList<>(source.getPoints().size());
            for (CapabilityLoadPointVO p : source.getPoints()) {
                DashboardCurveVO.Point point = new DashboardCurveVO.Point();
                point.setTime(p.getTime());
                point.setBaselineKw(p.getBaselineLoadKw());
                point.setLoadKw(p.getActualLoadKw());
                points.add(point);
            }
            curve.setPoints(points);
        }
        return curve;
    }

    @Override
    public DashboardMapVO getMap() {
        Integer tenantId = SecurityUtils.getTenantId();
        List<VppSite> sites = listSites(tenantId);
        List<VppResourcePoint> resources = listResources(tenantId);
        Map<Long, BigDecimal> capacityBySite = new HashMap<>();
        for (VppResourcePoint resource : resources) {
            if (resource.getSiteId() == null) {
                continue;
            }
            BigDecimal cap = nz(resource.getCapacityKw());
            capacityBySite.merge(resource.getSiteId(), cap, BigDecimal::add);
        }

        DashboardMapVO map = new DashboardMapVO();
        for (VppSite site : sites) {
            if (site.getLongitude() == null || site.getLatitude() == null) {
                continue;
            }
            DashboardMapVO.SitePoint point = new DashboardMapVO.SitePoint();
            point.setSiteId(site.getId());
            point.setSiteName(site.getSiteName());
            point.setLongitude(site.getLongitude());
            point.setLatitude(site.getLatitude());
            point.setProvince(site.getProvince());
            point.setCity(site.getCity());
            point.setCapacityKw(capacityBySite.getOrDefault(site.getId(), BigDecimal.ZERO)
                    .setScale(SCALE, RoundingMode.HALF_UP));
            map.getSites().add(point);
        }
        return map;
    }

    // ---------- access / basic / device ----------

    private void fillAccess(DashboardSummaryVO.Access access, List<VppSite> sites, List<VppResourcePoint> resources) {
        access.setSiteCount((long) sites.size());
        BigDecimal total = BigDecimal.ZERO;
        BigDecimal pv = BigDecimal.ZERO;
        BigDecimal ess = BigDecimal.ZERO;
        BigDecimal adjustable = BigDecimal.ZERO;
        for (VppResourcePoint resource : resources) {
            BigDecimal cap = nz(resource.getCapacityKw());
            total = total.add(cap);
            String category = VppCapabilityEvalHelper.resolveCategory(resource.getResourceType());
            if (VppCapabilityEvalHelper.CATEGORY_GENERATION.equals(category)) {
                pv = pv.add(cap);
            } else if (VppCapabilityEvalHelper.CATEGORY_STORAGE.equals(category)) {
                ess = ess.add(cap);
            }
            if (Objects.equals(resource.getIsControl(), IS_CONTROL)) {
                adjustable = adjustable.add(cap);
            }
        }
        access.setTotalCapacityKw(total.setScale(SCALE, RoundingMode.HALF_UP));
        access.setPvCapacityKw(pv.setScale(SCALE, RoundingMode.HALF_UP));
        access.setEssCapacityKw(ess.setScale(SCALE, RoundingMode.HALF_UP));
        access.setAdjustableCapacityKw(adjustable.setScale(SCALE, RoundingMode.HALF_UP));
    }

    private void fillBasic(DashboardSummaryVO.Basic basic, Integer tenantId,
                           List<VppResourcePoint> resources,
                           List<VppDrEvent> endedEvents, Map<Long, VppDrEvaluation> evaluationMap,
                           Map<Long, List<VppDrInvitation>> invitationsByEvent) {
        long customerCount = customerMapper.selectCount(
                new LambdaQueryWrapper<VppCustomer>()
                        .eq(VppCustomer::getDeleteFlag, VppAuditHelper.NOT_DELETED)
                        .eq(tenantId != null, VppCustomer::getTenantId, tenantId));
        basic.setCustomerCount(customerCount);

        BigDecimal loadCapacity = BigDecimal.ZERO;
        Set<Long> controlDeviceIds = new HashSet<>();
        long controlResourceWithoutDevice = 0L;
        BigDecimal maxUp = BigDecimal.ZERO;
        BigDecimal maxDown = BigDecimal.ZERO;
        for (VppResourcePoint resource : resources) {
            if (VppCapabilityEvalHelper.CATEGORY_LOAD.equals(
                    VppCapabilityEvalHelper.resolveCategory(resource.getResourceType()))) {
                loadCapacity = loadCapacity.add(nz(resource.getCapacityKw()));
            }
            if (Objects.equals(resource.getIsControl(), IS_CONTROL)) {
                if (resource.getDeviceId() != null) {
                    controlDeviceIds.add(resource.getDeviceId());
                } else {
                    controlResourceWithoutDevice++;
                }
                maxUp = maxUp.add(nz(resource.getMaxUpKw()));
                maxDown = maxDown.add(nz(resource.getMinDownKw()));
            }
        }
        basic.setLoadCapacityKw(loadCapacity.setScale(SCALE, RoundingMode.HALF_UP));
        basic.setAdjustableDeviceCount(controlDeviceIds.size() + controlResourceWithoutDevice);

        BigDecimal peak = BigDecimal.ZERO;
        BigDecimal valley = BigDecimal.ZERO;
        for (VppDrEvent event : endedEvents) {
            BigDecimal responseKw = resolveEventActualResponseKw(
                    event, evaluationMap.get(event.getId()), invitationsByEvent.get(event.getId()));
            if (Objects.equals(event.getEventType(), EVENT_TYPE_PEAK)) {
                peak = peak.add(responseKw);
            } else if (Objects.equals(event.getEventType(), EVENT_TYPE_VALLEY)) {
                valley = valley.add(responseKw);
            }
        }
        basic.setPeakShaveTotalKw(peak.setScale(SCALE, RoundingMode.HALF_UP));
        basic.setValleyFillTotalKw(valley.setScale(SCALE, RoundingMode.HALF_UP));

        CapabilityEvalSummaryVO capability = capabilityEvalService.getSummary(null);
        if (capability != null) {
            basic.setMaxUpKw(nz(capability.getMaxUpKw()).setScale(SCALE, RoundingMode.HALF_UP));
            basic.setMaxDownKw(nz(capability.getMaxDownKw()).setScale(SCALE, RoundingMode.HALF_UP));
        } else {
            basic.setMaxUpKw(maxUp.setScale(SCALE, RoundingMode.HALF_UP));
            basic.setMaxDownKw(maxDown.setScale(SCALE, RoundingMode.HALF_UP));
        }
    }

    /**
     * 设备状态：从 vpp_device 取 device_uuid，再查 dmp_device_status（1在线 / 2离线）。
     * 无 uuid 或 dmp 无记录的设备计为离线；dmp 无故障态，fault 固定为 0。
     */
    private void fillDeviceStatus(DashboardSummaryVO.DeviceStatus status, List<VppDevice> devices) {
        status.setTotal((long) devices.size());
        status.setFault(0L);
        if (CollectionUtils.isEmpty(devices)) {
            status.setOnline(0L);
            status.setOffline(0L);
            return;
        }

        List<String> deviceUuids = devices.stream()
                .map(VppDevice::getDeviceUuid)
                .filter(StringUtils::hasText)
                .map(String::trim)
                .distinct()
                .collect(Collectors.toList());

        Map<String, Integer> statusByUuid = Collections.emptyMap();
        if (!deviceUuids.isEmpty()) {
            statusByUuid = dmpDeviceStatusMapper.selectList(
                            new LambdaQueryWrapper<DmpDeviceStatus>()
                                    .select(DmpDeviceStatus::getDeviceUuid, DmpDeviceStatus::getDeviceStatus)
                                    .in(DmpDeviceStatus::getDeviceUuid, deviceUuids))
                    .stream()
                    .filter(s -> StringUtils.hasText(s.getDeviceUuid()))
                    .collect(Collectors.toMap(DmpDeviceStatus::getDeviceUuid, DmpDeviceStatus::getDeviceStatus, (a, b) -> a));
        }

        long online = 0;
        long offline = 0;
        for (VppDevice device : devices) {
            String uuid = StringUtils.hasText(device.getDeviceUuid()) ? device.getDeviceUuid().trim() : null;
            Integer dmpStatus = uuid == null ? null : statusByUuid.get(uuid);
            if (Objects.equals(dmpStatus, DMP_STATUS_ONLINE)) {
                online++;
            } else {
                // 2离线，或无 uuid / dmp 无记录
                offline++;
            }
        }
        status.setOnline(online);
        status.setOffline(offline);
    }

    private void fillSubsidy(DashboardSummaryVO.Subsidy subsidy, Map<Long, VppDrEvaluation> evaluationMap) {
        BigDecimal total = BigDecimal.ZERO;
        for (VppDrEvaluation evaluation : evaluationMap.values()) {
            total = total.add(nz(evaluation.getSubsidyAmount()));
        }
        subsidy.setTotalAmount(total.setScale(MONEY_SCALE, RoundingMode.HALF_UP));
    }

    private void fillYearRegulation(DashboardSummaryVO.YearRegulation yearRegulation, Integer tenantId,
                                    List<VppDrEvent> endedEvents, Map<Long, VppDrEvaluation> evaluationMap,
                                    Map<Long, List<VppDrInvitation>> invitationsByEvent) {
        int year = Year.now().getValue();
        yearRegulation.setYear(year);
        LocalDateTime yearStart = LocalDate.of(year, 1, 1).atStartOfDay();
        LocalDateTime yearEnd = LocalDate.of(year, 12, 31).atTime(23, 59, 59);

        List<VppDrEvent> yearEvents = endedEvents.stream()
                .filter(e -> e.getStartTime() != null
                        && !e.getStartTime().isBefore(yearStart)
                        && !e.getStartTime().isAfter(yearEnd))
                .collect(Collectors.toList());
        Set<Long> yearEventIds = yearEvents.stream().map(VppDrEvent::getId).collect(Collectors.toSet());

        List<VppDrInvitation> yearInvitations = yearEventIds.isEmpty()
                ? Collections.emptyList()
                : yearEventIds.stream()
                .flatMap(id -> invitationsByEvent.getOrDefault(id, Collections.emptyList()).stream())
                .collect(Collectors.toList());

        List<BigDecimal> deviationRates = new ArrayList<>();
        List<BigDecimal> completionRates = new ArrayList<>();
        BigDecimal effectiveKw = BigDecimal.ZERO;
        BigDecimal totalClearedKw = BigDecimal.ZERO;
        BigDecimal durationHour = BigDecimal.ZERO;
        boolean anyWinning = yearInvitations.stream()
                .anyMatch(i -> Objects.equals(i.getIsWinningBid(), IS_WINNING));

        for (VppDrInvitation invitation : yearInvitations) {
            BigDecimal actual = resolveInvitationActualResponseKw(invitation);
            BigDecimal cleared = nz(invitation.getActualClearedCapacityKw());
            BigDecimal inviteScale = nz(invitation.getDemandCapacityKw());

            if (cleared.compareTo(BigDecimal.ZERO) > 0) {
                BigDecimal absDiff = actual.subtract(cleared).abs();
                BigDecimal rate = BigDecimal.ONE.subtract(
                        absDiff.divide(cleared, SCALE, RoundingMode.HALF_UP));
                if (rate.compareTo(BigDecimal.ZERO) < 0) {
                    rate = BigDecimal.ZERO;
                }
                deviationRates.add(rate);
            }

            BigDecimal completion = invitation.getResponseCompletionRate();
            if (completion == null) {
                completion = VppDrParticipationHelper.calcResponseCompletionRate(
                        invitation.getActualResponseCapacityKw(), invitation.getActualClearedCapacityKw());
            }
            if (completion != null) {
                completionRates.add(completion);
            }

            boolean countAsEffective = anyWinning
                    ? Objects.equals(invitation.getIsWinningBid(), IS_WINNING)
                    : actual.compareTo(BigDecimal.ZERO) > 0;
            if (countAsEffective) {
                effectiveKw = effectiveKw.add(actual);
            }
            totalClearedKw = totalClearedKw.add(cleared.compareTo(BigDecimal.ZERO) > 0 ? cleared : inviteScale);
        }

        for (VppDrEvent event : yearEvents) {
            durationHour = durationHour.add(resolveDurationHour(event, evaluationMap.get(event.getId())));
            // 无邀约时回退事件出清规模
            if (!invitationsByEvent.containsKey(event.getId())
                    || invitationsByEvent.get(event.getId()).isEmpty()) {
                totalClearedKw = totalClearedKw.add(nz(event.getClearedCapacityKw()).compareTo(BigDecimal.ZERO) > 0
                        ? nz(event.getClearedCapacityKw())
                        : nz(event.getTargetCapacityKw()));
            }
        }

        yearRegulation.setExecutionDeviationRate(avg(deviationRates));
        yearRegulation.setResponseCompletionRate(avg(completionRates));
        yearRegulation.setEffectiveCapacityMw(toMw(effectiveKw));
        yearRegulation.setTotalCapacityMw(toMw(totalClearedKw));
        yearRegulation.setTotalDurationHour(durationHour.setScale(SCALE, RoundingMode.HALF_UP));
        yearRegulation.setInvitationCount((long) yearInvitations.size());
        yearRegulation.setParticipationCount(yearInvitations.stream()
                .filter(i -> Objects.equals(i.getReplyStatus(), REPLY_ACCEPT))
                .count());
    }

    private void fillSocialBenefit(DashboardSummaryVO.SocialBenefit socialBenefit,
                                   List<VppDrEvent> endedEvents,
                                   Map<Long, VppDrEvaluation> evaluationMap,
                                   Map<Long, List<VppDrInvitation>> invitationsByEvent) {
        BigDecimal responseMwh = BigDecimal.ZERO;
        for (VppDrEvent event : endedEvents) {
            VppDrEvaluation evaluation = evaluationMap.get(event.getId());
            BigDecimal kw = resolveEventActualResponseKw(event, evaluation, invitationsByEvent.get(event.getId()));
            BigDecimal hours = resolveDurationHour(event, evaluation);
            responseMwh = responseMwh.add(kw.multiply(hours).divide(KW_PER_MW, 8, RoundingMode.HALF_UP));
        }

        BigDecimal carbon = responseMwh.multiply(VppSocialBenefitConstants.CARBON_TON_PER_MWH)
                .setScale(SCALE, RoundingMode.HALF_UP);
        BigDecimal coal = responseMwh.multiply(VppSocialBenefitConstants.COAL_TON_PER_MWH)
                .setScale(SCALE, RoundingMode.HALF_UP);
        long trees = carbon.multiply(VppSocialBenefitConstants.TREES_PER_CARBON_TON)
                .setScale(0, RoundingMode.HALF_UP).longValue();
        long greenCert = responseMwh.multiply(VppSocialBenefitConstants.GREEN_CERT_PER_MWH)
                .setScale(0, RoundingMode.HALF_UP).longValue();

        socialBenefit.setCarbonReductionTon(carbon);
        socialBenefit.setCoalSavedTon(coal);
        socialBenefit.setEquivalentTrees(trees);
        socialBenefit.setEquivalentGreenCert(greenCert);
    }

    private void fillEarningsRank(DashboardSummaryVO vo, List<VppSite> sites,
                                  List<VppResourcePoint> resources,
                                  List<VppDrEvent> endedEvents,
                                  Map<Long, VppDrEvaluation> evaluationMap,
                                  List<VppDrInvitation> endedInvitations) {
        if (endedEvents.isEmpty()) {
            vo.setEarningsRank(emptyRank(sites));
            return;
        }

        Set<Long> eventIds = endedEvents.stream().map(VppDrEvent::getId).collect(Collectors.toSet());
        Map<Long, Long> resourceSiteMap = resources.stream()
                .filter(r -> r.getId() != null && r.getSiteId() != null)
                .collect(Collectors.toMap(VppResourcePoint::getId, VppResourcePoint::getSiteId, (a, b) -> a));

        List<VppDrParticipation> participations = participationMapper.selectList(
                new LambdaQueryWrapper<VppDrParticipation>()
                        .in(VppDrParticipation::getEventId, eventIds)
                        .eq(VppDrParticipation::getDeleteFlag, VppAuditHelper.NOT_DELETED));

        Map<Long, Set<Long>> eventSiteIds = new HashMap<>();
        for (VppDrParticipation participation : participations) {
            Long siteId = resourceSiteMap.get(participation.getResourceId());
            if (siteId == null) {
                continue;
            }
            eventSiteIds.computeIfAbsent(participation.getEventId(), k -> new LinkedHashSet<>()).add(siteId);
        }

        for (VppDrInvitation invitation : endedInvitations) {
            if (invitation.getSiteId() == null || invitation.getSiteId() <= 0) {
                continue;
            }
            eventSiteIds.computeIfAbsent(invitation.getDrEventId(), k -> new LinkedHashSet<>())
                    .add(invitation.getSiteId());
        }

        Map<Long, BigDecimal> earningsBySite = new HashMap<>();
        for (VppSite site : sites) {
            earningsBySite.put(site.getId(), BigDecimal.ZERO);
        }
        for (VppDrEvent event : endedEvents) {
            VppDrEvaluation evaluation = evaluationMap.get(event.getId());
            BigDecimal amount = evaluation != null ? nz(evaluation.getSubsidyAmount()) : BigDecimal.ZERO;
            if (amount.compareTo(BigDecimal.ZERO) <= 0) {
                continue;
            }
            Set<Long> siteIds = eventSiteIds.getOrDefault(event.getId(), Collections.emptySet());
            if (siteIds.isEmpty()) {
                continue;
            }
            BigDecimal share = amount.divide(BigDecimal.valueOf(siteIds.size()), 8, RoundingMode.HALF_UP);
            for (Long siteId : siteIds) {
                earningsBySite.merge(siteId, share, BigDecimal::add);
            }
        }

        Map<Long, String> siteNameMap = sites.stream()
                .collect(Collectors.toMap(VppSite::getId, VppSite::getSiteName, (a, b) -> a));
        List<DashboardSummaryVO.EarningsRankItem> rank = earningsBySite.entrySet().stream()
                .map(e -> {
                    DashboardSummaryVO.EarningsRankItem item = new DashboardSummaryVO.EarningsRankItem();
                    item.setSiteId(e.getKey());
                    item.setSiteName(siteNameMap.getOrDefault(e.getKey(), String.valueOf(e.getKey())));
                    item.setEarnings(e.getValue().setScale(MONEY_SCALE, RoundingMode.HALF_UP));
                    return item;
                })
                .sorted(Comparator.comparing(DashboardSummaryVO.EarningsRankItem::getEarnings).reversed())
                .collect(Collectors.toList());
        vo.setEarningsRank(rank);
    }

    private List<DashboardSummaryVO.EarningsRankItem> emptyRank(List<VppSite> sites) {
        return sites.stream().map(site -> {
            DashboardSummaryVO.EarningsRankItem item = new DashboardSummaryVO.EarningsRankItem();
            item.setSiteId(site.getId());
            item.setSiteName(site.getSiteName());
            item.setEarnings(BigDecimal.ZERO.setScale(MONEY_SCALE, RoundingMode.HALF_UP));
            return item;
        }).collect(Collectors.toList());
    }

    // ---------- data loaders ----------

    private List<VppSite> listSites(Integer tenantId) {
        return siteMapper.selectList(new LambdaQueryWrapper<VppSite>()
                .eq(VppSite::getDeleteFlag, VppAuditHelper.NOT_DELETED)
                .eq(tenantId != null, VppSite::getTenantId, tenantId));
    }

    private List<VppResourcePoint> listResources(Integer tenantId) {
        return resourcePointMapper.selectList(new LambdaQueryWrapper<VppResourcePoint>()
                .eq(VppResourcePoint::getDeleteFlag, VppAuditHelper.NOT_DELETED)
                .eq(tenantId != null, VppResourcePoint::getTenantId, tenantId));
    }

    private List<VppDevice> listDevices(Integer tenantId) {
        return deviceMapper.selectList(new LambdaQueryWrapper<VppDevice>()
                .eq(VppDevice::getDeleteFlag, VppAuditHelper.NOT_DELETED)
                .eq(tenantId != null, VppDevice::getTenantId, tenantId));
    }

    private List<VppDrEvent> listEndedEvents(Integer tenantId) {
        return eventMapper.selectList(new LambdaQueryWrapper<VppDrEvent>()
                .eq(VppDrEvent::getDeleteFlag, VppAuditHelper.NOT_DELETED)
                .eq(VppDrEvent::getEventStatus, EVENT_STATUS_ENDED)
                .eq(tenantId != null, VppDrEvent::getTenantId, tenantId));
    }

    private List<VppDrInvitation> listInvitationsByEvents(List<VppDrEvent> events, Integer tenantId) {
        if (events.isEmpty()) {
            return Collections.emptyList();
        }
        Set<Long> eventIds = events.stream().map(VppDrEvent::getId).collect(Collectors.toSet());
        return invitationMapper.selectList(new LambdaQueryWrapper<VppDrInvitation>()
                .in(VppDrInvitation::getDrEventId, eventIds)
                .eq(VppDrInvitation::getDeleteFlag, VppAuditHelper.NOT_DELETED)
                .eq(tenantId != null, VppDrInvitation::getTenantId, tenantId));
    }

    private Map<Long, VppDrEvaluation> loadEvaluationMap(List<VppDrEvent> events) {
        if (events.isEmpty()) {
            return Collections.emptyMap();
        }
        Set<Long> eventIds = events.stream().map(VppDrEvent::getId).collect(Collectors.toSet());
        return evaluationMapper.selectList(new LambdaQueryWrapper<VppDrEvaluation>()
                        .in(VppDrEvaluation::getEventId, eventIds)
                        .eq(VppDrEvaluation::getDeleteFlag, VppAuditHelper.NOT_DELETED))
                .stream()
                .collect(Collectors.toMap(VppDrEvaluation::getEventId, e -> e, (a, b) -> a));
    }

    // ---------- helpers ----------

    private static BigDecimal resolveEventActualResponseKw(VppDrEvent event, VppDrEvaluation evaluation,
                                                           List<VppDrInvitation> invitations) {
        if (!CollectionUtils.isEmpty(invitations)) {
            BigDecimal sum = BigDecimal.ZERO;
            boolean any = false;
            for (VppDrInvitation invitation : invitations) {
                BigDecimal kw = resolveInvitationActualResponseKw(invitation);
                if (kw.compareTo(BigDecimal.ZERO) > 0
                        || invitation.getActualResponseCapacityKw() != null
                        || invitation.getEstimatedResponseCapacityKw() != null) {
                    sum = sum.add(kw);
                    any = true;
                }
            }
            if (any) {
                return sum;
            }
        }
        if (evaluation != null && evaluation.getActualCapacityKw() != null) {
            return nz(evaluation.getActualCapacityKw());
        }
        return BigDecimal.ZERO;
    }

    private static BigDecimal resolveInvitationActualResponseKw(VppDrInvitation invitation) {
        if (invitation.getActualResponseCapacityKw() != null) {
            return nz(invitation.getActualResponseCapacityKw());
        }
        if (invitation.getEstimatedResponseCapacityKw() != null) {
            return nz(invitation.getEstimatedResponseCapacityKw());
        }
        return BigDecimal.ZERO;
    }

    private static BigDecimal resolveDurationHour(VppDrEvent event, VppDrEvaluation evaluation) {
        if (evaluation != null && evaluation.getResponseDurationMin() != null
                && evaluation.getResponseDurationMin() > 0) {
            return BigDecimal.valueOf(evaluation.getResponseDurationMin())
                    .divide(BigDecimal.valueOf(60), SCALE, RoundingMode.HALF_UP);
        }
        if (event.getStartTime() != null && event.getEndTime() != null
                && event.getEndTime().isAfter(event.getStartTime())) {
            long minutes = ChronoUnit.MINUTES.between(event.getStartTime(), event.getEndTime());
            if (minutes > 0) {
                return BigDecimal.valueOf(minutes)
                        .divide(BigDecimal.valueOf(60), SCALE, RoundingMode.HALF_UP);
            }
        }
        return BigDecimal.ZERO;
    }

    private static BigDecimal avg(List<BigDecimal> values) {
        if (values == null || values.isEmpty()) {
            return null;
        }
        BigDecimal sum = BigDecimal.ZERO;
        for (BigDecimal value : values) {
            sum = sum.add(value);
        }
        return sum.divide(BigDecimal.valueOf(values.size()), SCALE, RoundingMode.HALF_UP);
    }

    private static BigDecimal toMw(BigDecimal kw) {
        return nz(kw).divide(KW_PER_MW, SCALE, RoundingMode.HALF_UP);
    }

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