package com.usky.vpp.util;

import com.usky.vpp.constant.VppTsdbConstants;
import com.usky.vpp.service.vo.BaselinePointVO;
import com.usky.vpp.service.vo.DeclaredCapacityPointVO;

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.util.ArrayList;
import java.util.Collections;
import java.util.HashSet;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.TreeMap;
import java.util.function.BiFunction;

/**
 * 基线管理计算辅助（典型日筛选、原始基线、修正系数、曲线构建）
 */
public final class VppBaselineHelper {

    public static final int WEEKDAY_REFERENCE_COUNT = 5;
    public static final int NON_WEEKDAY_REFERENCE_COUNT = 3;
    public static final int CORRECTION_HOURS = 2;
    public static final int INTERVAL_MINUTES = 5;
    public static final BigDecimal K_MIN = new BigDecimal("0.7");
    public static final BigDecimal K_MAX = new BigDecimal("1.2");
    public static final BigDecimal K_DEFAULT = BigDecimal.ONE;

    /** 总有功功率指标（文档 p + 存量字段兼容） */
    public static final List<String> POWER_METRICS;

    private static final DateTimeFormatter TIME_FMT = DateTimeFormatter.ofPattern("HH:mm");
    private static final DateTimeFormatter TIME_WITH_SEC_FMT = DateTimeFormatter.ofPattern("HH:mm:ss");
    private static final DateTimeFormatter DATE_FMT = DateTimeFormatter.ofPattern("yyyy-MM-dd");

    static {
        List<String> metrics = new ArrayList<>();
        metrics.add("p");
        metrics.addAll(VppTsdbConstants.ACTIVE_POWER_METRICS);
        POWER_METRICS = Collections.unmodifiableList(metrics);
    }

    private VppBaselineHelper() {
    }

    /**
     * 响应日是否为工作日（Hutool 法定节假日 + 调休规则）
     */
    public static boolean isWorkdayResponse(LocalDate date) {
        return VppWorkdayHelper.isWorkday(date);
    }

    /**
     * @deprecated 使用 {@link #isWorkdayResponse(LocalDate)}
     */
    @Deprecated
    public static boolean isWeekday(LocalDate date) {
        return isWorkdayResponse(date);
    }

    /**
     * 选取典型历史日：工作日取前 5 个工作日，非工作日取前 3 个非工作日；
     * 剔除 {@code excludeDates} 中的日期及响应当日。
     */
    public static List<LocalDate> selectReferenceDates(LocalDate responseDate,
                                                       boolean workday,
                                                       Set<LocalDate> excludeDates,
                                                       int requiredCount) {
        return selectReferenceDates(responseDate, workday, excludeDates, requiredCount, null);
    }

    /**
     * 选取典型历史日，并可指定 {@code today}：候选参考日须严格早于 {@code today}（响应日 &gt;= 今日不参与选取）。
     */
    public static List<LocalDate> selectReferenceDates(LocalDate responseDate,
                                                       boolean workday,
                                                       Set<LocalDate> excludeDates,
                                                       int requiredCount,
                                                       LocalDate today) {
        Set<LocalDate> excludes = new HashSet<>(excludeDates != null ? excludeDates : Collections.emptySet());
        excludes.add(responseDate);

        List<LocalDate> result = new ArrayList<>();
        LocalDate cursor = responseDate.minusDays(1);
        int guard = 0;
        while (result.size() < requiredCount && guard < 400) {
            if (today != null && !cursor.isBefore(today)) {
                cursor = cursor.minusDays(1);
                guard++;
                continue;
            }
            if (excludes.contains(cursor)) {
                cursor = cursor.minusDays(1);
                guard++;
                continue;
            }
            if (VppWorkdayHelper.isWorkday(cursor) == workday) {
                result.add(cursor);
            }
            cursor = cursor.minusDays(1);
            guard++;
        }
        return result;
    }

