package com.njcn.gather.detection.util;

import cn.hutool.core.collection.CollUtil;
import cn.hutool.core.util.NumberUtil;
import cn.hutool.core.util.ObjectUtil;
import com.njcn.gather.detection.pojo.po.DevData;
import lombok.extern.slf4j.Slf4j;

import java.io.File;
import java.math.BigDecimal;
import java.math.RoundingMode;
import java.time.LocalDateTime;
import java.time.ZoneId;
import java.time.ZonedDateTime;
import java.time.format.DateTimeFormatter;
import java.time.format.DateTimeParseException;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import java.util.stream.Collectors;
import java.util.stream.Stream;

/**
 * 检测工具类
 * <p>
 * 提供电能质量检测相关的数据处理、时间转换、统计计算等工具方法。
 * 主要功能包括：
 * <ul>
 *   <li>相角矫正和标准化</li>
 *   <li>设备数据时间对齐判断</li>
 *   <li>时间格式转换和毫秒数计算</li>
 *   <li>数值零值判断</li>
 *   <li>统计数据处理（CP95分位数、部分值、平均值等）</li>
 *   <li>数据排序和索引计算</li>
 * </ul>
 *
 * @author caozehui
 * @version 1.0
 * @since 2025-07-28
 */
@Slf4j
public class DetectionUtil {
    /**
     * ISO 8601日期时间格式化器
     * 用于解析和格式化符合ISO 8601标准的日期时间字符串
     */
    public static final DateTimeFormatter FORMATTER = DateTimeFormatter.ISO_DATE_TIME;


    /**
     * 毫秒转秒的转换因子
     */
    private static final long MILLIS_TO_SECONDS = 1000L;

    /**
     * 毫秒转分钟的转换因子
     */
    private static final long MILLIS_TO_MINUTES = 1000 * 60L;

    /**
     * 时间对齐判断的容差毫秒数
     * 当两个时间戳差值小于此值时，认为时间是对齐的
     */
    private static final long TIME_ALIGNMENT_TOLERANCE_MS = 100L;

    /**
     * 角度相关常量
     */
    private static final double ANGLE_180 = 180.0;
    private static final double ANGLE_360 = 360.0;
    private static final double ANGLE_MINUS_180 = -180.0;

    /**
     * CP95算法相关常量
     */
    private static final int CP95_DATA_SIZE_THRESHOLD = 21;
    private static final double CP95_PERCENTILE = 0.05;
    private static final int CP95_SMALL_DATA_INDEX = 1;

    /**
     * 数据处理相关常量
     */
    private static final int MIN_DATA_SIZE_FOR_SECTION_VALUE = 2;


    /**
     * 相角矫正到统一区间[-180°, 180°]
     * <p>
     * 将任意角度值标准化到[-180°, 180°]范围内，便于相角比较和计算。
     * 使用模运算处理任意大小的角度值，能正确处理超出多个360°范围的情况。
     * <p>
     * 示例：
     * <ul>
     *   <li>-1080° → 0°</li>
     *   <li>-900° → 180°</li>
     *   <li>720° → 0°</li>
     *   <li>450° → 90°</li>
     * </ul>
     *
     * @param phase 待矫正的相角值（单位：度）
     * @return 矫正后的相角值，范围在[-180°, 180°]内
     */
    public static Double adjustPhase(Double phase) {
        if (phase == null) {
            return null;
        }
        // 使用模运算将角度标准化到[-180, 180]范围
        double normalizedPhase = phase % ANGLE_360;
        // 处理超出[-180, 180]范围的情况
        if (normalizedPhase > ANGLE_180) {
            normalizedPhase -= ANGLE_360;
        } else if (normalizedPhase < ANGLE_MINUS_180) {
            normalizedPhase += ANGLE_360;
        }

        return normalizedPhase;
    }

