package com.usky.vpp.service.impl;

import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
import com.usky.common.core.exception.BusinessException;
import com.usky.vpp.constant.VppDrEventStatus;
import com.usky.vpp.domain.VppDevice;
import com.usky.vpp.domain.VppDrEvent;
import com.usky.vpp.domain.VppResourcePoint;
import com.usky.vpp.domain.VppSite;
import com.usky.vpp.mapper.VppDeviceMapper;
import com.usky.vpp.mapper.VppDrEventMapper;
import com.usky.vpp.mapper.VppResourcePointMapper;
import com.usky.vpp.mapper.VppSiteMapper;
import com.usky.vpp.service.VppBaselineService;
import com.usky.vpp.service.VppTsdbQueryService;
import com.usky.vpp.service.vo.BaselinePointVO;
import com.usky.vpp.service.vo.SiteBaselineVO;
import com.usky.vpp.service.vo.SiteDeclaredCapacityVO;
import com.usky.vpp.util.VppAuditHelper;
import com.usky.vpp.util.VppBaselineHelper;
import com.usky.vpp.util.VppWorkdayHelper;
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.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.HashSet;
import java.util.LinkedHashSet;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import java.util.Set;
import java.util.TreeMap;
import java.util.stream.Collectors;

@Service
public class VppBaselineServiceImpl implements VppBaselineService {

    private static final int EVENT_STATUS_ENDED = VppDrEventStatus.ENDED;
    private static final DateTimeFormatter DATE_TIME_FMT = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss");

    @Autowired
    private VppSiteMapper siteMapper;
    @Autowired
    private VppResourcePointMapper resourcePointMapper;
    @Autowired
    private VppDeviceMapper deviceMapper;
    @Autowired
    private VppDrEventMapper drEventMapper;
    @Autowired
    private VppTsdbQueryService vppTsdbQueryService;

    /**
     * 查询站点基线负荷曲线（预测基线 / 今日实际 / 昨日实际）。
     *
     * <p><b>入参</b></p>
     * <ul>
     *   <li>siteId — 站点 ID，校验站点存在且未软删除</li>
     *   <li>responseStartTime — 响应开始时间，格式 yyyy-MM-dd HH:mm:ss</li>
     * </ul>
     *
     * <p><b>处理流程</b></p>
     * <ol>
     *   <li>查询站点下 is_support_peak=1 的资源点关联设备 UUID；无设备则返回空曲线</li>
     *   <li>按 Hutool 法定节假日规则判定响应日类型（工作日 / 非工作日），选取典型历史日：
     *       工作日 5 天 / 非工作日 3 天，剔除历史响应日</li>
     *   <li>本模块按典型历史日逐日调用 TSDB {@code queryHistoryDeviceData}，聚合计算全天原始基线（10 分钟粒度）</li>
     *   <li>取响应开始时间前连续 2 小时的今日实时负荷（绿色曲线）与同时段原始基线比值，计算修正系数 K（0.7 ~ 1.2）</li>
     *   <li>原始基线 × K 得到预测基线；并查询今日 / 昨日实测曲线</li>
     *   <li>组装 {@link SiteBaselineVO}，dataSource 为 tsdb；无 TSDB 数据时返回空曲线</li>
     * </ol>
     *
     * @param siteId            站点 ID
     * @param responseStartTime 响应开始时间
     * @return 基线曲线及修正系数等信息
     * @throws BusinessException 站点不存在、响应开始时间为空或格式无效
     */
    @Override
    public SiteBaselineVO getSiteBaseline(Long siteId, String responseStartTime) {
        return getSiteBaseline(siteId, responseStartTime, null, null);
    }