    /**
     * 多历史日同时段各时间点负荷求平均（先按日汇总设备负荷，再跨日取均值）。
     * <p>通过 historyLoader 按日调用 TSDB {@code queryHistoryDeviceData} 获取原始时序后在本模块聚合。</p>
     */
    public static Map<String, BigDecimal> aggregateBaselineAvgByTimePoint(
            List<LocalDate> referenceDates,
            LocalTime periodStart,
            LocalTime periodEnd,
            int intervalMinutes,
            BigDecimal correctionFactor,
            BiFunction<LocalDateTime, LocalDateTime, Map<String, Map<String, TreeMap<LocalDateTime, BigDecimal>>>> historyLoader) {
        if (referenceDates == null || referenceDates.isEmpty() || historyLoader == null) {
            return Collections.emptyMap();
        }

        LocalTime dayStart = periodStart != null ? periodStart : LocalTime.MIN;
        LocalTime dayEnd = periodEnd != null ? periodEnd : LocalTime.of(23, 59, 59);
        int interval = intervalMinutes > 0 ? intervalMinutes : INTERVAL_MINUTES;

        Map<String, List<BigDecimal>> samplesByTime = new TreeMap<>();
        for (LocalDate date : referenceDates) {
            LocalDateTime start = LocalDateTime.of(date, dayStart);
            LocalDateTime end = LocalDateTime.of(date, dayEnd);
            if (end.isBefore(start)) {
                continue;
            }
            Map<String, Map<String, TreeMap<LocalDateTime, BigDecimal>>> dayData = historyLoader.apply(start, end);
            Map<String, BigDecimal> dayBuckets = buildActualLoadByTimePoint(dayData, interval);
            if (dayBuckets.isEmpty()) {
                continue;
            }
            for (Map.Entry<String, BigDecimal> entry : dayBuckets.entrySet()) {
                samplesByTime.computeIfAbsent(entry.getKey(), key -> new ArrayList<>()).add(entry.getValue());
            }
        }

        Map<String, BigDecimal> result = new LinkedHashMap<>();
        for (Map.Entry<String, List<BigDecimal>> entry : samplesByTime.entrySet()) {
            BigDecimal avg = average(entry.getValue());
            if (correctionFactor != null) {
                avg = avg.multiply(correctionFactor);
            }
            result.put(entry.getKey(), scale(avg));
        }
        return result;
    }

    /**
     * 将设备历史时序聚合为 HH:mm 时间点总负荷。
     */
    public static Map<String, BigDecimal> buildActualLoadByTimePoint(
            Map<String, Map<String, TreeMap<LocalDateTime, BigDecimal>>> deviceData,
            int intervalMinutes) {
        if (deviceData == null || deviceData.isEmpty()) {
            return Collections.emptyMap();
        }

        Map<String, List<BigDecimal>> samples = new TreeMap<>();
        for (Map<String, TreeMap<LocalDateTime, BigDecimal>> metricMap : deviceData.values()) {
            TreeMap<LocalDateTime, BigDecimal> series = pickSeries(metricMap);
            if (series == null || series.isEmpty()) {
                continue;
            }
            Map<String, List<BigDecimal>> deviceBuckets = bucketSeries(series, intervalMinutes);
            for (Map.Entry<String, List<BigDecimal>> entry : deviceBuckets.entrySet()) {
                BigDecimal deviceAvg = average(entry.getValue());
                samples.computeIfAbsent(entry.getKey(), key -> new ArrayList<>()).add(deviceAvg);
            }
        }

        Map<String, BigDecimal> result = new LinkedHashMap<>();
        for (Map.Entry<String, List<BigDecimal>> entry : samples.entrySet()) {
            result.put(entry.getKey(), scale(sum(entry.getValue())));
        }
        return result;
    }

