package com.fuyuanshen.global.mqtt.rule.xinghan;

import cn.hutool.core.bean.BeanUtil;
import cn.hutool.core.collection.CollUtil;
import cn.hutool.core.util.ObjectUtil;
import cn.hutool.core.util.StrUtil;
import cn.hutool.json.JSONArray;
import cn.hutool.json.JSONObject;
import com.alibaba.fastjson2.JSONWriter;
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.fuyuanshen.common.core.constant.GlobalConstants;
import com.fuyuanshen.common.core.utils.StringUtils;
import com.fuyuanshen.common.core.utils.date.DurationUtils;
import com.fuyuanshen.common.json.utils.JsonUtils;
import com.fuyuanshen.common.redis.utils.RedisUtils;
import com.fuyuanshen.equipment.domain.Device;
import com.fuyuanshen.equipment.domain.DeviceFenceAccessRecord;
import com.fuyuanshen.equipment.domain.DeviceGeoFence;
import com.fuyuanshen.equipment.domain.bo.DeviceAlarmBo;
import com.fuyuanshen.equipment.domain.bo.DeviceFenceAccessRecordBo;
import com.fuyuanshen.equipment.domain.vo.DeviceAlarmVo;
import com.fuyuanshen.equipment.domain.vo.DeviceGeoFenceVo;
import com.fuyuanshen.equipment.mapper.DeviceAlarmMapper;
import com.fuyuanshen.equipment.mapper.DeviceFenceAccessRecordMapper;
import com.fuyuanshen.equipment.mapper.DeviceGeoFenceMapper;
import com.fuyuanshen.equipment.service.DeviceService;
import com.fuyuanshen.equipment.service.IDeviceAlarmService;
import com.fuyuanshen.equipment.service.IDeviceFenceAccessRecordService;
import com.fuyuanshen.equipment.service.IDeviceGeoFenceService;
import com.fuyuanshen.equipment.utils.map.AmapTrackUtil;
import com.fuyuanshen.equipment.utils.map.GetAddressFromLatUtil;
import com.fuyuanshen.equipment.utils.map.LngLonUtil;
import com.fuyuanshen.global.mqtt.base.MqttMessageRule;
import com.fuyuanshen.global.mqtt.base.MqttRuleContext;
import com.fuyuanshen.global.mqtt.base.MqttXinghanJson;
import com.fuyuanshen.global.mqtt.constants.DeviceRedisKeyConstants;
import com.fuyuanshen.global.mqtt.constants.XingHanCommandTypeConstants;
import com.fuyuanshen.global.mqtt.listener.domain.FunctionAccessStatus;
import com.fuyuanshen.web.enums.AlarmTypeEnum;
import lombok.AllArgsConstructor;
import lombok.Getter;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.redisson.api.RLock;
import org.redisson.api.RedissonClient;
import org.springframework.scheduling.annotation.Async;
import org.springframework.stereotype.Component;
import org.springframework.beans.factory.annotation.Autowired;

import java.text.DecimalFormat;
import java.time.Duration;
import java.util.*;
import java.util.concurrent.CompletableFuture;
import java.util.concurrent.TimeUnit;
import java.util.stream.Collectors;

import static com.fuyuanshen.common.core.constant.GlobalConstants.FUNCTION_ACCESS_KEY;
import static com.fuyuanshen.common.core.constant.GlobalConstants.GLOBAL_REDIS_KEY;
import static com.fuyuanshen.global.mqtt.constants.DeviceRedisKeyConstants.*;
import static com.fuyuanshen.global.mqtt.constants.DeviceRedisKeyConstants.DEVICE_KEY_PREFIX;

/**
 * 主动上报设备数据命令处理
 * 第一键值对，静电预警档位：3,2,1,0,分别表示高档/中档/低挡/关闭.
 * 第二键值对，照明档位，2,1,0,分别表示弱光/强光/关闭
 * 第三键值对，SOS档位，2,1,0, 分别表示红蓝模式/爆闪模式/关闭
 * 第四键值对，剩余照明时间，0-5999，单位分钟。
 * 第五键值对, 剩余电量百分比，0-100。
 * 第六键值对, 近电预警级别, 0-无预警，1-弱预警，2-中预警，3-强预警，4-非常强预警。
 * 第七键值对, 静止报警状态，0-未静止报警，1-正在静止报警。
 * 第八键值对, 4G信号强度，0-32，数值越大，信号越强。
 * 第九键值对，IMIE卡号
 * 第十键值对，经度
 * 第十一键值对，纬度
 */
