package com.ningdatech.carapi.gps.task;

import cn.hutool.core.collection.CollUtil;
import cn.hutool.core.collection.ListUtil;
import cn.hutool.core.date.StopWatch;
import com.alibaba.fastjson.JSON;
import com.alibaba.fastjson.JSONObject;
import com.baomidou.mybatisplus.core.toolkit.Wrappers;
import com.ningdatech.carapi.gps.manage.GpsDataPullManage;
import com.ningdatech.carapi.gps.model.entity.DataAccessGpsRealTimeData;
import com.ningdatech.carapi.gps.service.IDataAccessGpsRealTimeDataService;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.lang3.StringUtils;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.scheduling.annotation.Scheduled;
import org.springframework.stereotype.Component;

import java.sql.Connection;
import java.sql.DriverManager;
import java.sql.Statement;
import java.time.Instant;
import java.time.LocalDateTime;
import java.time.ZoneId;
import java.time.format.DateTimeFormatter;
import java.util.*;
import java.util.concurrent.CountDownLatch;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.TimeUnit;
import java.util.stream.Collectors;

/**
 * @author CMM
 * GPS实时数据拉取定时任务
 * @since 2024/10/12 10:39
 */
@Component
@Slf4j
@RequiredArgsConstructor
public class GpsRealTimeDataPullTask {

    @Value("${task.switch.is-open}")
    private boolean flag;

    @Value("${task.gps-data-pull.domain}")
    private String domain;

    @Value("${task.gps-data-pull.real-time-data-url}")
    private String realTimeDataUrl;

    @Value("${task.gps-data-pull.key}")
    private String key;

    private final GpsDataPullManage gpsDataPullManage;
    private final IDataAccessGpsRealTimeDataService dataAccessGpsRealTimeDataService;

    @Value("${spring.datasource.url}")
    private String dataBaseUrl;
    @Value("${spring.datasource.username}")
    private String username;
    @Value("${spring.datasource.password}")
    private String password;