    /**
     * 计算日内修正系数 K = 响应开始前 2 小时今日实测 / 同时段原始基线，并限制在 [0.7, 1.2]。
     */
    public static BigDecimal calculateCorrectionFactor(LocalDateTime responseStartTime,
                                                       Map<String, BigDecimal> todayActualByTime,
                                                       Map<String, BigDecimal> originalBaselineByTime) {
        LocalDateTime windowStart = responseStartTime.minusHours(CORRECTION_HOURS);
        BigDecimal todayTotal = sumValuesInWindow(todayActualByTime, windowStart, responseStartTime);
        BigDecimal baselineTotal = sumValuesInWindow(originalBaselineByTime, windowStart, responseStartTime);
        if (baselineTotal == null || baselineTotal.compareTo(BigDecimal.ZERO) <= 0) {
            return K_DEFAULT;
        }
        if (todayTotal == null || todayTotal.compareTo(BigDecimal.ZERO) <= 0) {
            return K_DEFAULT;
        }
        BigDecimal k = todayTotal.divide(baselineTotal, 6, RoundingMode.HALF_UP);
        if (k.compareTo(K_MIN) < 0) {
            return K_MIN;
        }
        if (k.compareTo(K_MAX) > 0) {
            return K_MAX;
        }
        return k.setScale(4, RoundingMode.HALF_UP);
    }

    public static List<BaselinePointVO> buildBaselinePoints(LocalDate responseDate,
                                                            LocalDateTime responseStartTime,
                                                            Map<String, BigDecimal> predictedBaselineByTime,
                                                            Map<String, BigDecimal> todayActualByTime,
                                                            Map<String, BigDecimal> yesterdayActualByTime) {
        return buildBaselinePoints(responseDate, responseStartTime, responseStartTime,
                predictedBaselineByTime, todayActualByTime, yesterdayActualByTime);
    }

    /**
     * @param todayActualUntil 当日实测曲线填充截止时间（含），用于响应后窗口展示
     */
    public static List<BaselinePointVO> buildBaselinePoints(LocalDate responseDate,
                                                            LocalDateTime responseStartTime,
                                                            LocalDateTime todayActualUntil,
                                                            Map<String, BigDecimal> predictedBaselineByTime,
                                                            Map<String, BigDecimal> todayActualByTime,
                                                            Map<String, BigDecimal> yesterdayActualByTime) {
        List<BaselinePointVO> points = new ArrayList<>();
        LocalDateTime actualUntil = todayActualUntil != null ? todayActualUntil : responseStartTime;
        for (Map.Entry<String, BigDecimal> entry : predictedBaselineByTime.entrySet()) {
            String timeKey = entry.getKey();
            LocalTime time = parseTimeKey(timeKey);
            LocalDateTime pointTime = LocalDateTime.of(responseDate, time);

            BaselinePointVO point = new BaselinePointVO();
            point.setTime(timeKey);
            point.setPredictedBaselineKw(scale(entry.getValue()));

            if (!pointTime.isAfter(actualUntil)) {
                point.setTodayActualKw(scale(todayActualByTime.get(timeKey)));
            }
            point.setYesterdayActualKw(scale(yesterdayActualByTime.get(timeKey)));
            points.add(point);
        }
        return points;
    }

    public static boolean hasTsdbPowerData(Map<String, Map<String, TreeMap<LocalDateTime, BigDecimal>>> deviceMetricData) {
        return VppCapabilityEvalHelper.hasTsdbPowerData(deviceMetricData, POWER_METRICS);
    }

    /*
    public static List<BaselinePointVO> buildMockBaselinePoints(Long siteId,
                                                                LocalDate responseDate,
                                                                LocalDateTime responseStartTime) {
        List<BaselinePointVO> points = new ArrayList<>();
        long seed = (siteId != null ? siteId : 0L) + responseDate.toEpochDay();
        for (int minute = 0; minute < 1440; minute += INTERVAL_MINUTES) {
            LocalTime time = LocalTime.of(minute / 60, minute % 60);
            String timeKey = time.format(TIME_FMT);
            double hourFactor = mockHourFactor(time);
            double noise = 0.9 + pseudoRandom(seed, minute, 0) * 0.2;

            BigDecimal yesterday = scale(BigDecimal.valueOf(900 * hourFactor * noise));
            BigDecimal original = yesterday.multiply(BigDecimal.valueOf(0.98 + pseudoRandom(seed, minute, 1) * 0.04))
                    .setScale(2, RoundingMode.HALF_UP);
            BigDecimal predicted = original.multiply(BigDecimal.valueOf(1.03)).setScale(2, RoundingMode.HALF_UP);

            BaselinePointVO point = new BaselinePointVO();
            point.setTime(timeKey);
            point.setPredictedBaselineKw(predicted);
            point.setYesterdayActualKw(yesterday);
            if (!responseDate.atTime(time).isAfter(responseStartTime)) {
                point.setTodayActualKw(scale(original.multiply(BigDecimal.valueOf(1.01 + pseudoRandom(seed, minute, 2) * 0.05))));
            }
            points.add(point);
        }
        return points;
    }

    public static BigDecimal mockCorrectionFactor(Long siteId, LocalDate responseDate) {
        long seed = (siteId != null ? siteId : 0L) + responseDate.toEpochDay();
        double raw = 0.85 + pseudoRandom(seed, 99, 3) * 0.25;
        BigDecimal k = BigDecimal.valueOf(raw).setScale(4, RoundingMode.HALF_UP);
        if (k.compareTo(K_MIN) < 0) {
            return K_MIN;
        }
        if (k.compareTo(K_MAX) > 0) {
            return K_MAX;
        }
        return k;
    }
    */