@Component
@RequiredArgsConstructor
@Slf4j
public class XinghanDeviceDataRule implements MqttMessageRule {

    @Override
    public String getCommandType() {
        return XingHanCommandTypeConstants.XingHan_DEVICE_DATA;
    }

    @Autowired
    private ObjectMapper objectMapper;
    private final IDeviceAlarmService deviceAlarmService;
    private final DeviceService deviceService;
    private final DeviceAlarmMapper deviceAlarmMapper;
    private final AmapTrackUtil amapTrackUtil;
    private final IDeviceFenceAccessRecordService deviceFenceAccessRecordService;
    private final DeviceFenceAccessRecordMapper deviceFenceAccessRecordMapper;
    private final DeviceGeoFenceMapper deviceGeoFenceMapper;
    /** 位置未发生明显变化的距离阈值（米），可通过配置中心动态调整 */
    private final double MOVEMENT_THRESHOLD_METER = 10.0;

    @Override
    public void execute(MqttRuleContext context) {
        String functionAccess = FUNCTION_ACCESS_KEY + "DATA:" + context.getDeviceImei();
        try {
            // Latitude, longitude
            //主灯档位,激光灯档位,电量百分比,充电状态,电池剩余续航时间
            MqttXinghanJson deviceStatus = objectMapper.convertValue(context.getPayloadDict(), MqttXinghanJson.class);
            deviceStatus.setBatteryPercentage(deviceStatus.getStaPowerPercent().toString());
            // 发送设备状态和位置信息到Redis
            asyncSendDeviceDataToRedisWithFuture(context.getDeviceImei(),deviceStatus);
            RedisUtils.setCacheObject(functionAccess, FunctionAccessStatus.OK.getCode(), Duration.ofSeconds(20));
        } catch (Exception e) {
            log.error("处理上报数据命令时出错", e);
            RedisUtils.setCacheObject(functionAccess, FunctionAccessStatus.FAILED.getCode(), Duration.ofSeconds(20));
        }
    }

    /**
     * 发送设备状态信息和位置信息到Redis
     *
     * @param deviceImei 设备IMEI
     * @param deviceStatus 机器主动上报的状态信息
     */
    public void asyncSendDeviceDataToRedisWithFuture(String deviceImei, MqttXinghanJson deviceStatus) {
        CompletableFuture.runAsync(() -> {
            try {

                // 将设备状态信息存储到Redis中
                String deviceRedisKey = GlobalConstants.GLOBAL_REDIS_KEY+ DeviceRedisKeyConstants.DEVICE_KEY_PREFIX + deviceImei + DEVICE_STATUS_KEY_PREFIX;
                String deviceInfoJson = JsonUtils.toJsonString(deviceStatus);
                // 存储到Redis
                RedisUtils.setCacheObject(deviceRedisKey, deviceInfoJson);

                log.info("设备状态信息已异步发送到Redis: device={}, deviceInfoJson={}",
                        deviceImei, deviceInfoJson);

                //设备坐标缓存KEY
                String functionAccess = FUNCTION_ACCESS_KEY + deviceImei;
                // 异步发送经纬度到Redis
                asyncSendLocationToRedisWithFuture(deviceImei, deviceStatus.getStalatitude(), deviceStatus.getStalongitude());

                // 保存报警信息
                saveAlarm(deviceImei,deviceStatus);

            } catch (Exception e) {
                log.error("异步发送设备信息到Redis时出错: device={}, error={}", deviceImei, e.getMessage(), e);
            }
        });
    }

    /**
     * 入口：保存报警（SOS 与 Shake 完全独立）
     */
    public void saveAlarm(String deviceImei, MqttXinghanJson status) {
        try {
            int sos = Optional.ofNullable(status.getStaSOSGrade()).orElse(0);
            // 1. 处理 SOS 报警
            handleSingleAlarm(deviceImei,
                    sos > 0,
                    AlarmTypeEnum.SOS);
            int shake = Optional.ofNullable(status.getStaShakeBit()).orElse(0);
            // 2. 处理 Shake 报警
            handleSingleAlarm(deviceImei,
                    shake > 0,
                    AlarmTypeEnum.SHAKE);
        } catch (Exception e) {
            log.error("异步保存报警（SOS 与 Shake 完全独立）报错: device={}, error={}", deviceImei, e.getMessage(), e);
        }
    }