    /**
     * 判断被检设备数据与标准设备数据的时间是否对齐
     * <p>
     * 数据对齐的判断标准（满足任一条件即可）：
     * 1. 将时间戳四舍五入到秒级精度后完全相等（处理跨秒边界情况）
     * 2. 两个时间戳的毫秒差值小于容差值（处理同秒内的精确对齐）
     * <p>
     * 示例：499ms vs 509ms → 四舍五入后不同秒，但差值仅10ms < 100ms → 对齐
     *
     * @param devData         被检设备数据，包含时间戳信息
     * @param standardDevData 标准设备数据，包含时间戳信息
     * @return true表示数据时间对齐，false表示不对齐
     */
    public static boolean isAlignData(DevData devData, DevData standardDevData, int type) {
        if (ObjectUtil.isNotNull(devData) && ObjectUtil.isNotNull(standardDevData)) {
            // 获取两个设备数据的时间戳（毫秒）
            long devMillis = getMillis(devData.getTime());
            long standardMillis = getMillis(standardDevData.getTime());
            if (type == 1) { // 实时数据
                // 方式1：将时间戳转换为秒级精度进行比较（处理跨秒边界情况）
                BigDecimal devSeconds = BigDecimal.valueOf(devMillis).divide(BigDecimal.valueOf(MILLIS_TO_SECONDS), 0, RoundingMode.HALF_UP);
                BigDecimal standardSeconds = BigDecimal.valueOf(standardMillis).divide(BigDecimal.valueOf(MILLIS_TO_SECONDS), 0, RoundingMode.HALF_UP);
                if (devSeconds.compareTo(standardSeconds) == 0) {
                    return true;
                }

                // 方式2：毫秒级时间差小于容差值也认为是对齐的（处理精确对齐）
                if (Math.abs(devMillis - standardMillis) < TIME_ALIGNMENT_TOLERANCE_MS) {
                    return true;
                }
            }
            if (type == 2) { //统计数据
                BigDecimal devSeconds = BigDecimal.valueOf(devMillis).divide(BigDecimal.valueOf(MILLIS_TO_MINUTES), 0, RoundingMode.HALF_UP);
                BigDecimal standardSeconds = BigDecimal.valueOf(standardMillis).divide(BigDecimal.valueOf(MILLIS_TO_MINUTES), 0, RoundingMode.HALF_UP);
                if (devSeconds.compareTo(standardSeconds) == 0) {
                    return true;
                }
            }
        }

        return false;
    }

    /**
     * 将字符串日期时间转换为LocalDateTime对象
     * <p>
     * 使用指定的格式解析时间字符串，支持带时区的ISO 8601格式。
     * 解析时会将时间统一转换为UTC时区的LocalDateTime。
     *
     * @param dateTimeStr 日期时间字符串，应符合指定格式
     * @param formatter   时间格式化器，用于解析字符串
     * @return 解析成功返回LocalDateTime对象，解析失败返回null
     */
    public static LocalDateTime timeFormat(String dateTimeStr, DateTimeFormatter formatter) {
        try {
            // 使用UTC时区解析时间字符串
            ZonedDateTime zonedDateTime = ZonedDateTime.parse(dateTimeStr, formatter.withZone(ZoneId.of("UTC")));
            // 转换为LocalDateTime对象
            return zonedDateTime.toLocalDateTime();
        } catch (DateTimeParseException e) {
            log.error("日期时间字符串格式错误: {}", e.getMessage());
            return null;
        }
    }

    /**
     * 获取字符串日期时间对应的UTC毫秒时间戳
     * <p>
     * 使用默认的ISO_DATE_TIME格式解析时间字符串，
     * 并转换为UTC时区的毫秒时间戳。
     *
     * @param dateTimeStr ISO 8601格式的日期时间字符串
     * @return UTC时区的毫秒时间戳
     */
    public static long getMillis(String dateTimeStr) {
        if (dateTimeStr == null || dateTimeStr.trim().isEmpty()) {
            throw new IllegalArgumentException("日期时间字符串不能为空");
        }
        LocalDateTime localDateTime = timeFormat(dateTimeStr, FORMATTER);
        if (localDateTime == null) {
            throw new IllegalArgumentException("无法解析日期时间字符串: " + dateTimeStr);
        }
        return getMillis(localDateTime);
    }


    /**
     * 获取LocalDateTime对应的UTC毫秒时间戳
     * <p>
     * 将LocalDateTime对象转换为UTC时区的毫秒时间戳。
     *
     * @param localDateTime 本地日期时间对象
     * @return UTC时区的毫秒时间戳
     */
    public static long getMillis(LocalDateTime localDateTime) {
        if (localDateTime == null) {
            throw new IllegalArgumentException("LocalDateTime参数不能为null");
        }
        return localDateTime.atZone(ZoneId.of("UTC")).toInstant().toEpochMilli();
    }