    @Override
    public SiteBaselineVO getSiteBaseline(Long siteId, String responseStartTime,
                                          Integer baselineDays, String actualEndTime) {
        VppSite site = resolveSite(siteId);
        LocalDateTime startTime = parseResponseStartTime(responseStartTime);
        LocalDate responseDate = startTime.toLocalDate();
        LocalDateTime actualUntil = StringUtils.hasText(actualEndTime)
                ? parseResponseStartTime(actualEndTime)
                : startTime;

        List<String> deviceUuids = listControllableDeviceUuids(siteId);
        SiteBaselineVO result = new SiteBaselineVO();
        result.setSiteId(siteId);
        result.setSiteName(site.getSiteName());
        result.setResponseStartTime(startTime);

        if (deviceUuids.isEmpty()) {
            // return buildMockResult(result, siteId, responseDate, startTime);
            return result;
        }

        Set<LocalDate> excludeDates = loadResponseHistoryDates();
        boolean workday = VppBaselineHelper.isWorkdayResponse(responseDate);
        result.setResponseDayType(VppWorkdayHelper.resolveDayType(responseDate));
        result.setResponseDayTypeLabel(VppWorkdayHelper.resolveDayTypeLabel(responseDate));
        int requiredCount;
        if (baselineDays != null && (baselineDays == 3 || baselineDays == 5)) {
            requiredCount = baselineDays;
        } else {
            requiredCount = workday
                    ? VppBaselineHelper.WEEKDAY_REFERENCE_COUNT
                    : VppBaselineHelper.NON_WEEKDAY_REFERENCE_COUNT;
        }
        List<LocalDate> referenceDates = VppBaselineHelper.selectReferenceDates(
                responseDate, workday, excludeDates, requiredCount);
        result.setReferenceDates(referenceDates.stream()
                .map(VppBaselineHelper::formatDate)
                .collect(Collectors.toList()));

        //原始基线
        Map<String, BigDecimal> originalBaselineByTime = vppTsdbQueryService.queryBaselineAvgByTimePoint(
                deviceUuids,
                referenceDates,
                LocalTime.MIN,
                LocalTime.of(23, 59, 59),
                VppBaselineHelper.POWER_METRICS,
                VppBaselineHelper.INTERVAL_MINUTES,
                null);
        if (originalBaselineByTime.isEmpty()) {
            // return buildMockResult(result, siteId, responseDate, startTime);
            return result;
        }

        //历史日的响应开始时间前两个小时原始基线
        LocalDateTime correctionWindowStart = startTime.minusHours(VppBaselineHelper.CORRECTION_HOURS);
        Map<String, BigDecimal> correctionBaselineByTime = vppTsdbQueryService.queryBaselineAvgByTimePoint(
                deviceUuids,
                referenceDates,
                correctionWindowStart.toLocalTime(),
                startTime.toLocalTime(),
                VppBaselineHelper.POWER_METRICS,
                VppBaselineHelper.INTERVAL_MINUTES,
                null);

        LocalDate yesterday = responseDate.minusDays(1);
        //响应日实测（可延至响应后窗口）
        LocalDateTime todayQueryEnd = actualUntil.isAfter(startTime) ? actualUntil : startTime;
        Map<String, BigDecimal> todayActualByTime = VppBaselineHelper.buildActualLoadByTimePoint(
                vppTsdbQueryService.queryDeviceMetricHistory(
                        deviceUuids, responseDate.atStartOfDay(), todayQueryEnd, VppBaselineHelper.POWER_METRICS),
                VppBaselineHelper.INTERVAL_MINUTES);
        //响应开始时间日前一天数据线
        Map<String, BigDecimal> yesterdayActualByTime = VppBaselineHelper.buildActualLoadByTimePoint(
                vppTsdbQueryService.queryDeviceMetricHistory(
                        deviceUuids, yesterday.atStartOfDay(), yesterday.atTime(23, 59, 59), VppBaselineHelper.POWER_METRICS),
                VppBaselineHelper.INTERVAL_MINUTES);

        //计算修正系数
        BigDecimal correctionFactorK = VppBaselineHelper.calculateCorrectionFactor(
                startTime, todayActualByTime, correctionBaselineByTime.isEmpty()
                        ? originalBaselineByTime : correctionBaselineByTime);

        //预测基线
        Map<String, BigDecimal> predictedBaselineByTime = vppTsdbQueryService.queryBaselineAvgByTimePoint(
                deviceUuids,
                referenceDates,
                LocalTime.MIN,
                LocalTime.of(23, 59, 59),
                VppBaselineHelper.POWER_METRICS,
                VppBaselineHelper.INTERVAL_MINUTES,
                correctionFactorK);

        List<BaselinePointVO> points = VppBaselineHelper.buildBaselinePoints(
                responseDate,
                startTime,
                todayQueryEnd,
                predictedBaselineByTime.isEmpty() ? applyCorrection(originalBaselineByTime, correctionFactorK)
                        : predictedBaselineByTime,
                todayActualByTime,
                yesterdayActualByTime);

        result.setCorrectionFactorK(correctionFactorK);
        result.setPoints(points);
        result.setDataSource("tsdb");
        return result;
    }