    // 定时更新车辆实时GPS数据 每5分钟一次 每次覆盖更新50万条数据
    @Scheduled(cron = "0 */5 * * * ?")
    public void doTask() throws Exception {
        if (!flag){
            log.info("GPS实时数据拉取定时任务未开启!");
            return;
        }
        log.info("=========== GPS实时数据拉取 ======== 任务开始");
        StopWatch stopWatch = new StopWatch();
        stopWatch.start();
        gpsDataPullManage.trustAllCertificates();

        // 经纬度范围
        // 最小纬度
        String sLon = null;
        // 最大纬度
        String eLon = null;
        // 最小经度
        String sLat = null;
        // 最大经度
        String eLat = null;

        // 获取当前表中的最大更新时间
        long maxUpdateTime = LocalDateTime.now().atZone(ZoneId.of("Asia/Shanghai")).toInstant().toEpochMilli();
        Optional<DataAccessGpsRealTimeData> max = dataAccessGpsRealTimeDataService.list(Wrappers.lambdaQuery(DataAccessGpsRealTimeData.class)
                .select(DataAccessGpsRealTimeData::getUpdateTime)).stream().max(Comparator.comparing(DataAccessGpsRealTimeData::getUpdateTime));
        if (max.isPresent()){
            maxUpdateTime = max.get().getUpdateTime().atZone(ZoneId.of("Asia/Shanghai")).toInstant().toEpochMilli();
        }
        log.info("当前表中最大更新时间：{}", Instant.ofEpochMilli(maxUpdateTime).atZone(ZoneId.of("Asia/Shanghai")).toLocalDateTime().format(DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss")));
        Map<String, String> requestParams = assemblyParams(key,String.valueOf(maxUpdateTime), sLon, eLon, sLat, eLat);
        String requestUrl = domain + realTimeDataUrl;
        String response = gpsDataPullManage.sendPostRequest(requestUrl, requestParams);
        if (response == null) {
            log.error("下载路径请求失败！");
            return;
        }
        JSONObject object = JSONObject.parseObject(response);
        System.out.println("Response from server: " + response);
        String downloadUrl = object.getString("data");
        if (downloadUrl == null){
            log.error("下载路径请求失败！");
            return;
        }

        String downloadPath = domain + downloadUrl;
        String result = gpsDataPullManage.sendDownloadRequest(downloadPath);
        if (result == null){
            log.error("文件下载失败！");
            return;
        }
        List<DataAccessGpsRealTimeData> dataList = JSON.parseArray(result, com.alibaba.fastjson.JSONObject.class).stream().map(jsonObject -> {
            String vehicleNo = jsonObject.getString("vehicle_no");
            Integer color = jsonObject.getInteger("color");
            // 纬度
            String longitude = jsonObject.getString("lon");
            // 经度
            String latitude = jsonObject.getString("lat");
            String direction = jsonObject.getString("direction");
            String gpsSpeed = jsonObject.getString("gps_speed");
            Long gpsTime = jsonObject.getLong("gps_time");
            DataAccessGpsRealTimeData dataAccessGpsRealTimeData = new DataAccessGpsRealTimeData();
            dataAccessGpsRealTimeData.setCarPlate(vehicleNo);
            dataAccessGpsRealTimeData.setPlateColor(color.toString());
            dataAccessGpsRealTimeData.setCarLongitude(latitude);
            dataAccessGpsRealTimeData.setCarLatitude(longitude);
            dataAccessGpsRealTimeData.setCarDirection(direction);
            dataAccessGpsRealTimeData.setCarVelocity(gpsSpeed);
            // 将毫秒时间戳转换为Instant对象
            Instant instant = Instant.ofEpochMilli(gpsTime);
            // 使用北京时间时区转换为LocalDateTime
            LocalDateTime localDateTime = instant.atZone(ZoneId.of("Asia/Shanghai")).toLocalDateTime();
            dataAccessGpsRealTimeData.setUpdateTime(localDateTime);
            dataAccessGpsRealTimeData.setCreateTime(LocalDateTime.now());
            return dataAccessGpsRealTimeData;
        }).collect(Collectors.toList());
        if (CollUtil.isEmpty(dataList)){
            log.info("数据为空！");
            return;
        }

        // 获取此次拉取到的数据中上传时间大于上次拉取数据的最大上报时间的数据
        long finalMaxUpdateTime = maxUpdateTime;
        dataList = dataList.stream().filter(e -> e.getUpdateTime().atZone(ZoneId.of("Asia/Shanghai")).toInstant().toEpochMilli() > finalMaxUpdateTime).collect(Collectors.toList());
        // 如果没有 说明没有新上报的数据 不更新
        if (CollUtil.isEmpty(dataList)){
            log.info("没有新上报的数据！");
            return;
        }
        log.info("本次拉取到{}条数据", dataList.size());
        // 将数据库中的数据根据更新时间排序 按更新时间 从小到大 根据对应id删除此次拉取到的数据量
        List<DataAccessGpsRealTimeData> dataAccessGpsRealTimeDataList = dataAccessGpsRealTimeDataService.list(Wrappers.lambdaQuery(DataAccessGpsRealTimeData.class)
                        .select(DataAccessGpsRealTimeData::getUpdateTime, DataAccessGpsRealTimeData::getRecordId))
                .stream().sorted(Comparator.comparing(DataAccessGpsRealTimeData::getUpdateTime)).collect(Collectors.toList());
        // 截取dataList.size()条数据
        dataAccessGpsRealTimeDataList = dataAccessGpsRealTimeDataList.subList(0, dataList.size());
        // 从旧数据中删除这些数据
        List<Long> recordIdList = dataAccessGpsRealTimeDataList.stream().map(DataAccessGpsRealTimeData::getRecordId).collect(Collectors.toList());
        String tableName = "nd_data_access_gps_real_time_data";
        dataAccessGpsRealTimeDataService.removeByRecordIds(tableName,recordIdList);
        // 先清空表中的数据
        //truncateTable();

        // 批量保存数据 每批保存1万条
        // 分批
        List<List<DataAccessGpsRealTimeData>> batchList = new ArrayList<>(ListUtil.partition(new ArrayList<>(dataList), 10000));
        // 创建线程池 线程数是 分批的数量
        ExecutorService executorService = Executors.newFixedThreadPool(batchList.size());
        // 线程计数器,就是 分批的数
        CountDownLatch countDownLatch = new CountDownLatch(batchList.size());
        batchList.forEach(e -> {
            // 每个分批用一个线程执行
            executorService.execute(() -> dataAccessGpsRealTimeDataService.saveBatch(e, 10000));
            countDownLatch.countDown();
        });
        // 等待所有线程执行完成
        countDownLatch.await();
        executorService.shutdown();
        executorService.awaitTermination(5, TimeUnit.MINUTES);
        if (executorService.isTerminated()) {
            log.info("子线程执行完毕");
            log.info("主线程开始");
        }
        stopWatch.stop();
        log.info("=========== GPS实时数据拉取 ======== 任务结束 {}s",stopWatch.getTotalTimeSeconds());
    }

    private void truncateTable() {
        // SQL 语句清空表
        String truncateTableSQL = "TRUNCATE TABLE nd_data_access_gps_real_time_data";

        Connection conn = null;
        Statement stmt = null;

        try {
            // 加载数据库驱动
            Class.forName("com.mysql.cj.jdbc.Driver");
            // 建立连接
            conn = DriverManager.getConnection(dataBaseUrl, username, password);
            // 创建 Statement 对象
            stmt = conn.createStatement();
            // 执行 SQL 语句
            stmt.executeUpdate(truncateTableSQL);
            log.info("表已被清空，自增主键已重置。");
        } catch (Exception e) {
            log.error("Error truncating table: {}", e.getMessage());
        } finally {
            // 关闭资源
            try {
                if (stmt != null) {
                    stmt.close();
                }
                if (conn != null) {
                    conn.close();
                }
            } catch (Exception e) {
                log.error("Error closing resources: {}", e.getMessage());
            }
        }
    }

    private Map<String, String> assemblyParams(String key, String queryTime, String sLon, String eLon, String sLat, String eLat) {
        long timeNow = System.currentTimeMillis() / 1000;
        String token = gpsDataPullManage.generateMD5Token(timeNow, key);

        Map<String, String> params = new HashMap<>();
        params.put("timeNow", String.valueOf(timeNow));
        params.put("token", token);
        if (StringUtils.isNotBlank(queryTime)) {
            params.put("timeLine", queryTime);
        }
        if (StringUtils.isNotBlank(sLon)) {
            params.put("Slon", sLon);
        }
        if (StringUtils.isNotBlank(eLon)) {
            params.put("Elon", eLon);
        }
        if (StringUtils.isNotBlank(sLat)) {
            params.put("Slat", sLat);
        }
        if (StringUtils.isNotBlank(eLat)) {
            params.put("Elat", eLat);
        }
        return params;
    }

}
