package com.ylx.home.hot.service.impl;

import cn.hutool.core.collection.CollUtil;
import cn.hutool.core.util.ObjectUtil;
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
import com.google.common.collect.Lists;
import com.ylx.common.core.domain.entity.SysDictData;
import com.ylx.common.utils.DictUtils;
import com.ylx.common.utils.DistanceUtil;
import com.ylx.home.hot.domain.dto.HotRecommendDTO;
import com.ylx.home.hot.domain.vo.HotRecommendVO;
import com.ylx.home.hot.domain.vo.ProjectSalesVO;
import com.ylx.home.hot.service.HomeHotRecommendService;
import com.ylx.massage.domain.MaTechnician;
import com.ylx.massage.service.IMaTechnicianService;
import com.ylx.merchant.domain.vo.MerchantWithAddressVO;
import com.ylx.project.domain.Project;
import com.ylx.project.service.ProjectService;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.collections4.CollectionUtils;
import org.springframework.data.redis.connection.ReturnType;
import org.springframework.data.redis.core.DefaultTypedTuple;
import org.springframework.data.redis.core.RedisCallback;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.data.redis.core.ZSetOperations;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;

import javax.annotation.Resource;
import java.math.BigDecimal;
import java.time.Duration;
import java.util.*;
import java.util.stream.Collectors;

import static com.ylx.order.constant.OrderConstant.DICT_UNIT_TYPE;

@Slf4j
@Service
public class HomeHotRecommendServiceImpl implements HomeHotRecommendService {

    @Resource
    private RedisTemplate<String, String> redisTemplate;
    @Resource
    private ProjectService projectService;
    @Resource
    private IMaTechnicianService maTechnicianService;

    private static final String HOT_MERCHANT_RANK_KEY = "hot:merchant:rank";
    private static final String HOT_PROJECT_RANK_KEY = "hot:project:rank";

    private static final String SYNC_LOCK_KEY = "lock:hot_rank_sync";
    private static final int BATCH_SIZE = 200;

    private static final Map<String, String> unitDictCache = new HashMap<>();

    static {
        List<SysDictData> dictList = DictUtils.getSortedDictCache(DICT_UNIT_TYPE);
        if (CollUtil.isNotEmpty(dictList)) {
            dictList.forEach(dict -> unitDictCache.put(dict.getDictValue(), dict.getDictLabel()));
        } else {
            log.warn("项目计量单位字典未加载，请检查字典配置：{}", DICT_UNIT_TYPE);
        }
    }

    @Override
    public Page<HotRecommendVO> getHotRecommendPage(HotRecommendDTO dto) {
        long pageSize = dto.getSize();
        long pageNum = dto.getCurrent();
        BigDecimal userLng = dto.getLongitude();
        BigDecimal userLat = dto.getLatitude();

        long offset = (pageNum - 1) * pageSize;
        // 优化：多预取一倍ID，解决交替翻页断层
        long fetchCount = pageSize * 2;
        long end = offset + fetchCount - 1;
        List<MerchantWithAddressVO> techWithAddrList;
        List<Project> projectList;
        boolean redisDown = false;
        try {
            // 1. Redis ZSet倒序分页获取ID
            Set<String> techIdStrSet = redisTemplate.opsForZSet().reverseRange(HOT_MERCHANT_RANK_KEY, offset, end);
            Set<String> projectIdStrSet = redisTemplate.opsForZSet().reverseRange(HOT_PROJECT_RANK_KEY, offset, end);

            // 转ID集合
            List<Integer> techIds = techIdStrSet.stream().map(Integer::valueOf).collect(Collectors.toList());
            List<Integer> projectIds = projectIdStrSet.stream().map(Integer::valueOf).collect(Collectors.toList());

            // 2. 批量查库获取完整详情
            techWithAddrList = this.maTechnicianService.selectTechWithAddressByIds(techIds);
            projectList = this.projectService.listByIds(projectIds);

            // 3. 内存二次过滤脏数据（缓存不一致兜底）
            techWithAddrList = techWithAddrList.stream().filter(t -> t.getNStar() != null).collect(Collectors.toList());
            projectList = projectList.stream().filter(p -> p.getIsDelete() == 0 && p.getStatus() == 0).collect(Collectors.toList());
        } catch (Exception e) {
            // Redis异常降级，切回纯MySQL分页方案
            log.error("Redis ZSet查询异常，降级MySQL分页", e);
            redisDown = true;
            techWithAddrList = this.maTechnicianService.listValidTechWithAddress(offset, fetchCount);
            projectList = this.projectService.selectProjectPage(offset, fetchCount);
        }

        // 4. 交替合并 商户-项目循环，截断pageSize
        List<HotRecommendVO> mixResult = mergeMixData(techWithAddrList, projectList, pageSize, userLat, userLng);

        // 5. 分页总数
        Long totalTech, totalProject;
        if (!redisDown) {
            totalTech = ObjectUtil.defaultIfNull(redisTemplate.opsForZSet().zCard(HOT_MERCHANT_RANK_KEY), 0L);
            totalProject = ObjectUtil.defaultIfNull(redisTemplate.opsForZSet().zCard(HOT_PROJECT_RANK_KEY), 0L);
        } else {
            totalTech = this.maTechnicianService.countValidTech();
            totalProject = this.projectService.countValidProject();
        }
        Page<HotRecommendVO> page = new Page<>(dto.getCurrent(), dto.getSize());
        page.setRecords(mixResult);
        // 注意：混合交替分页total仅作展示，请勿用于前端分页页码计算，存在数据穿插断层
        page.setTotal(totalTech + totalProject);
        return page;
    }