    /**
     * 查询站点响应申报容量和预估响应容量。
     *
     * <p>申报容量公式：响应时段内同时间段历史负荷均值 × 执行时段小时数 / 2。</p>
     * <p>预估响应容量公式：响应时段内今日负荷均值 − 响应时段内历史负荷均值 × 修正系数 K；
     * K 由响应开始前 2 小时今日实测 / 同时段历史基线计算，限制在 [0.7, 1.2]。</p>
     * <p>典型历史日选取规则（工作日 5 天 / 非工作日 3 天），剔除响应当日、历史响应日（vpp_dr_event 已结束）
     * 及响应日 &gt;= 今日。</p>
     */
    @Override
    public SiteDeclaredCapacityVO getSiteDeclaredCapacity(Long siteId,
                                                          String responseStartTime,
                                                          String responseEndTime) {
        VppSite site = resolveSite(siteId);
        LocalDateTime startTime = parseResponseStartTime(responseStartTime);
        LocalDateTime endTime = parseResponseEndTime(responseEndTime, startTime);
        LocalDate responseDate = startTime.toLocalDate();

        SiteDeclaredCapacityVO result = new SiteDeclaredCapacityVO();
        result.setSiteId(siteId);
        result.setSiteName(site.getSiteName());
        result.setResponseStartTime(startTime);
        result.setResponseEndTime(endTime);

        List<String> deviceUuids = listControllableDeviceUuids(siteId);
        if (deviceUuids.isEmpty()) {
            // return buildMockDeclaredCapacityResult(result, siteId, responseDate, startTime, endTime);
            return result;
        }

        Set<LocalDate> excludeDates = loadDeclaredCapacityExcludeDates();
        boolean workday = VppBaselineHelper.isWorkdayResponse(responseDate);
        result.setResponseDayType(VppWorkdayHelper.resolveDayType(responseDate));
        result.setResponseDayTypeLabel(VppWorkdayHelper.resolveDayTypeLabel(responseDate));
        int requiredCount = workday
                ? VppBaselineHelper.WEEKDAY_REFERENCE_COUNT
                : VppBaselineHelper.NON_WEEKDAY_REFERENCE_COUNT;
        List<LocalDate> referenceDates = VppBaselineHelper.selectReferenceDates(
                responseDate, workday, excludeDates, requiredCount, LocalDate.now());
        result.setReferenceDates(referenceDates.stream()
                .map(VppBaselineHelper::formatDate)
                .collect(Collectors.toList()));
        if (referenceDates.isEmpty()) {
            return result;
        }

        Map<String, BigDecimal> todayActualByTime = VppBaselineHelper.buildActualLoadByTimePoint(
                vppTsdbQueryService.queryDeviceMetricHistory(
                        deviceUuids, responseDate.atStartOfDay(), endTime, VppBaselineHelper.POWER_METRICS),
                VppBaselineHelper.INTERVAL_MINUTES);

        Map<String, BigDecimal> historicalAvgByTime = vppTsdbQueryService.queryBaselineAvgByTimePoint(
                deviceUuids,
                referenceDates,
                startTime.toLocalTime(),
                endTime.toLocalTime(),
                VppBaselineHelper.POWER_METRICS,
                VppBaselineHelper.INTERVAL_MINUTES,
                null);
        if (historicalAvgByTime.isEmpty()) {
            // return buildMockDeclaredCapacityResult(result, siteId, responseDate, startTime, endTime);
            return result;
        }

        LocalDateTime correctionWindowStart = startTime.minusHours(VppBaselineHelper.CORRECTION_HOURS);
        Map<String, BigDecimal> correctionBaselineByTime = vppTsdbQueryService.queryBaselineAvgByTimePoint(
                deviceUuids,
                referenceDates,
                correctionWindowStart.toLocalTime(),
                startTime.toLocalTime(),
                VppBaselineHelper.POWER_METRICS,
                VppBaselineHelper.INTERVAL_MINUTES,
                null);
        BigDecimal correctionFactorK = VppBaselineHelper.calculateCorrectionFactor(
                startTime, todayActualByTime, correctionBaselineByTime);

        result.setDeclaredCapacityKw(VppBaselineHelper.calculateDeclaredCapacityKw(
                historicalAvgByTime, startTime, endTime));
        result.setEstimatedResponseCapacityKw(VppBaselineHelper.calculateEstimatedResponseCapacityKw(
                todayActualByTime, historicalAvgByTime, correctionFactorK, startTime, endTime));
        result.setPoints(VppBaselineHelper.buildDeclaredCapacityPoints(
                historicalAvgByTime, startTime, endTime));
        result.setDataSource("tsdb");
        return result;
    }