    /**
     * 通用：对单个报警源的“开始/结束”生命周期管理
     */
    private void handleSingleAlarm(String deviceImei, boolean nowAlarming, AlarmTypeEnum type) {
        String redisKey = buildAlarmRedisKey(deviceImei, type);

        Long alarmId = RedisUtils.getCacheObject(redisKey);

        String lockKey  = redisKey + ":lock";                     // 分布式锁 key
        RedissonClient client = RedisUtils.getClient();           // 唯一用到的“旧”入口
        RLock lock = client.getLock(lockKey);

        // ---------- 情况 1：当前正在报警 ----------
        if (nowAlarming) {
            // 已存在未结束报警 -> 什么都不做（同一条报警）
            if (alarmId != null) {
                // key 还在 -> 同一条报警，只续期
                RedisUtils.setCacheObject(redisKey, alarmId, Duration.ofMinutes(10));
                return;
            }
            // 需要新建，抢锁
            boolean locked = false;
            try {
                locked = lock.tryLock(3, TimeUnit.SECONDS);   // 最多等 3 s
                if (!locked) {                                // 抢不到直接放弃
                    return;
                }
                // 锁内二次校验（double-check）
                alarmId = RedisUtils.getCacheObject(redisKey);
                if (alarmId != null) {
                    return;           // 并发线程已建好
                }

                // 不存在 -> 新建
                DeviceAlarmBo bo = createAlarmBo(deviceImei, type);
                if (bo == null){
                    return;
                }
                deviceAlarmService.insertByBo(bo);
                RedisUtils.setCacheObject(redisKey, bo.getId(), Duration.ofMinutes(10)); // 5分钟后结束过期
            }catch (InterruptedException ignore) {
                // 立即中断并退出，禁止继续往下走
                Thread.currentThread().interrupt();
            } finally {
                if (locked && lock.isHeldByCurrentThread()) {
                    lock.unlock();
                }
            }
            return;
        }

        // ---------- 情况 2：当前不报警 ----------
        if (alarmId != null) {
            // 结束它
            finishAlarm(alarmId);
            RedisUtils.deleteObject(redisKey);
        }
    }

    /**
     * 结束报警：写库
     */
    private void finishAlarm(Long alarmId) {
        DeviceAlarmVo vo = deviceAlarmService.queryById(alarmId);
        if (vo == null || vo.getTreatmentState() == 0) {
            return; // 已处理或已被删
        }
        DeviceAlarmBo bo = BeanUtil.toBean(vo, DeviceAlarmBo.class);
        bo.setFinishTime(new Date());
        bo.setDurationTime(DurationUtils.getDurationBetween(vo.getStartTime(), bo.getFinishTime()));
        bo.setTreatmentState(0); // 已处理
        deviceAlarmService.updateByBo(bo);
    }

    /**
     * 新建报警 Bo
     */
    private DeviceAlarmBo createAlarmBo(String deviceImei, AlarmTypeEnum type) {
        Device device = deviceService.selectDeviceByImei(deviceImei);
        if (device == null) {
            return null;
        }
        DeviceAlarmBo bo = new DeviceAlarmBo();
        bo.setDeviceId(device.getId());
        bo.setDeviceImei(deviceImei);
        bo.setDeviceAction(2); // 自动报警
        bo.setStartTime(new Date());
        bo.setTreatmentState(1); // 未处理
        bo.setTenantId(device.getTenantId());

        // 报警内容
        bo.setContent("自动报警：" + type.getDesc());

        // 位置
        String location = RedisUtils.getCacheObject(
                GLOBAL_REDIS_KEY + DEVICE_KEY_PREFIX + deviceImei + DEVICE_LOCATION_KEY_PREFIX);
        if (StrUtil.isNotBlank(location)) {
            bo.setLocation(com.alibaba.fastjson2.JSONObject.parseObject(location).getString("address"));
        }
        return bo;
    }

    /**
     * key 构建
     */
    private String buildAlarmRedisKey(String deviceImei, AlarmTypeEnum type) {
        return StrUtil.format("{}{}{}{}:alarm_id",
                GLOBAL_REDIS_KEY, DEVICE_KEY_PREFIX, deviceImei, type.getSuffix());
    }