    public static String formatDate(LocalDate date) {
        return date.format(DATE_FMT);
    }

    private static final BigDecimal DECLARED_CAPACITY_DIVISOR = new BigDecimal("2");
    private static final BigDecimal MINUTES_PER_HOUR = new BigDecimal("60");

    /**
     * 响应执行时段小时数（执行开始至结束）。
     */
    public static BigDecimal responseWindowHours(LocalDateTime windowStart, LocalDateTime windowEnd) {
        if (windowStart == null || windowEnd == null || windowEnd.isBefore(windowStart)) {
            return BigDecimal.ZERO;
        }
        long minutes = java.time.Duration.between(windowStart, windowEnd).toMinutes();
        return BigDecimal.valueOf(minutes).divide(MINUTES_PER_HOUR, 4, RoundingMode.HALF_UP);
    }

    /**
     * 响应申报容量 = 响应时段内历史负荷均值 × 执行时段小时数 / 2。
     */
    public static BigDecimal calculateDeclaredCapacityKw(Map<String, BigDecimal> historicalAvgByTime,
                                                         LocalDateTime windowStart,
                                                         LocalDateTime windowEnd) {
        BigDecimal historicalAvg = averageValuesInWindow(historicalAvgByTime, windowStart, windowEnd);
        if (historicalAvg == null || historicalAvg.compareTo(BigDecimal.ZERO) <= 0) {
            return BigDecimal.ZERO;
        }
        return applyDeclaredCapacityFormula(historicalAvg, windowStart, windowEnd);
    }

    /**
     * 预估响应容量 = 响应时段内今日负荷均值 − 响应时段内历史负荷均值 × 修正系数。
     */
    public static BigDecimal calculateEstimatedResponseCapacityKw(Map<String, BigDecimal> todayActualByTime,
                                                                  Map<String, BigDecimal> historicalAvgByTime,
                                                                  BigDecimal correctionFactorK,
                                                                  LocalDateTime windowStart,
                                                                  LocalDateTime windowEnd) {
        BigDecimal todayAvg = averageValuesInWindow(todayActualByTime, windowStart, windowEnd);
        if (todayAvg == null) {
            return BigDecimal.ZERO;
        }
        BigDecimal historicalAvg = averageValuesInWindow(historicalAvgByTime, windowStart, windowEnd);
        BigDecimal k = correctionFactorK != null ? correctionFactorK : K_DEFAULT;
        BigDecimal correctedHistorical = historicalAvg != null
                ? historicalAvg.multiply(k)
                : BigDecimal.ZERO;
        return scale(todayAvg.subtract(correctedHistorical));
    }

    private static BigDecimal applyDeclaredCapacityFormula(BigDecimal historicalAvg,
                                                           LocalDateTime windowStart,
                                                           LocalDateTime windowEnd) {
        BigDecimal windowHours = responseWindowHours(windowStart, windowEnd);
        return scale(historicalAvg.multiply(windowHours)
                .divide(DECLARED_CAPACITY_DIVISOR, 6, RoundingMode.HALF_UP));
    }