    /**
     * 判断数值是否为零（在容差范围内）
     * <p>
     * 使用BigDecimal进行精确计算，避免浮点数精度问题。
     * 当数值的绝对值小于预设阈值（0.01）时，认为该数值为零。
     * 主要用于电流等物理量的零值判断。
     *
     * @param value        待判断的数值，null值被认为是零
     * @param ratedCurrent 额定电流，用于计算阈值
     * @return true表示数值为零（在容差范围内），false表示非零
     */
    public static boolean isZero(Double value, Double ratedCurrent) {
        if (value == null) {
            return true;
        }
        double threshold = 0.01 * ratedCurrent;
        BigDecimal bd = BigDecimal.valueOf(value);
        return bd.subtract(BigDecimal.ZERO).abs().compareTo(BigDecimal.valueOf(threshold)) < 0;
    }

    /**
     * 获取CP95分位数值
     * <p>
     * CP95表示95%分位数，即有95%的数据小于等于此值。
     * 算法逻辑：
     * <ul>
     *   <li>数据量=1时：返回该数据</li>
     *   <li>数据量<21时：返回第2个数据（索引1）</li>
     *   <li>数据量≥21时：计算5%位置的数据（适用于从大到小排序的数据）</li>
     * </ul>
     *
     * @param t 已排序的数据列表（从大到小排序）
     * @return CP95分位数值列表，包含单个元素
     */
    public static List<Double> getCP95Doubles(List<Double> t) {
        if (CollUtil.isEmpty(t)) {
            return new ArrayList<>();
        }

        // 单个数据直接返回
        if (t.size() == 1) {
            return new ArrayList<>(t);
        }

        // 数据量较少时，取第2个数据作为CP95值
        if (t.size() < CP95_DATA_SIZE_THRESHOLD) {
            return t.subList(CP95_SMALL_DATA_INDEX, CP95_SMALL_DATA_INDEX + 1);
        }

        // 数据量充足时，计算真正的95%分位数
        // 由于数据已从大到小排序，95%分位数位于5%位置
        int cp95Index = (int) Math.ceil(t.size() * CP95_PERCENTILE) - 1;
        return t.subList(cp95Index, cp95Index + 1);
    }

    /**
     * 获取CP95分位数值在列表中的索引位置
     * <p>
     * 计算CP95分位数在已排序列表中的索引位置。
     * 索引计算规则与getCP95Doubles方法保持一致。
     *
     * @param t 已排序的数据列表（从大到小排序）
     * @return CP95分位数的索引位置，列表为空时返回-1
     */
    public static int getCP95Idx(List<Double> t) {
        if (CollUtil.isEmpty(t)) {
            return -1;
        }

        // 单个数据返回索引0
        if (t.size() == 1) {
            return 0;
        }

        // 数据量较少时，返回索引1
        if (t.size() < CP95_DATA_SIZE_THRESHOLD) {
            return CP95_SMALL_DATA_INDEX;
        }

        // 数据量充足时，计算95%分位数的索引位置
        // 由于数据已从大到小排序，95%分位数索引为5%位置
        return (int) Math.ceil(t.size() * CP95_PERCENTILE) - 1;
    }

    /**
     * 获取部分值（去除最大最小值后的数据）
     * <p>
     * 用于数据预处理，去除可能的异常值。
     * 算法逻辑：
     * <ul>
     *   <li>数据量≤2时：返回原数据副本</li>
     *   <li>数据量>2时：移除一个最大值和一个最小值后返回剩余数据</li>
     * </ul>
     * 注意：该方法不会修改原始列表，而是返回新的列表。
     *
     * @param t 原始数据列表
     * @return 去除最大最小值后的数据列表副本
     */
    public static List<Double> getSectionValueDoubles(List<Double> t) {
        if (CollUtil.isEmpty(t) || t.size() <= MIN_DATA_SIZE_FOR_SECTION_VALUE) {
            return new ArrayList<>(t);
        }

        // 创建副本避免修改原始列表
        List<Double> result = new ArrayList<>(t);
        Double max = Collections.max(result);
        Double min = Collections.min(result);
        result.remove(max);
        result.remove(min);
        return result;
    }