    /**
     * 异步发送位置信息到Redis（使用CompletableFuture）
     *
     * @param deviceImei 设备IMEI
     * @param latitude   纬度
     * @param longitude  经度
     */
    public void asyncSendLocationToRedisWithFuture(String deviceImei, String latitude, String longitude) {
        CompletableFuture.runAsync(() -> {
            try {
                if (StringUtils.isAnyBlank(deviceImei, latitude, longitude)) {
                    log.warn("位置上报参数为空，deviceImei={}", deviceImei);
                    return;
                }
                //log.info("位置上报，deviceImei={}, lat={}, lon={}", deviceImei, latitude, longitude);
                // 1. 解析当前上报的经纬度
                Double curLat = parseDoubleSafe(latitude.trim());
                Double curLon = parseDoubleSafe(longitude.trim());
                if (curLat == null || curLon == null) {
                    log.warn("经纬度格式错误，直接更新，deviceImei={}, lat={}, lon={}", deviceImei, latitude, longitude);
                    doSaveLocation(deviceImei, latitude, longitude);
                    return;
                }

                // 2. 读取 Redis 中缓存的上一次位置
                String redisKey = GlobalConstants.GLOBAL_REDIS_KEY + DEVICE_KEY_PREFIX + deviceImei + DEVICE_LOCATION_KEY_PREFIX;
                String cachedJson = RedisUtils.getCacheObject(redisKey);

                if (StringUtils.isNotBlank(cachedJson)) {
                    com.alibaba.fastjson2.JSONObject cachedObj = com.alibaba.fastjson2.JSONObject.parseObject(cachedJson);
                    String cachedWgs84Lat = cachedObj.getString("wgs84_latitude");
                    String cachedWgs84Lon = cachedObj.getString("wgs84_longitude");

                    Double oldLat = parseDoubleSafe(cachedWgs84Lat);
                    Double oldLon = parseDoubleSafe(cachedWgs84Lon);

                    if (oldLat != null && oldLon != null) {
                        double distance = haversine(oldLat, oldLon, curLat, curLon);
                        if (distance <= MOVEMENT_THRESHOLD_METER) {
                            log.info("位置未发生明显变化（{}米 <= {}米），不更新 Redis，deviceImei={}, lat={}, lon={}",
                                    distance, MOVEMENT_THRESHOLD_METER, deviceImei, latitude, longitude);
                            return;
                        }
                    }
                }

                // 3. 位置有明显变化，执行保存
                doSaveLocation(deviceImei, latitude, longitude);

            } catch (Exception e) {
                log.error("异步发送位置信息到 Redis 失败，deviceImei={}", deviceImei, e);
            }
        });
    }

    /** 真正执行保存逻辑（抽取出来便于测试和阅读） */
    private void doSaveLocation(String deviceImei, String wgs84Lat, String wgs84Lon) {

//        Map<String, Object> locationInfo = new LinkedHashMap<>();
//        locationInfo.put("deviceImei", deviceImei);
//        locationInfo.put("latitude", gcj02[0]);      // GCJ02 纬度
//        locationInfo.put("longitude", gcj02[1]);     // GCJ02 经度
//        locationInfo.put("wgs84_latitude", wgs84Lat);
//        locationInfo.put("wgs84_longitude", wgs84Lon);
//
//
//        locationInfo.put("address", StringUtils.defaultIfBlank(address, "未知"));
//        locationInfo.put("timestamp", System.currentTimeMillis());
//
//        String locationJson = JsonUtils.toJsonString(locationInfo);
        //  使用 fastjson2 零 GC 序列化
        //  WGS84 → GCJ02（火星坐标）
        double[] gcj02 = LngLonUtil.gps84_To_Gcj02(
                Double.parseDouble(wgs84Lat),
                Double.parseDouble(wgs84Lon)
        );

        String gcj02Lat = String.format("%.6f", gcj02[0]);
        String gcj02Lon = String.format("%.6f", gcj02[1]);

        // 逆地理编码（可自行决定是否异步）
        String address = GetAddressFromLatUtil.getAdd(gcj02Lon, gcj02Lat);

        String locationJson = buildLocationJsonFastJSON2(deviceImei, wgs84Lat, wgs84Lon, gcj02Lon, gcj02Lat,address);

        // 主位置信息（最新一条）
        String redisKey = GlobalConstants.GLOBAL_REDIS_KEY + DEVICE_KEY_PREFIX + deviceImei + DEVICE_LOCATION_KEY_PREFIX;
        RedisUtils.setCacheObject(redisKey, locationJson);

        // 轨迹存储（SortedSet，按时间戳排序）
        storeDeviceTrajectoryWithSortedSet(deviceImei, locationJson);
        // 轨迹上传 查询检测对象与围栏关系  记录设备进出围栏事件
        uploadTrackPointAsync(deviceImei, gcj02Lat, gcj02Lon, address);

        log.info("位置信息已更新 Redis，deviceImei={}, wgs84=({},{})", deviceImei, wgs84Lat, wgs84Lon);
    }