    @Override
    public void syncAllHotRank() {
        long start = System.currentTimeMillis();
        // 锁超时10分钟，适配大批量同步
        Boolean locked = redisTemplate.opsForValue().setIfAbsent(SYNC_LOCK_KEY, "1", Duration.ofMinutes(10));
        if (!Boolean.TRUE.equals(locked)) {
            log.warn("热门排行同步任务正在执行中，本次跳过");
            return;
        }

        try {
            syncMerchantRank();
            calibrateAndSyncProjectRank();
            log.info("热门排行ZSet全量同步完成，总耗时{}ms", System.currentTimeMillis() - start);
        } catch (Exception e) {
            log.error("热门排行同步异常，总耗时{}ms", System.currentTimeMillis() - start, e);
        } finally {
            releaseLock(SYNC_LOCK_KEY, "1");
        }
    }

    /**
     * 交替合并：商户1、项目1、商户2、项目2
     */
    private List<HotRecommendVO> mergeMixData(List<MerchantWithAddressVO> techList, List<Project> projectList, Long pageSize, BigDecimal userLat, BigDecimal userLng) {
        List<HotRecommendVO> result = new ArrayList<>(Math.toIntExact(pageSize));
        int maxLoop = Math.max(techList.size(), projectList.size());
        for (int i = 0; i < maxLoop; i++) {
            if (result.size() >= pageSize) break;
            // 先组装商户卡片
            if (i < techList.size()) {
                HotRecommendVO vo = convertTechToVO(techList.get(i), userLat, userLng);
                vo.setCardType(1);
                result.add(vo);
                if (result.size() >= pageSize) break;
            }
            // 再组装项目卡片
            if (i < projectList.size()) {
                HotRecommendVO vo = convertProjectToVO(projectList.get(i));
                vo.setCardType(2);
                result.add(vo);
                if (result.size() >= pageSize) break;
            }
        }
        return result;
    }

    /**
     * 商户实体转VO（包含距离计算、标签拆分）
     */
    private HotRecommendVO convertTechToVO(MerchantWithAddressVO tech, BigDecimal userLat, BigDecimal userLng) {
        HotRecommendVO vo = new HotRecommendVO();
        vo.setCardType(1);
        vo.setTechId(tech.getId());
        vo.setCoverImg(tech.getAvatar() != null ? tech.getAvatar() : tech.getTeAvatar());
        vo.setTeNickName(tech.getTeNickName());
        vo.setNStar(tech.getNStar());
        vo.setServiceCount(tech.getNNum());
        // 距离兜底
        String distance = DistanceUtil.formatDisplay(userLat, userLng, tech.getLatitude(), tech.getLongitude());
        vo.setDistance(ObjectUtil.isNotEmpty(distance) ? distance : "距离未知");

        String tagStr = tech.getTeProject();
        if (tagStr != null && tagStr.trim().length() > 0) {
            List<String> tagList = Arrays.stream(tagStr.split(","))
                    .map(String::trim)
                    .filter(s -> s.length() > 0)
                    .collect(Collectors.toList());
            vo.setTagList(tagList);
        }
        return vo;
    }