    /**
     * 计算数据列表的算术平均值
     * <p>
     * 对输入的数值列表计算算术平均值，并以单元素列表形式返回。
     * 空列表会返回空列表。
     *
     * @param t 数值列表
     * @return 包含平均值的单元素列表，输入为空时返回空列表
     */
    public static List<Double> getAvgDoubles(List<Double> t) {
        if (CollUtil.isEmpty(t)) {
            return new ArrayList<>();
        }

        // 计算列表中所有数值的算术平均值
        double average = t.stream().mapToDouble(Double::doubleValue).average().orElse(0.0);
        // 将平均值包装为单元素列表返回
        return Collections.singletonList(average);
    }

    /**
     * 对数据列表进行排序并返回原始索引序列
     * <p>
     * 使用选择排序算法对列表进行排序，同时跟踪每个元素的原始索引位置。
     * 这样可以在数据排序后仍然知道每个数据在原始列表中的位置。
     *
     * <b>注意：</b>该方法会直接修改输入的列表。
     *
     * @param list  待排序的数据列表（会被直接修改）
     * @param isAsc 排序方式，true为升序，false为降序
     * @return 排序后各元素在原始列表中的索引位置
     */
    public static List<Integer> sort(List<Double> list, Boolean isAsc) {
        if (CollUtil.isEmpty(list)) {
            return new ArrayList<>();
        }
        if (isAsc == null) {
            throw new IllegalArgumentException("排序方式参数不能为null");
        }
        // 创建索引列表，记录每个元素的原始位置
        List<Integer> indexList = Stream.iterate(0, i -> i + 1).limit(list.size()).collect(Collectors.toList());
        // 使用选择排序算法，同时维护索引映射
        for (int i = 0; i < list.size(); i++) {
            // 当前轮次要放置的目标位置
            int targetIdx = i;
            // 在未排序部分寻找最值
            for (int j = i + 1; j < list.size(); j++) {
                if (isAsc) {
                    // 升序：寻找最小值
                    if (list.get(j) < list.get(targetIdx)) {
                        targetIdx = j;
                    }
                } else {
                    // 降序：寻找最大值
                    if (list.get(j) > list.get(targetIdx)) {
                        targetIdx = j;
                    }
                }
            }
            // 交换数据值和对应的索引
            if (targetIdx != i) {
                // 交换数据值
                double temp = list.get(i);
                list.set(i, list.get(targetIdx));
                list.set(targetIdx, temp);

                // 交换对应的原始索引
                int tempIdx = indexList.get(i);
                indexList.set(i, indexList.get(targetIdx));
                indexList.set(targetIdx, tempIdx);
            }
        }
        return indexList;
    }

    /**
     * 根据idxList索引列表从oldList中获取新数组
     *
     * @param oldList
     * @param idxList
     * @return
     */
    public static List<Double> getNewArray(List<Double> oldList, List<Integer> idxList) {
        if (CollUtil.isNotEmpty(oldList) && CollUtil.isNotEmpty(idxList)) {
            if (CollUtil.max(idxList) > oldList.size() - 1 || CollUtil.min(idxList) < 0) {
                return null;
            }
            List<Double> newList = new ArrayList<>();
            for (int i = 0; i < idxList.size(); i++) {
                newList.add(oldList.get(idxList.get(i)));
            }
            return newList;
        }
        return Collections.emptyList();
    }

    /**
     * 检查文件是否存在
     */
    public static void checkFileExists(String filePath, String description) {
        File file = new File(filePath);
        if (!file.exists()) {
            System.err.println("警告: " + description + " 不存在: " + filePath);
            System.err.println("请确保文件路径正确，或修改测试中的文件路径");
        } else {
            System.out.println(description + " 存在: " + filePath);
            System.out.println("  文件大小: " + file.length() + " bytes");
        }
    }

