package com.njcn.msgpush.module.push.service.retry;

import cn.hutool.core.collection.CollUtil;
import cn.hutool.core.util.ObjectUtil;
import cn.hutool.core.util.StrUtil;
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import com.njcn.msgpush.framework.common.pojo.PageResult;
import com.njcn.msgpush.module.push.constant.MsgStatusConstant;
import com.njcn.msgpush.module.push.controller.admin.retry.vo.MessageRetryQueueReqVO;
import com.njcn.msgpush.module.push.dal.dataobject.message.MessageRecordDO;
import com.njcn.msgpush.module.push.dal.dataobject.retry.MessageRetryQueueDO;
import com.njcn.msgpush.module.push.dal.dataobject.retry.RetryStrategyConfigDO;
import com.njcn.msgpush.module.push.dal.mysql.retry.MessageRetryQueueMapper;
import com.njcn.msgpush.module.push.dal.redis.MessageRetryRedisDAO;
import com.njcn.msgpush.module.push.enums.ChannelTypeEnum;
import com.njcn.msgpush.module.push.enums.RetrySourceEnum;
import com.njcn.msgpush.module.push.service.message.MessageRecordService;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.CommandLineRunner;
import org.springframework.scheduling.annotation.Scheduled;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;

import java.time.Duration;
import java.time.LocalDateTime;
import java.util.Arrays;
import java.util.Collections;
import java.util.List;
import java.util.concurrent.atomic.AtomicBoolean;
import java.util.stream.Collectors;