    /**
     * 项目实体转卡片VO
     */
    private HotRecommendVO convertProjectToVO(Project project) {
        HotRecommendVO vo = new HotRecommendVO();
        vo.setCardType(2);
        vo.setProjectId(project.getId());
        vo.setCoverImg(project.getCover());
        vo.setProjectTitle(project.getTitle());
        vo.setHighlight(project.getHighlight());
        vo.setPriceMin(project.getPriceMin());
        vo.setSalesCount(project.getSalesCompleted());
        vo.setType(project.getType());

        Integer unitType = project.getUnitType();
        String unitKey = ObjectUtil.isNotNull(unitType) ? unitType.toString() : "";
        String unitTypeName = unitDictCache.getOrDefault(unitKey, "未知单位");
        // 时长空值兜底
        Integer duration = ObjectUtil.defaultIfNull(project.getStandardDuration(), 0);
        String durationText = duration + unitTypeName;
        vo.setDurationText(durationText);
        return vo;
    }

    /**
     * 同步商户排行ZSet
     */
    private void syncMerchantRank() {
        List<MaTechnician> validTechList = maTechnicianService.selectAllValidTech();
        Map<String, Double> scores = validTechList.stream()
                .collect(Collectors.toMap(
                        t -> String.valueOf(t.getId()),
                        t -> t.getNNum() != null ? t.getNNum().doubleValue() : 0D,
                        (a, b) -> a
                ));
        replaceRankZSet(HOT_MERCHANT_RANK_KEY, scores);
        log.info("商户热门排行同步完成，数量:{}", scores.size());
    }

    /**
     * DB校准项目销量 + 同步项目排行ZSet
     */
    private void calibrateAndSyncProjectRank() {
        // 查询最新销量（事务外读取，缩短事务持有时间）
        List<ProjectSalesVO> allSales = this.projectService.selectAllProjectSalesCount();
        // DB批量校准（独立事务）
        batchCalibrateProjectSales(allSales);

        // Redis同步，异常隔离不影响DB数据
        try {
            Map<String, Double> scores = allSales.stream()
                    .collect(Collectors.toMap(
                            vo -> String.valueOf(vo.getId()),
                            vo -> vo.getSalesCount() != null ? vo.getSalesCount().doubleValue() : 0D,
                            (a, b) -> a
                    ));
            replaceRankZSet(HOT_PROJECT_RANK_KEY, scores);
            log.info("项目热门排行同步完成，数量:{}", scores.size());
        } catch (Exception e) {
            log.error("项目热门排行Redis同步失败，DB已校准完成，等待下次定时任务修复缓存", e);
        }
    }

    /**
     * 独立事务：批量更新项目销量冗余字段
     */
    @Transactional(rollbackFor = Exception.class)
    public void batchCalibrateProjectSales(List<ProjectSalesVO> salesList) {
        if (CollectionUtils.isEmpty(salesList)) return;
        Lists.partition(salesList, BATCH_SIZE).forEach(batch -> this.projectService.batchUpdateSales(batch));
    }

    /**
     * 通用方法：原子替换排行ZSet，临时key中转避免线上空白列表
     */
    private void replaceRankZSet(String rankKey, Map<String, Double> scores) {
        String tempKey = rankKey + ":temp";
        redisTemplate.delete(tempKey);
        if (scores.isEmpty()) {
            redisTemplate.delete(rankKey);
            return;
        }
        Set<ZSetOperations.TypedTuple<String>> tuples = scores.entrySet().stream()
                .map(entry -> new DefaultTypedTuple<>(entry.getKey(), entry.getValue()))
                .collect(Collectors.toSet());
        redisTemplate.opsForZSet().add(tempKey, tuples);
        redisTemplate.rename(tempKey, rankKey);
    }

    /**
     * Lua脚本原子释放分布式锁，防止误删其他线程锁
     */
    private void releaseLock(String lockKey, String expectedValue) {
        String script = "if redis.call('get', KEYS[1]) == ARGV[1] then return redis.call('del', KEYS[1]) else return 0 end";
        try {
            redisTemplate.execute((RedisCallback<Object>) connection -> {
                byte[] keyBytes = redisTemplate.getStringSerializer().serialize(lockKey);
                byte[] valBytes = redisTemplate.getStringSerializer().serialize(expectedValue);
                return connection.eval(script.getBytes(), ReturnType.INTEGER, 1, keyBytes, valBytes);
            });
        } catch (Exception e) {
            log.error("释放分布式锁失败，key={}", lockKey, e);
        }
    }
}