    private String buildLocationJsonFastJSON2(
            String deviceImei,
            String wgs84Lat, String wgs84Lon,
            String gcj02Lon, String gcj02Lat,String address) {
        long timestamp = System.currentTimeMillis();


        // 直接用默认 writer，零配置，自动零 GC
        try (JSONWriter w = JSONWriter.of()) {
            w.startObject();
            w.writeString("deviceImei"); w.writeColon(); w.writeString(deviceImei); w.writeComma();
            w.writeString("latitude");   w.writeColon(); w.writeString(gcj02Lat); w.writeComma();
            w.writeString("longitude");  w.writeColon(); w.writeString(gcj02Lon); w.writeComma();
            w.writeString("wgs84_latitude"); w.writeColon(); w.writeString(wgs84Lat); w.writeComma();
            w.writeString("wgs84_longitude"); w.writeColon(); w.writeString(wgs84Lon); w.writeComma();
            w.writeString("address");    w.writeColon(); w.writeString(StringUtils.defaultIfBlank(address, "未知")); w.writeComma();
            w.writeString("timestamp");  w.writeColon(); w.writeInt64(timestamp);
            w.endObject();
            return w.toString();
        }
    }

    /**
     * 上传轨迹点并处理电子围栏出入事件（高德猎鹰服务）
     * 优化点：
     * 1. 避免重复查询数据库（围栏信息批量缓存）
     * 2. 防御性编程：全面空指针防护
     * 3. Redis 操作原子性 + 合理 TTL
     * 4. 减少不必要的对象创建和流操作
     * 5. 精确的事件时间使用 locationTime
     * 6. 失败不阻塞主流程，记录关键错误
     */
    private void uploadTrackPointAsync(String deviceImei,
                                       String gcj02Lat,
                                       String gcj02Lon,String address) {

        if (StrUtil.hasBlank(deviceImei, gcj02Lat, gcj02Lon)) {
            log.warn("上传轨迹点参数非法，deviceImei={}, lat={}, lon={}", deviceImei, gcj02Lat, gcj02Lon);
            return;
        }
        long locationTime = System.currentTimeMillis();
        String fenceStatusKey = GLOBAL_REDIS_KEY + DEVICE_KEY_PREFIX + deviceImei + ":terminal:status";

        try {
            // 1. 查询设备信息（建议加缓存，热点设备可大幅降低DB压力）
            Device device = deviceService.selectDeviceByImei(deviceImei);
            if (device == null || device.getSid() == null || device.getTid() == null) {
                log.warn("设备不存在或未完成高德终端创建，imei={}", deviceImei);
                return;
            }

            Long trid = ObjectUtil.defaultIfNull(device.getTrid(), 0L); // trid 可能为空

            // 2. 上传轨迹点
            JSONObject point = new JSONObject();
            point.set("location", String.format("%s,%s", gcj02Lon, gcj02Lat));
            point.set("locatetime", locationTime);

            JSONArray pointsArray = new JSONArray();
            pointsArray.add(point);
            log.info("上传轨迹点开始，deviceImei={}, point={}", deviceImei, point);
            JSONObject uploadResult = amapTrackUtil.uploadPoints(device.getSid(), device.getTid(), trid, pointsArray);
            if (uploadResult == null || uploadResult.getInt("errcode", -1) != 10000) {
                return;
            }
            log.info("上传轨迹点成功，deviceImei={}, result={}", deviceImei, uploadResult);
            // 3. 查询当前围栏状态
            JSONObject fenceResult = amapTrackUtil.queryTerminalFenceStatus(device.getSid(), null, device.getTid());
            if (fenceResult == null || fenceResult.getInt("errcode", -1) != 10000) {
                log.warn("查询设备围栏状态失败，imei={}, result={}", deviceImei, fenceResult);
                return;
            }
            log.info("查询设备围栏状态成功，imei={}, result={}", deviceImei, fenceResult);
            JSONArray results = fenceResult.getByPath("data.results", JSONArray.class);
            if (results == null || results.isEmpty()) {
                // 没有任何围栏关系，清空旧状态
                RedisUtils.deleteObject(fenceStatusKey);
                return;
            }

            // 4. 当前在围栏内的 gfid 集合
            Set<Long> newInFenceGfids = new HashSet<>();
            List<JSONObject> currentInList = new ArrayList<>();

            for (Object obj : results) {
                JSONObject item = (JSONObject) obj;
                if (item.getInt("in", 0) == 1) {
                    Long gfid = item.getLong("gfid");
                    if (gfid != null) {
                        newInFenceGfids.add(gfid);
                        currentInList.add(item);
                    }
                }
            }

            // 5. 获取上一次的围栏状态
            List<JSONObject> oldInList = RedisUtils.getCacheObject(fenceStatusKey);
            Set<Long> oldInFenceGfids = (oldInList == null || oldInList.isEmpty())
                    ? Collections.emptySet()
                    : oldInList.stream()
                    .map(o -> o.getLong("gfid"))
                    .collect(Collectors.toSet());

            // 6. 计算出入事件（使用高效的 Set 操作）
            Set<Long> enteredGfids = new HashSet<>(newInFenceGfids);
            enteredGfids.removeAll(oldInFenceGfids);// 进入：这次有，上次没有

            Set<Long> exitedGfids = new HashSet<>(oldInFenceGfids);
            exitedGfids.removeAll(newInFenceGfids);// 离开：上次有，这次没有

            Date eventTime = new Date(locationTime);
            Double latitude = Double.valueOf(gcj02Lat);
            Double longitude = Double.valueOf(gcj02Lon);

            // 批量查询围栏信息（关键优化：避免 N+1 查询）
            Set<Long> allChangedGfids = new HashSet<>();
            allChangedGfids.addAll(enteredGfids);
            allChangedGfids.addAll(exitedGfids);

            Map<Long, DeviceGeoFenceVo> fenceMap = new HashMap<>();
            if (CollUtil.isNotEmpty(allChangedGfids)) {
                List<DeviceGeoFenceVo> fenceList = deviceGeoFenceMapper.selectVoList(
                        new LambdaQueryWrapper<DeviceGeoFence>().in(DeviceGeoFence::getGfid, allChangedGfids));
                fenceMap = fenceList.stream()
                        .filter(Objects::nonNull)
                        .collect(Collectors.toMap(DeviceGeoFenceVo::getGfid, v -> v, (a, b) -> a));
            }

            // 7. 记录进入事件
            if (CollUtil.isNotEmpty(enteredGfids)) {
                List<DeviceFenceAccessRecord> enterRecords = new ArrayList<>();
                for (Long gfid : enteredGfids) {
                    DeviceFenceAccessRecord record = buildFenceRecord(device, fenceMap.get(gfid), 1L, latitude, longitude, eventTime, address);
                    if (record != null) {
                        enterRecords.add(record);
                    }
                }
                deviceFenceAccessRecordMapper.insertBatch(enterRecords);
                log.info("设备进入围栏，imei={}, gfids={}", deviceImei, enteredGfids);
            }

            // 8. 记录离开事件
            if (CollUtil.isNotEmpty(exitedGfids)) {
                List<DeviceFenceAccessRecord> exitRecords = new ArrayList<>();
                for (Long gfid : exitedGfids) {
                    DeviceFenceAccessRecord record = buildFenceRecord(device, fenceMap.get(gfid), 2L, latitude, longitude, eventTime, address);
                    if (record != null) {
                        exitRecords.add(record);
                    }
                }
                deviceFenceAccessRecordMapper.insertBatch(exitRecords);
                log.info("设备离开围栏，imei={}, gfids={}", deviceImei, exitedGfids);
            }

            // 9. 更新 Redis 状态（TTL 5分钟，防止频繁查询）
            if (CollUtil.isNotEmpty(currentInList)) {
                RedisUtils.setCacheObject(fenceStatusKey, currentInList, Duration.ofMinutes(5));
            } else {
                RedisUtils.deleteObject(fenceStatusKey);
            }

        } catch (Exception e) {
            log.error("上传轨迹点并处理围栏事件异常，imei={}, lat={}, lon={}, time={}",
                    deviceImei, gcj02Lat, gcj02Lon, locationTime, e);
            // 可落库待重试或发告警，此处不抛异常影响定位主流程
        }
    }