    public static List<DeclaredCapacityPointVO> buildDeclaredCapacityPoints(Map<String, BigDecimal> historicalAvgByTime,
                                                                              LocalDateTime windowStart,
                                                                              LocalDateTime windowEnd) {
        List<DeclaredCapacityPointVO> points = new ArrayList<>();
        if (historicalAvgByTime == null || historicalAvgByTime.isEmpty()) {
            return points;
        }
        LocalDate date = windowStart.toLocalDate();
        for (Map.Entry<String, BigDecimal> entry : historicalAvgByTime.entrySet()) {
            LocalDateTime pointTime = LocalDateTime.of(date, parseTimeKey(entry.getKey()));
            if (pointTime.isBefore(windowStart) || pointTime.isAfter(windowEnd)) {
                continue;
            }
            BigDecimal historicalAvg = entry.getValue() != null ? entry.getValue() : BigDecimal.ZERO;
            DeclaredCapacityPointVO point = new DeclaredCapacityPointVO();
            point.setTime(entry.getKey());
            point.setHistoricalAvgKw(scale(historicalAvg));
            point.setDeclaredCapacityKw(applyDeclaredCapacityFormula(
                    historicalAvg, windowStart, windowEnd));
            points.add(point);
        }
        return points;
    }

    /*
    public static BigDecimal mockDeclaredCapacityKw(Long siteId,
                                                    LocalDate responseDate,
                                                    LocalDateTime windowStart,
                                                    LocalDateTime windowEnd) {
        return calculateDeclaredCapacityKw(
                buildMockHistoricalWindow(siteId, responseDate, windowStart, windowEnd),
                windowStart,
                windowEnd);
    }

    public static List<DeclaredCapacityPointVO> buildMockDeclaredCapacityPoints(Long siteId,
                                                                                LocalDate responseDate,
                                                                                LocalDateTime windowStart,
                                                                                LocalDateTime windowEnd) {
        return buildDeclaredCapacityPoints(
                buildMockHistoricalWindow(siteId, responseDate, windowStart, windowEnd),
                windowStart,
                windowEnd);
    }

    private static Map<String, BigDecimal> buildMockHistoricalWindow(Long siteId,
                                                                     LocalDate responseDate,
                                                                     LocalDateTime windowStart,
                                                                     LocalDateTime windowEnd) {
        Map<String, BigDecimal> mockHistorical = new TreeMap<>();
        long seed = (siteId != null ? siteId : 0L) + responseDate.toEpochDay();
        LocalDateTime cursor = windowStart;
        while (!cursor.isAfter(windowEnd)) {
            double hourFactor = mockHourFactor(cursor.toLocalTime());
            double noise = 0.9 + pseudoRandom(seed, cursor.getHour() * 60 + cursor.getMinute(), 4) * 0.2;
            mockHistorical.put(cursor.toLocalTime().format(TIME_FMT),
                    BigDecimal.valueOf(900 * hourFactor * noise).setScale(2, RoundingMode.HALF_UP));
            cursor = cursor.plusMinutes(INTERVAL_MINUTES);
        }
        return mockHistorical;
    }
    */

    private static BigDecimal averageValuesInWindow(Map<String, BigDecimal> valuesByTime,
                                                    LocalDateTime windowStart,
                                                    LocalDateTime windowEnd) {
        if (valuesByTime == null || valuesByTime.isEmpty()) {
            return null;
        }
        List<BigDecimal> values = new ArrayList<>();
        LocalDate date = windowStart.toLocalDate();
        for (Map.Entry<String, BigDecimal> entry : valuesByTime.entrySet()) {
            LocalDateTime pointTime = LocalDateTime.of(date, parseTimeKey(entry.getKey()));
            if (pointTime.isBefore(windowStart) || pointTime.isAfter(windowEnd)) {
                continue;
            }
            if (entry.getValue() != null) {
                values.add(entry.getValue());
            }
        }
        return values.isEmpty() ? null : average(values);
    }

    private static Map<String, List<BigDecimal>> bucketSeries(TreeMap<LocalDateTime, BigDecimal> series,
                                                              int intervalMinutes) {
        Map<String, List<BigDecimal>> buckets = new TreeMap<>();
        for (Map.Entry<LocalDateTime, BigDecimal> entry : series.entrySet()) {
            if (entry.getValue() == null) {
                continue;
            }
            String bucket = toBucketKey(entry.getKey(), intervalMinutes);
            buckets.computeIfAbsent(bucket, key -> new ArrayList<>()).add(entry.getValue());
        }
        return buckets;
    }