    /*
    private SiteDeclaredCapacityVO buildMockDeclaredCapacityResult(SiteDeclaredCapacityVO result,
                                                                 Long siteId,
                                                                 LocalDate responseDate,
                                                                 LocalDateTime startTime,
                                                                 LocalDateTime endTime) {
        result.setReferenceDates(new ArrayList<>());
        result.setResponseDayType(VppWorkdayHelper.resolveDayType(responseDate));
        result.setResponseDayTypeLabel(VppWorkdayHelper.resolveDayTypeLabel(responseDate));
        result.setDeclaredCapacityKw(VppBaselineHelper.mockDeclaredCapacityKw(
                siteId, responseDate, startTime, endTime));
        result.setPoints(VppBaselineHelper.buildMockDeclaredCapacityPoints(
                siteId, responseDate, startTime, endTime));
        result.setDataSource("mock");
        return result;
    }
    */

    private Map<String, BigDecimal> applyCorrection(Map<String, BigDecimal> originalBaselineByTime,
                                                    BigDecimal correctionFactorK) {
        Map<String, BigDecimal> corrected = new TreeMap<>();
        for (Map.Entry<String, BigDecimal> entry : originalBaselineByTime.entrySet()) {
            BigDecimal value = entry.getValue() != null ? entry.getValue() : BigDecimal.ZERO;
            corrected.put(entry.getKey(), value.multiply(correctionFactorK).setScale(2, java.math.RoundingMode.HALF_UP));
        }
        return corrected;
    }

    /*
    private SiteBaselineVO buildMockResult(SiteBaselineVO result,
                                           Long siteId,
                                           LocalDate responseDate,
                                           LocalDateTime responseStartTime) {
        result.setReferenceDates(new ArrayList<>());
        result.setResponseDayType(VppWorkdayHelper.resolveDayType(responseDate));
        result.setResponseDayTypeLabel(VppWorkdayHelper.resolveDayTypeLabel(responseDate));
        result.setCorrectionFactorK(VppBaselineHelper.mockCorrectionFactor(siteId, responseDate));
        result.setPoints(VppBaselineHelper.buildMockBaselinePoints(siteId, responseDate, responseStartTime));
        result.setDataSource("mock");
        return result;
    }
    */