    /**
     * 构建围栏出入记录（提取公共逻辑，避免重复代码）
     */
    private DeviceFenceAccessRecord buildFenceRecord(Device device,
                                                       DeviceGeoFenceVo fence,
                                                       Long eventType,
                                                       Double latitude,
                                                       Double longitude,
                                                       Date eventTime,String address) {
        if (fence == null || fence.getId() == null) {
            log.warn("围栏信息不存在，gfid 可能已被删除或未绑定");
            return null;
        }
        DeviceFenceAccessRecord bo = new DeviceFenceAccessRecord();
        bo.setDeviceId(device.getId().toString());
        bo.setFenceId(fence.getId());
        bo.setEventType(eventType);        // 1=进入, 2=离开
        bo.setAccuracy(20L);
        bo.setLatitude(latitude);
        bo.setLongitude(longitude);
        bo.setEventTime(eventTime);
        bo.setTenantId(device.getTenantId());
        bo.setCreateBy(device.getCreateBy());
        bo.setCreateDept(device.getCreateDept());
        bo.setEventAddress(address);
        return bo;
    }

    /** 安全解析 double，解析失败返回 null */
    private Double parseDoubleSafe(String str) {
        if (StringUtils.isBlank(str)) return null;
        try {
            return Double.parseDouble(str.trim());
        } catch (NumberFormatException e) {
            return null;
        }
    }