    private static String toBucketKey(LocalDateTime timestamp, int intervalMinutes) {
        int totalMinutes = timestamp.getHour() * 60 + timestamp.getMinute();
        int bucketMinutes = (totalMinutes / intervalMinutes) * intervalMinutes;
        return LocalTime.of(bucketMinutes / 60, bucketMinutes % 60).format(TIME_FMT);
    }

    private static LocalTime parseTimeKey(String timeKey) {
        if (timeKey.length() == 5) {
            return LocalTime.parse(timeKey + ":00", TIME_WITH_SEC_FMT);
        }
        return LocalTime.parse(timeKey, TIME_WITH_SEC_FMT);
    }

    private static BigDecimal sumValuesInWindow(Map<String, BigDecimal> valuesByTime,
                                                LocalDateTime windowStart,
                                                LocalDateTime windowEnd) {
        if (valuesByTime == null || valuesByTime.isEmpty()) {
            return null;
        }
        BigDecimal total = BigDecimal.ZERO;
        int count = 0;
        LocalDate date = windowStart.toLocalDate();
        for (Map.Entry<String, BigDecimal> entry : valuesByTime.entrySet()) {
            LocalDateTime pointTime = LocalDateTime.of(date, parseTimeKey(entry.getKey()));
            if (pointTime.isBefore(windowStart) || pointTime.isAfter(windowEnd)) {
                continue;
            }
            if (entry.getValue() != null) {
                total = total.add(entry.getValue());
                count++;
            }
        }
        return count == 0 ? null : total;
    }

    private static TreeMap<LocalDateTime, BigDecimal> pickSeries(Map<String, TreeMap<LocalDateTime, BigDecimal>> metricMap) {
        if (metricMap == null || metricMap.isEmpty()) {
            return null;
        }
        for (String metric : POWER_METRICS) {
            TreeMap<LocalDateTime, BigDecimal> series = metricMap.get(metric);
            if (series != null && !series.isEmpty()) {
                return series;
            }
        }
        for (TreeMap<LocalDateTime, BigDecimal> series : metricMap.values()) {
            if (series != null && !series.isEmpty()) {
                return series;
            }
        }
        return null;
    }

    private static BigDecimal sum(List<BigDecimal> values) {
        if (values == null || values.isEmpty()) {
            return BigDecimal.ZERO;
        }
        BigDecimal total = BigDecimal.ZERO;
        for (BigDecimal value : values) {
            if (value != null) {
                total = total.add(value);
            }
        }
        return total;
    }

    private static BigDecimal average(List<BigDecimal> values) {
        if (values == null || values.isEmpty()) {
            return BigDecimal.ZERO;
        }
        return sum(values).divide(BigDecimal.valueOf(values.size()), 4, RoundingMode.HALF_UP);
    }

    private static BigDecimal scale(BigDecimal value) {
        if (value == null) {
            return null;
        }
        return value.setScale(2, RoundingMode.HALF_UP);
    }

    /*
    private static double mockHourFactor(LocalTime time) {
        int hour = time.getHour();
        int minute = time.getMinute();
        double h = hour + minute / 60.0;
        if (h < 6) {
            return 0.45 + h / 6 * 0.15;
        }
        if (h < 9) {
            return 0.6 + (h - 6) / 3 * 0.35;
        }
        if (h < 12) {
            return 0.95 + (h - 9) / 3 * 0.05;
        }
        if (h < 14) {
            return 0.85;
        }
        if (h < 17) {
            return 0.9 + (h - 14) / 3 * 0.1;
        }
        if (h < 21) {
            return 1.0 - (h - 17) / 4 * 0.35;
        }
        return 0.5 - (h - 21) / 3 * 0.15;
    }

    private static double pseudoRandom(long seed, int minuteOfDay, int seriesSalt) {
        long mixed = seed * 31L + minuteOfDay * 17L + seriesSalt * 13L;
        return (mixed % 1000) / 1000.0;
    }
    */
}