@Slf4j
@Service
public class MessageRetryQueueServiceImpl extends ServiceImpl<MessageRetryQueueMapper, MessageRetryQueueDO>
        implements MessageRetryQueueService, CommandLineRunner {

    private static final int DEFAULT_BATCH_SIZE = 100;
    private static final int DEFAULT_MAX_RETRY_COUNT = 5;
    private static final int PROCESS_STATUS_WAITING = 0;
    private static final int PROCESS_STATUS_PROCESSING = 1;
    private static final int DEFAULT_LOCK_SECONDS = 120;

    /** 限流类失败的统一错误码(回执后由 Sender/MessageRecordServiceImpl 写入 message.errorCode) */
    private static final String RATE_LIMIT_ERROR_CODE = "RATE_LIMIT";
    /** 限流类固定重试间隔:2 分钟(不走阶梯) */
    private static final int RATE_LIMIT_INTERVAL_SECONDS = 120;
    /** 限流类时间兜底:首次失败起满 12 小时未成功才放弃 */
    private static final long RATE_LIMIT_MAX_DURATION_SECONDS = 43200;

    private static final List<String> CHANNELS = Arrays.asList(
            ChannelTypeEnum.SMS.getCode(),
            ChannelTypeEnum.EMAIL.getCode(),
            ChannelTypeEnum.APP.getCode());

    @Autowired
    private MessageRetryRedisDAO messageRetryRedisDAO;
    @Autowired
    private MessageRecordService messageRecordService;
    @Autowired
    private RetryStrategyConfigService retryStrategyConfigService;

    private final AtomicBoolean startupRebuildCompleted = new AtomicBoolean(false);

    @Override
    @Transactional(rollbackFor = Exception.class)
    public void saveOrUpdateRetryMessage(MessageRecordDO message) {
        MessageRetryQueueDO existing = baseMapper.selectOne(new LambdaQueryWrapper<MessageRetryQueueDO>()
                .eq(MessageRetryQueueDO::getMessageId, message.getId()));
        message.setLastRetryTime(message.getSendTime());

        if (ObjectUtil.isNull(existing)) {
            MessageRetryQueueDO retryRecord = new MessageRetryQueueDO();
            retryRecord.setMessageId(message.getId());
            retryRecord.setChannel(message.getChannel());
            retryRecord.setReceiver(message.getReceiver());
            retryRecord.setRetryCount(1);
            RetryStrategyConfigDO strategyConfig = retryStrategyConfigService.getStrategyConfig(message.getChannel());
            retryRecord.setMaxRetry(ObjectUtil.isNull(strategyConfig) ? DEFAULT_MAX_RETRY_COUNT : strategyConfig.getMaxRetryCount());
            retryRecord.setFirstFailTime(message.getSendTime());
            retryRecord.setLastRetryTime(message.getSendTime());
            retryRecord.setLastErrorMsg(message.getErrorMsg());
            LocalDateTime nextRetryTime = calculateNextRetryTime(message.getChannel(), retryRecord.getRetryCount(), isRateLimit(message));
            retryRecord.setNextRetryTime(nextRetryTime);
            retryRecord.setProcessStatus(PROCESS_STATUS_WAITING);
            retryRecord.setLockUntil(null);
            message.setNextRetryTime(nextRetryTime);
            save(retryRecord);
            if (!messageRetryRedisDAO.addToRetryQueue(message)) {
                log.warn("消息已写入数据库重试队列，但 Redis 入队失败: messageId={}", message.getId());
            }
            return;
        }

        applyRetryUpdate(message, existing);
    }

    private void applyRetryUpdate(MessageRecordDO message, MessageRetryQueueDO existing) {
        int newRetryCount = existing.getRetryCount() + 1;
        existing.setLastRetryTime(message.getSendTime());
        existing.setLastErrorMsg(message.getErrorMsg());

        boolean rateLimit = isRateLimit(message);
        boolean exhausted = rateLimit
                ? isRateLimitExhausted(existing.getFirstFailTime(), LocalDateTime.now())
                : newRetryCount >= existing.getMaxRetry();
        if (exhausted) {
            message.setStatus(MsgStatusConstant.FINALFAILED);
            message.setNextRetryTime(null);
            existing.setNextRetryTime(null);
            updateById(existing);
            deleteByMessageIds(Collections.singletonList(message.getId()));
            messageRetryRedisDAO.removeFromRetryQueue(message.getChannel(), message.getId());
            return;
        }

        LocalDateTime nextRetryTime = calculateNextRetryTime(message.getChannel(), newRetryCount, rateLimit);
        existing.setRetryCount(newRetryCount);
        existing.setNextRetryTime(nextRetryTime);
        existing.setProcessStatus(PROCESS_STATUS_WAITING);
        existing.setLockUntil(null);
        message.setNextRetryTime(nextRetryTime);
        updateById(existing);
        if (!messageRetryRedisDAO.addToRetryQueue(message)) {
            log.warn("消息已更新数据库重试队列，但 Redis 入队失败: messageId={}", message.getId());
        }
    }

    /** 限流类识别:仅当统一错误码为 RATE_LIMIT */
    static boolean isRateLimit(MessageRecordDO message) {
        return message != null && RATE_LIMIT_ERROR_CODE.equals(message.getErrorCode());
    }

    /**
     * 限流类时间兜底判定:首次失败起是否已满 12 小时。
     * firstFailTime 为 null(历史数据)时视为未满,继续重试。
     */
    static boolean isRateLimitExhausted(LocalDateTime firstFailTime, LocalDateTime now) {
        if (firstFailTime == null) {
            return false;
        }
        return Duration.between(firstFailTime, now).getSeconds() >= RATE_LIMIT_MAX_DURATION_SECONDS;
    }

    LocalDateTime calculateNextRetryTime(String channel, int retryCount, boolean rateLimit) {
        if (rateLimit) {
            return LocalDateTime.now().plusSeconds(RATE_LIMIT_INTERVAL_SECONDS);
        }
        RetryStrategyConfigDO strategyConfig = retryStrategyConfigService.getStrategyConfig(channel);
        long plusSeconds = 0;
        if (ObjectUtil.isNull(strategyConfig)) {
            switch (ChannelTypeEnum.getByCode(channel)) {
                case SMS -> {
                    if (retryCount == 1) {
                        plusSeconds = 60 * 5;
                    } else if (retryCount == 2) {
                        plusSeconds = 60 * 10;
                    } else {
                        plusSeconds = 60 * 30;
                    }
                }
                case EMAIL -> {
                    if (retryCount == 1) {
                        plusSeconds = 60 * 2;
                    } else if (retryCount == 2) {
                        plusSeconds = 60 * 30;
                    } else if (retryCount == 3) {
                        plusSeconds = 60 * 60;
                    } else if (retryCount == 4) {
                        plusSeconds = 60 * 60 * 2;
                    } else {
                        plusSeconds = 60 * 60 * 4;
                    }
                }
                case APP -> {
                    if (retryCount == 1) {
                        plusSeconds = 60;
                    } else {
                        plusSeconds = 60 * 60;
                    }
                }
            }
        } else {
            String retryIntervals = strategyConfig.getRetryIntervals();
            String[] split = retryIntervals.split(String.valueOf(StrUtil.C_COMMA));
            plusSeconds = retryCount >= split.length
                    ? Long.parseLong(split[split.length - 1])
                    : Long.parseLong(split[retryCount - 1]);
        }
        return LocalDateTime.now().plusSeconds(plusSeconds);
    }

    @Override
    public void processRetryBatch(String channel) {
        long epochMilli = System.currentTimeMillis();
        List<String> claimedMessageIds = messageRetryRedisDAO.claimAndRemoveDueMessages(channel, epochMilli, DEFAULT_BATCH_SIZE);

        if (CollUtil.isEmpty(claimedMessageIds)) {
            log.debug("Redis 中没有待重试消息：channel={}", channel);
            fallbackToDatabase(channel);

            epochMilli = System.currentTimeMillis();
            claimedMessageIds = messageRetryRedisDAO.claimAndRemoveDueMessages(channel, epochMilli, DEFAULT_BATCH_SIZE);
            if (CollUtil.isEmpty(claimedMessageIds)) {
                return;
            }
        }

        LocalDateTime currentTime = LocalDateTime.now();
        LocalDateTime lockUntil = currentTime.plusSeconds(DEFAULT_LOCK_SECONDS);
        List<Long> dbClaimedMessageIds = claimedMessageIds.stream()
                .map(Long::valueOf)
                .filter(messageId -> claimRetryMessage(messageId, channel, currentTime, lockUntil))
                .collect(Collectors.toList());

        if (CollUtil.isEmpty(dbClaimedMessageIds)) {
            return;
        }

        List<MessageRecordDO> messageRecordDOList = messageRecordService.listByIds(dbClaimedMessageIds);
        for (MessageRecordDO messageRecordDO : messageRecordDOList) {
            processSingleRetry(messageRecordDO, RetrySourceEnum.AUTO_RETRY);
        }
    }

    private void processSingleRetry(MessageRecordDO messageRecordDO, RetrySourceEnum retrySource) {
        if (MsgStatusConstant.RETRYABLE_FAILED.equals(messageRecordDO.getStatus())) {
            messageRecordDO.setStatus(MsgStatusConstant.PENDING);
            messageRecordService.processSendMsg(Collections.singletonList(messageRecordDO), retrySource);
            messageRecordService.updateRetryCount(messageRecordDO.getId());
            finalizeRetryLease(messageRecordDO);
        } else {
            // 能被重新认领说明 120s 租约已过期、20s 回执窗口早过 —— SENDING/PENDING 即卡住,与终态一并清理
            deleteByMessageIds(Collections.singletonList(messageRecordDO.getId()));
        }
    }

    @Override
    @Transactional(rollbackFor = Exception.class)
    public void manualRetry(List<String> messageIds) {
        if (CollUtil.isEmpty(messageIds)) {
            return;
        }
        List<MessageRecordDO> messageRecordDOList = messageRecordService.listByIds(messageIds);
        for (MessageRecordDO messageRecordDO : messageRecordDOList) {
            processSingleRetry(messageRecordDO, RetrySourceEnum.MANUAL_RETRY);
        }
    }

    @Override
    public PageResult<MessageRetryQueueDO> getPage(MessageRetryQueueReqVO reqVO) {
        LambdaQueryWrapper<MessageRetryQueueDO> queryWrapper = new LambdaQueryWrapper<MessageRetryQueueDO>()
                .eq(ObjectUtil.isNotNull(reqVO.getMessageId()), MessageRetryQueueDO::getMessageId, reqVO.getMessageId())
                .eq(StrUtil.isNotBlank(reqVO.getChannel()), MessageRetryQueueDO::getChannel, reqVO.getChannel())
                .eq(StrUtil.isNotBlank(reqVO.getReceiver()), MessageRetryQueueDO::getReceiver, reqVO.getReceiver())
                .ge(ObjectUtil.isNotNull(reqVO.getMinRetryCount()), MessageRetryQueueDO::getRetryCount, reqVO.getMinRetryCount())
                .le(ObjectUtil.isNotNull(reqVO.getMaxRetryCount()), MessageRetryQueueDO::getRetryCount, reqVO.getMaxRetryCount())
                .orderByAsc(MessageRetryQueueDO::getNextRetryTime)
                .orderByDesc(MessageRetryQueueDO::getId);
        return baseMapper.selectPage(reqVO, queryWrapper);
    }

    @Override
    public boolean deleteByMessageIds(List<Long> messageIds) {
        if (CollUtil.isEmpty(messageIds)) {
            return false;
        }
        return this.baseMapper.delete(new LambdaQueryWrapper<MessageRetryQueueDO>().in(MessageRetryQueueDO::getMessageId, messageIds)) > 0;
    }

    @Override
    public void run(String... args) {
        rebuildRetryQueueOnStartup();
        startupRebuildCompleted.set(true);
    }

    @Override
    @Transactional(rollbackFor = Exception.class)
    public void rebuildRetryQueueOnStartup() {
        log.info("========== 开始重建重试队列 ==========");
        for (String channel : CHANNELS) {
            try {
                log.info("正在清空渠道 [{}] 的 Redis 重试队列", channel);
                messageRetryRedisDAO.clearRetryQueue(channel);

                log.info("正在从数据库加载渠道 [{}] 的待重试消息", channel);
                List<MessageRetryQueueDO> dbRecords = listEligibleQueueRecords(channel);
                if (CollUtil.isEmpty(dbRecords)) {
                    log.info("渠道 [{}] 没有待重试的消息", channel);
                    continue;
                }

                List<Long> messageIds = dbRecords.stream().map(MessageRetryQueueDO::getMessageId).collect(Collectors.toList());
                List<MessageRecordDO> messages = messageRecordService.listByIds(messageIds);
                if (CollUtil.isEmpty(messages)) {
                    log.warn("渠道 [{}] 的消息记录不存在", channel);
                    continue;
                }

                int successCount = messageRetryRedisDAO.batchAddToRetryQueue(messages);
                log.info("渠道 [{}] 重建完成：数据库记录数={}, 成功入队 Redis 数={}", channel, dbRecords.size(), successCount);
            } catch (Exception e) {
                log.error("重建渠道 [{}] 的重试队列失败", channel, e);
            }
        }
        log.info("========== 重试队列重建完成 ==========");
    }

    // 每60分钟主动从数据库中扫描一次，将消息重试数据同步到 Redis 中
    @Scheduled(fixedRate = 600000)
    @Override
    public void syncRetryQueueConsistency() {
        if (!startupRebuildCompleted.get()) {
            log.debug("启动重建未完成，跳过本次同步");
            return;
        }
        log.info("开始同步 Redis 与数据库重试队列一致性");

        for (String channel : CHANNELS) {
            List<MessageRetryQueueDO> dbRecords = listEligibleQueueRecords(channel);
            if (CollUtil.isEmpty(dbRecords)) {
                continue;
            }

            for (MessageRetryQueueDO dbRecord : dbRecords) {
                boolean existsInRedis = messageRetryRedisDAO.existsInRetryQueue(channel, dbRecord.getMessageId());
                if (existsInRedis) {
                    continue;
                }

                Long messageId = dbRecord.getMessageId();
                MessageRecordDO message = messageRecordService.getById(messageId);
                if (message != null && message.getNextRetryTime() != null) {
                    boolean added = messageRetryRedisDAO.addToRetryQueue(message);
                    if (added) {
                        log.debug("补回消息到 Redis: channel={}, messageId={}", channel, messageId);
                    } else {
                        log.warn("补回消息到 Redis 失败：channel={}, messageId={}", channel, messageId);
                    }
                } else {
                    log.warn("消息记录不存在或 next_retry_time 为空：channel={}, messageId={}", channel, messageId);
                }
            }
        }
    }

    @Override
    public void fallbackToDatabase(String channel) {
        if (!messageRetryRedisDAO.isRedisAvailable()) {
            log.warn("Redis 不可用，channel={}", channel);
            return;
        }
        fallbackToDatabaseDirectly(channel);
    }

    private void fallbackToDatabaseDirectly(String channel) {
        List<MessageRetryQueueDO> dbRecords = baseMapper.selectNeedRetryMessages(channel, LocalDateTime.now(), DEFAULT_BATCH_SIZE);
        if (CollUtil.isEmpty(dbRecords)) {
            return;
        }

        List<Long> messageIds = dbRecords.stream().map(MessageRetryQueueDO::getMessageId).collect(Collectors.toList());
        List<MessageRecordDO> messages = messageRecordService.listByIds(messageIds);
        if (CollUtil.isEmpty(messages)) {
            return;
        }

        if (messageRetryRedisDAO.isRedisAvailable()) {
            messageRetryRedisDAO.batchAddToRetryQueue(messages);
        }
    }

    /**
     * 通过设置lockUntil时间、process_status=1来从数据库层抢占该条重试消息。
     */
    private boolean claimRetryMessage(Long messageId, String channel, LocalDateTime currentTime, LocalDateTime lockUntil) {
        return baseMapper.claimRetryMessage(messageId, channel, currentTime, lockUntil) > 0;
    }

    /**
     * 完成重试租约处理，根据消息状态执行相应的清理或重置操作
     * - 如果消息状态为可重试失败，直接返回，不进行任何处理
     * - SENDING(两段式中间态)直接返回，保留记录与租约，等回执推进
     * - 如果消息状态为待处理，重置队列记录的处理状态和锁时间，并将消息重新加入Redis重试队列
     * - 其他状态（成功、最终失败等），从Redis重试队列移除并删除数据库中的重试队列记录
     *
     * @param messageRecordDO 消息记录对象
     */
    private void finalizeRetryLease(MessageRecordDO messageRecordDO) {
        String status = messageRecordDO.getStatus();

        // 可重试失败:保留记录,等下一轮认领重发 —— 不变
        if (MsgStatusConstant.RETRYABLE_FAILED.equals(status)) {
            return;
        }

        // 两段式发送的中间态:重发已受理、消息在途,保留记录、保持租约锁住、不回 Redis,等 20s 回执推进
        if (MsgStatusConstant.SENDING.equals(status)) {
            return;
        }

        // 处理过程中断但仍停留在 PENDING:主动释放 lease 并回队列(兜底补偿)—— 不变
        if (MsgStatusConstant.PENDING.equals(status)) {
            lambdaUpdate()
                    .eq(MessageRetryQueueDO::getMessageId, messageRecordDO.getId())
                    .set(MessageRetryQueueDO::getProcessStatus, PROCESS_STATUS_WAITING)
                    .set(MessageRetryQueueDO::getLockUntil, null)
                    .update();
            if (messageRecordDO.getNextRetryTime() != null) {
                messageRetryRedisDAO.addToRetryQueue(messageRecordDO);
            }
            return;
        }

        // 其余即终态:从 Redis 移除并删除数据库重试队列记录
        messageRetryRedisDAO.removeFromRetryQueue(messageRecordDO.getChannel(), messageRecordDO.getId());
        deleteByMessageIds(Collections.singletonList(messageRecordDO.getId()));
    }

    /**
     * 查询指定通道下符合重试条件的队列记录
     * 筛选条件：
     * - 匹配指定通道
     * - 下次重试时间不为空
     * - 处理状态为等待中，或处理状态为空，或锁时间为空，或锁时间已过期
     *
     * @param channel 消息通道
     * @return 符合条件的重试队列记录列表
     */
    private List<MessageRetryQueueDO> listEligibleQueueRecords(String channel) {
        LocalDateTime now = LocalDateTime.now();
        return lambdaQuery()
                .eq(MessageRetryQueueDO::getChannel, channel)
                .isNotNull(MessageRetryQueueDO::getNextRetryTime)
                .and(wrapper -> wrapper.eq(MessageRetryQueueDO::getProcessStatus, PROCESS_STATUS_WAITING)
                        .or().isNull(MessageRetryQueueDO::getProcessStatus)
                        .or().isNull(MessageRetryQueueDO::getLockUntil)
                        .or().lt(MessageRetryQueueDO::getLockUntil, now))
                .list();
    }
}
