package cn.novalon.gym.manage.member.handler;

import cn.novalon.gym.manage.member.entity.MemberCardRecord;
import com.fasterxml.jackson.databind.ObjectMapper;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.data.domain.Range;
import org.springframework.data.redis.connection.RedisZSetCommands;
import org.springframework.data.redis.core.ReactiveStringRedisTemplate;
import org.springframework.scheduling.annotation.Scheduled;
import org.springframework.stereotype.Component;
import reactor.core.publisher.Flux;
import reactor.core.publisher.Mono;

import java.time.Duration;
import java.time.LocalDateTime;
import java.util.UUID;

/**
 * 会员卡到期提醒服务
 * 
 * @author 付嘉
 * @date 2026-05-27
 */
@Slf4j
@Component
@RequiredArgsConstructor
public class ExpirationReminderService {

    private final ReactiveStringRedisTemplate redisTemplate;
    private final ObjectMapper objectMapper;

    private static final String REMINDER_QUEUE = "queue:member_card_expiration";
    private static final String DEAD_LETTER_QUEUE = "queue:member_card_expiration_dead_letter";
    private static final int REMINDER_DAYS_BEFORE = 7;
    private static final long MAX_DELAY_MILLIS = Duration.ofDays(365).toMillis();

    /**
     * 设置到期提醒（购卡/续费时调用）
     */
    public Mono<Void> scheduleExpirationReminder(MemberCardRecord record) {
        if (record.getExpireTime() == null) {
            return Mono.empty();
        }

        LocalDateTime now = LocalDateTime.now();

        Flux<Void> reminderFlux = Flux.range(1, REMINDER_DAYS_BEFORE)
            .flatMap(daysBefore -> {
                LocalDateTime reminderTime = record.getExpireTime().minusDays(daysBefore);

                if (reminderTime.isBefore(now)) {
                    log.debug("会员卡记录ID={} 的{}天前提醒时间已过，跳过",
                        record.getMemberCardRecordId(), daysBefore);
                    return Mono.empty();
                }

                long delayMillis = Duration.between(now, reminderTime).toMillis();

                if (delayMillis > MAX_DELAY_MILLIS) {
                    log.warn("会员卡记录ID={} 的{}天后提醒时间超过1年，跳过",
                        record.getMemberCardRecordId(), daysBefore);
                    return Mono.empty();
                }

                try {
                    String messageId = UUID.randomUUID().toString();
                    String message = objectMapper.writeValueAsString(new ExpirationMessage(
                            messageId,
                            record.getMemberCardRecordId(),
                            record.getMemberId(),
                            record.getExpireTime(),
                            daysBefore
                    ));

                    double executeTime = System.currentTimeMillis() + delayMillis;

                    return redisTemplate.opsForZSet()
                            .add(REMINDER_QUEUE, message, executeTime)
                            .doOnSuccess(v -> log.info("设置会员卡到期提醒: recordId={}, daysBefore={}, expireTime={}, executeTime={}",
                                    record.getMemberCardRecordId(), daysBefore, record.getExpireTime(), executeTime))
                            .then();
                } catch (Exception e) {
                    log.error("设置会员卡到期提醒失败: recordId={}, daysBefore={}",
                        record.getMemberCardRecordId(), daysBefore, e);
                    return Mono.error(e);
                }
            });

        return reminderFlux.then();
    }

    /**
     * 定时任务：每分钟扫描到期的提醒并发送
     */
    @Scheduled(fixedRate = 60000)
    public void processDueReminders() {
        double now = System.currentTimeMillis();

        redisTemplate.opsForZSet()
                .rangeByScoreWithScores(
                        REMINDER_QUEUE,
                        Range.from(Range.Bound.inclusive(0.0))
                                .to(Range.Bound.inclusive(now)),
                        RedisZSetCommands.Limit.limit().count(100)
                )
                .flatMap(tuple -> {
                    String message = tuple.getValue();
                    if (message == null) {
                        return Mono.empty();
                    }

                    return Mono.fromCallable(() -> objectMapper.readValue(message, ExpirationMessage.class))
                        .flatMap(expirationMessage -> {
                            log.info("处理到期提醒: messageId={}, memberId={}, expireTime={}, daysBefore={}",
                                    expirationMessage.messageId(),
                                    expirationMessage.memberId(),
                                    expirationMessage.expireTime(),
                                    expirationMessage.daysBefore());

                            // TODO: 集成微信/短信通知服务
                            sendNotification(expirationMessage);

                            return redisTemplate.opsForZSet()
                                .remove(REMINDER_QUEUE, message)
                                .doOnSuccess(removed -> {
                                    if (removed > 0) {
                                        log.info("成功删除已处理的提醒消息");
                                    }
                                });
                        })
                        .onErrorResume(e -> {
                            log.error("解析到期提醒消息失败，移至死信队列: message={}", message, e);
                            return redisTemplate.opsForZSet()
                                .add(DEAD_LETTER_QUEUE, message, System.currentTimeMillis())
                                .then(Mono.empty());
                        });
                })
                .then()
                .subscribe();
    }


    private void sendNotification(ExpirationMessage reminder) {
        // TODO: 实际项目中调用微信模板消息或短信API
        log.info("[模拟发送] 会员卡到期提醒 - 会员ID: {}, 到期时间: {}, 提前天数: {}",
            reminder.memberId(), reminder.expireTime(), reminder.daysBefore());
    }

    public record ExpirationMessage(
            String messageId,
            Long recordId,
            Long memberId,
            LocalDateTime expireTime,
            int daysBefore
    ) {}
}