    /**
     * 获取皮尔逊系数
     *
     * <p>相关系数绝对值|r|所代表的相关强度</p>
     * <ul>
     *     <li>0.8 - 1.0：极强相关</li>
     *     <li>0.6 - 0.8：强相关</li>
     *     <li>0.4 - 0.6：中等程度相关</li>
     *     <li>0.2 - 0.4：弱相关</li>
     *     <li>0.0 - 0.2：极弱相关或无相关</li>
     * </ul>
     *
     * @return
     */
    public static BigDecimal getPearsonCorrelationCoefficient(List<Double> x, List<Double> y) {
        Double xAvg = x.parallelStream().mapToDouble(Double::doubleValue).average().orElse(0.0);
        Double yAvg = y.parallelStream().mapToDouble(Double::doubleValue).average().orElse(0.0);

        // 协方差求和
        BigDecimal covarianceSum = BigDecimal.ZERO;
        // x的方差
        BigDecimal varianceX = BigDecimal.ZERO;
        // y的方差
        BigDecimal varianceY = BigDecimal.ZERO;
        for (int i = 0; i < x.size(); i++) {
            covarianceSum = covarianceSum.add(BigDecimal.valueOf((x.get(i) - xAvg) * (y.get(i) - yAvg)));
            varianceX = varianceX.add(BigDecimal.valueOf(Math.pow(x.get(i) - xAvg, 2)));
            varianceY = varianceY.add(BigDecimal.valueOf(Math.pow(y.get(i) - yAvg, 2)));
        }
        BigDecimal fm = BigDecimal.valueOf(Math.sqrt(varianceX.doubleValue()) * Math.sqrt(varianceY.doubleValue()));
        if (NumberUtil.equals(fm.doubleValue(), BigDecimal.ZERO.doubleValue())) {
            if (NumberUtil.equals(covarianceSum, BigDecimal.ZERO)) {
                return BigDecimal.valueOf(1.0);
            } else {
                return BigDecimal.ZERO;
            }
        } else {
            return BigDecimal.valueOf(Math.abs(covarianceSum.divide(fm, 8, BigDecimal.ROUND_HALF_UP).doubleValue())); //相关系数
        }
    }

    /**
     * 比值一致性，获取CV系数
     *
     * <p>一致性等级</p>
     * <ul>
     *     <li>CV<1%：非常优秀</li>
     *     <li>1%≤CV＜5%：优良</li>
     *     <li>5%≤CV＜10%：良好</li>
     *     <li>10%≤CV＜20%：中等</li>
     *     <li>20%≤CV：较差</li>
     * </ul>
     *
     * @param x
     * @param y
     * @return
     */
    public static BigDecimal getCvCoefficient(List<Double> x, List<Double> y) {
        List<BigDecimal> ratioList = new ArrayList<>();
        for (int i = 0; i < x.size(); i++) {
            ratioList.add(BigDecimal.valueOf(x.get(i) / y.get(i)));
        }

        BigDecimal avg = BigDecimal.valueOf(ratioList.parallelStream().mapToDouble(BigDecimal::doubleValue).average().orElse(0.0));
        BigDecimal variance = BigDecimal.valueOf(
                ratioList.parallelStream().mapToDouble(BigDecimal::doubleValue)
                        .map(v -> Math.pow(v - avg.doubleValue(), 2)).average().orElse(0.0));

        return BigDecimal.valueOf(Math.sqrt(variance.doubleValue()) / avg.doubleValue());
    }

    /**
     * 计算新的系数
     *
     * @param devList
     * @param stdDevList
     * @return
     */
    public static Double getNewXi(List<Double> devList, List<Double> stdDevList) {
        ArrayList<Double> xiList = new ArrayList<>();
        for (int i = 0; i < stdDevList.size(); i++) {
            if (!stdDevList.get(i).equals(0)) {
                BigDecimal bigDecimalDev = BigDecimal.valueOf(devList.get(i));
                BigDecimal bigDecimalStdDev = BigDecimal.valueOf(stdDevList.get(i));
                xiList.add((bigDecimalDev.subtract(bigDecimalStdDev)).divide(BigDecimal.valueOf(2.0).multiply(bigDecimalDev), 8, BigDecimal.ROUND_HALF_UP).doubleValue() + 1.0);
            }
        }
        return xiList.stream().mapToDouble(Double::doubleValue).average().orElse(1.0);
    }
}