    /** Haversine 公式计算两点球面距离（米） */
    private double haversine(double lat1, double lon1, double lat2, double lon2) {
        double dLat = Math.toRadians(lat2 - lat1);
        double dLon = Math.toRadians(lon2 - lon1);
        double a = Math.sin(dLat / 2) * Math.sin(dLat / 2) +
                Math.cos(Math.toRadians(lat1)) * Math.cos(Math.toRadians(lat2)) *
                        Math.sin(dLon / 2) * Math.sin(dLon / 2);
        double c = 2 * Math.atan2(Math.sqrt(a), Math.sqrt(1 - a));
        /** 地球平均半径（米） */
        double EARTH_RADIUS = 6371_393.0;
        return EARTH_RADIUS * c;
    }

    /**
     * 存储设备30天历史轨迹到Redis (使用Sorted Set)
     */
    public void storeDeviceTrajectoryWithSortedSet(String deviceImei, String locationJson) {
        try {
            String trajectoryKey = GlobalConstants.GLOBAL_REDIS_KEY+ DEVICE_KEY_PREFIX + deviceImei + DeviceRedisKeyConstants.DEVICE_LOCATION_HISTORY_KEY_PREFIX;
//            String trajectoryKey = "device:trajectory:zset:" + deviceImei;
//            String locationJson = JsonUtils.toJsonString(locationInfo);
            long timestamp = System.currentTimeMillis();

            // 添加到Sorted Set，使用时间戳作为score
            RedisUtils.zAdd(trajectoryKey, locationJson, timestamp);

//            // 设置30天过期时间
//            RedisUtils.expire(trajectoryKey, Duration.ofDays(30));

            // 清理30天前的数据（冗余保护）
            long thirtyDaysAgo = System.currentTimeMillis() - (7L * 24 * 60 * 60 * 1000);
            RedisUtils.zRemoveRangeByScore(trajectoryKey, 0, thirtyDaysAgo);
        } catch (Exception e) {
            log.error("存储设备轨迹到Redis(ZSet)失败: device={}, error={}", deviceImei, e.getMessage(), e);
        }
    }

    private Map<String, Object> buildLocationDataMap(String latitude, String longitude) {
        String[] latArr = latitude.split("\\.");
        String[] lonArr = longitude.split("\\.");

        ArrayList<Integer> intData = new ArrayList<>();
        intData.add(11);
        intData.add(Integer.parseInt(latArr[0]));
        String str1 = latArr[1];
        intData.add(Integer.parseInt(str1.substring(0,4)));
        String str2 = lonArr[1];
        intData.add(Integer.parseInt(lonArr[0]));
        intData.add(Integer.parseInt(str2.substring(0,4)));

        Map<String, Object> map = new HashMap<>();
        map.put("instruct", intData);
        return map;
    }



}