    private List<String> listControllableDeviceUuids(Long siteId) {
        List<VppResourcePoint> resources = resourcePointMapper.selectList(
                new LambdaQueryWrapper<VppResourcePoint>()
                        .eq(VppResourcePoint::getSiteId, siteId)
                        .eq(VppResourcePoint::getIsSupportPeak, 1)
                        .eq(VppResourcePoint::getDeleteFlag, VppAuditHelper.NOT_DELETED));
        if (CollectionUtils.isEmpty(resources)) {
            return new ArrayList<>();
        }

        Set<Long> deviceIds = resources.stream()
                .map(VppResourcePoint::getDeviceId)
                .filter(Objects::nonNull)
                .collect(Collectors.toSet());
        if (deviceIds.isEmpty()) {
            return new ArrayList<>();
        }

        List<VppDevice> devices = deviceMapper.selectBatchIds(deviceIds).stream()
                .filter(device -> !VppAuditHelper.isDeleted(device.getDeleteFlag()))
                .collect(Collectors.toList());

        Set<String> uuids = new LinkedHashSet<>();
        for (VppDevice device : devices) {
            if (StringUtils.hasText(device.getDeviceUuid())) {
                uuids.add(device.getDeviceUuid().trim());
            }
        }
        return new ArrayList<>(uuids);
    }

    private Set<LocalDate> loadResponseHistoryDates() {
        return new HashSet<>(loadCompleteResponseDates());
    }

    /** 申报容量参考日剔除：历史响应日（vpp_dr_event 已结束）。 */
    private Set<LocalDate> loadDeclaredCapacityExcludeDates() {
        return loadResponseHistoryDates();
    }

    /** 已结束需求响应事件的起始日期（历史完整响应日）。 */
    private List<LocalDate> loadCompleteResponseDates() {
        List<VppDrEvent> events = drEventMapper.selectList(
                new LambdaQueryWrapper<VppDrEvent>()
                        .eq(VppDrEvent::getEventStatus, EVENT_STATUS_ENDED)
                        .eq(VppDrEvent::getDeleteFlag, VppAuditHelper.NOT_DELETED));
        Set<LocalDate> dates = new LinkedHashSet<>();
        for (VppDrEvent event : events) {
            if (event.getStartTime() != null) {
                dates.add(event.getStartTime().toLocalDate());
            }
        }
        return new ArrayList<>(dates);
    }

    private VppSite resolveSite(Long siteId) {
        if (siteId == null) {
            throw new BusinessException("站点不存在");
        }
        VppSite site = siteMapper.selectById(siteId);
        if (site == null || VppAuditHelper.isDeleted(site.getDeleteFlag())) {
            throw new BusinessException("站点不存在");
        }
        return site;
    }

    private LocalDateTime parseResponseStartTime(String responseStartTime) {
        if (!StringUtils.hasText(responseStartTime)) {
            throw new BusinessException("响应开始时间不能为空");
        }
        try {
            return LocalDateTime.parse(responseStartTime.trim(), DATE_TIME_FMT);
        } catch (DateTimeParseException ex) {
            throw new BusinessException("响应开始时间格式无效，请使用 yyyy-MM-dd HH:mm:ss");
        }
    }

    private LocalDateTime parseResponseEndTime(String responseEndTime, LocalDateTime responseStartTime) {
        if (!StringUtils.hasText(responseEndTime)) {
            throw new BusinessException("响应结束时间不能为空");
        }
        LocalDateTime endTime;
        try {
            endTime = LocalDateTime.parse(responseEndTime.trim(), DATE_TIME_FMT);
        } catch (DateTimeParseException ex) {
            throw new BusinessException("响应结束时间格式无效，请使用 yyyy-MM-dd HH:mm:ss");
        }
        if (endTime.isBefore(responseStartTime)) {
            throw new BusinessException("响应结束时间不能早于响应开始时间");
        }
        if (!endTime.toLocalDate().equals(responseStartTime.toLocalDate())) {
            throw new BusinessException("响应结束时间需与响应开始时间在同一天");
        }
        return endTime;
    }
}
