package tech.easyflow.common.cache;

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.data.redis.core.script.DefaultRedisScript;
import org.springframework.stereotype.Component;

import java.nio.charset.StandardCharsets;
import java.security.MessageDigest;
import java.security.NoSuchAlgorithmException;
import java.time.Duration;
import java.util.Collections;
import java.util.HexFormat;
import java.util.Objects;
import java.util.UUID;
import java.util.concurrent.Executors;
import java.util.concurrent.ScheduledExecutorService;
import java.util.concurrent.ScheduledFuture;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicBoolean;

/**
 * 基于 Redis owner token 的通用幂等执行器。
 *
 * <p>该执行器用于降低持久化触发器重复投递造成的副作用重复执行窗口。处理中凭证和完成
 * 凭证均有界过期，异常时仅允许当前 owner 释放凭证。</p>
 */
@Component
public class RedisIdempotencyExecutor {

    private static final Logger log =
            LoggerFactory.getLogger(RedisIdempotencyExecutor.class);
    private static final String KEY_PREFIX = "idempotency:";
    private static final String PROCESSING_PREFIX = "P:";
    private static final String COMPLETED_PREFIX = "C:";
    private static final Duration DEFAULT_PROCESSING_TTL =
            Duration.ofMinutes(5);
    private static final Duration DEFAULT_COMPLETED_TTL =
            Duration.ofDays(3);
    private static final ScheduledExecutorService LEASE_RENEWER =
            Executors.newSingleThreadScheduledExecutor(runnable -> {
                Thread thread = new Thread(
                        runnable, "redis-idempotency-lease-renewer");
                thread.setDaemon(true);
                return thread;
            });
    private static final DefaultRedisScript<Long> CLAIM_SCRIPT = script(
            "local current = redis.call('get', KEYS[1]); "
                    + "if current == ARGV[3] then return 2 end; "
                    + "if current and string.sub(current, 3, 66) ~= ARGV[4] then return -1 end; "
                    + "if current then return 0 end; "
                    + "redis.call('psetex', KEYS[1], ARGV[2], ARGV[1]); return 1");
    private static final DefaultRedisScript<Long> COMPLETE_SCRIPT = script(
            "if redis.call('get', KEYS[1]) == ARGV[1] then "
                    + "redis.call('psetex', KEYS[1], ARGV[2], ARGV[3]); return 1 "
                    + "else return 0 end");
    private static final DefaultRedisScript<Long> RELEASE_SCRIPT = script(
            "if redis.call('get', KEYS[1]) == ARGV[1] then "
                    + "return redis.call('del', KEYS[1]) else return 0 end");
    private static final DefaultRedisScript<Long> RENEW_SCRIPT = script(
            "if redis.call('get', KEYS[1]) == ARGV[1] then "
                    + "return redis.call('pexpire', KEYS[1], ARGV[2]) "
                    + "else return 0 end");

    private final StringRedisTemplate redisTemplate;
    private final Duration processingTtl;
    private final Duration completedTtl;

    /**
     * 创建 Redis 幂等执行器。
     *
     * @param redisTemplate Redis 字符串模板
     */
    @Autowired
    public RedisIdempotencyExecutor(StringRedisTemplate redisTemplate) {
        this(
                redisTemplate,
                DEFAULT_PROCESSING_TTL,
                DEFAULT_COMPLETED_TTL);
    }

    /**
     * 创建可指定租约周期的 Redis 幂等执行器。
     *
     * @param redisTemplate Redis 字符串模板
     * @param processingTtl 处理中 owner 租约周期
     * @param completedTtl 完成凭证保留周期
     */
    RedisIdempotencyExecutor(
            StringRedisTemplate redisTemplate,
            Duration processingTtl,
            Duration completedTtl) {
        this.redisTemplate = Objects.requireNonNull(
                redisTemplate, "redisTemplate must not be null");
        this.processingTtl = requirePositive(
                processingTtl, "processingTtl");
        this.completedTtl = requirePositive(
                completedTtl, "completedTtl");
    }

    /**
     * 以稳定幂等键至多执行一次当前可观测操作。
     *
     * <p>返回 {@code false} 表示该键已有成功记录。另一个 owner 仍在执行时抛出明确异常，
     * 由上层持久化触发器稍后重试，避免提前返回虚假成功。</p>
     *
     * @param idempotencyKey 稳定业务幂等键
     * @param action 副作用操作
     * @return 本次实际执行操作时为 {@code true}，已有成功记录时为 {@code false}
     * @throws IdempotentOperationInProgressException 同一操作仍由其他 owner 执行时抛出
     * @throws IllegalStateException 操作完成后无法确认幂等凭证时抛出
     */
    public boolean executeOnce(String idempotencyKey, Runnable action) {
        return executeOnce(idempotencyKey, sha256(""), action);
    }

    /**
     * 以稳定幂等键和负载摘要至多执行一次当前可观测操作。
     *
     * @param idempotencyKey 稳定业务幂等键
     * @param payloadHash 负载摘要，用于拒绝同键不同数据
     * @param action 副作用操作
     * @return 本次实际执行操作时为 {@code true}，已有成功记录时为 {@code false}
     * @throws IdempotentOperationInProgressException 同一操作仍在执行
     * @throws IdempotencyPayloadMismatchException 同一幂等键对应不同负载
     */
    public boolean executeOnce(String idempotencyKey, String payloadHash, Runnable action) {
        if (idempotencyKey == null || idempotencyKey.isBlank()) {
            Objects.requireNonNull(action, "action must not be null").run();
            return true;
        }
        Objects.requireNonNull(action, "action must not be null");
        String normalizedPayloadHash = Objects.requireNonNull(
                payloadHash, "payloadHash must not be null");
        String key = KEY_PREFIX + sha256(idempotencyKey);
        String ownerToken = UUID.randomUUID().toString();
        String processingValue = PROCESSING_PREFIX + normalizedPayloadHash + ":" + ownerToken;
        String completedValue = COMPLETED_PREFIX + normalizedPayloadHash;
        Long claim = redisTemplate.execute(
                CLAIM_SCRIPT,
                Collections.singletonList(key),
                processingValue,
                String.valueOf(processingTtl.toMillis()),
                completedValue,
                normalizedPayloadHash);
        if (Long.valueOf(2L).equals(claim)) {
            return false;
        }
        if (Long.valueOf(-1L).equals(claim)) {
            throw new IdempotencyPayloadMismatchException(idempotencyKey);
        }
        if (!Long.valueOf(1L).equals(claim)) {
            throw new IdempotentOperationInProgressException(idempotencyKey);
        }

        AtomicBoolean ownershipLost = new AtomicBoolean();
        long renewIntervalMillis = Math.max(
                1L, processingTtl.toMillis() / 3L);
        ScheduledFuture<?> renewal = LEASE_RENEWER.scheduleAtFixedRate(
                () -> renewLease(
                        key, processingValue, ownershipLost),
                renewIntervalMillis,
                renewIntervalMillis,
                TimeUnit.MILLISECONDS);
        try {
            action.run();
        } catch (RuntimeException | Error error) {
            renewal.cancel(false);
            redisTemplate.execute(
                    RELEASE_SCRIPT, Collections.singletonList(key), processingValue);
            throw error;
        }
        renewal.cancel(false);
        if (ownershipLost.get()) {
            throw new IllegalStateException(
                    "Idempotency ownership lost while operation was running");
        }
        Long completed = redisTemplate.execute(
                COMPLETE_SCRIPT,
                Collections.singletonList(key),
                processingValue,
                String.valueOf(completedTtl.toMillis()),
                completedValue);
        if (!Long.valueOf(1L).equals(completed)) {
            // 副作用已经完成时保留处理中凭证，避免确认异常立即打开重复执行窗口。
            throw new IllegalStateException(
                    "Idempotency ownership lost after operation completed");
        }
        return true;
    }

    /**
     * 续期仍由当前 owner 持有的处理中凭证。
     *
     * @param key Redis 幂等键
     * @param processingValue 当前 owner 完整凭证
     * @param ownershipLost owner 丢失标记
     */
    private void renewLease(
            String key,
            String processingValue,
            AtomicBoolean ownershipLost) {
        try {
            Long renewed = redisTemplate.execute(
                    RENEW_SCRIPT,
                    Collections.singletonList(key),
                    processingValue,
                    String.valueOf(processingTtl.toMillis()));
            if (!Long.valueOf(1L).equals(renewed)) {
                ownershipLost.set(true);
            }
        } catch (RuntimeException error) {
            // 短暂 Redis 故障由后续周期继续续期；最终完成脚本仍会校验 owner token。
            log.warn("Redis idempotency lease renewal failed", error);
        }
    }

    /**
     * 校验正数时长配置。
     *
     * @param duration 配置值
     * @param name 配置名
     * @return 原配置值
     */
    private Duration requirePositive(
            Duration duration, String name) {
        Duration value = Objects.requireNonNull(
                duration, name + " must not be null");
        if (value.isZero() || value.isNegative()) {
            throw new IllegalArgumentException(
                    name + " must be positive");
        }
        return value;
    }

    /**
     * 计算有界 Redis key 摘要。
     *
     * @param value 原始幂等键
     * @return SHA-256 十六进制摘要
     */
    private String sha256(String value) {
        try {
            MessageDigest digest = MessageDigest.getInstance("SHA-256");
            return HexFormat.of().formatHex(digest.digest(
                    value.getBytes(StandardCharsets.UTF_8)));
        } catch (NoSuchAlgorithmException error) {
            throw new IllegalStateException("SHA-256 is unavailable", error);
        }
    }

    /**
     * 创建返回 Long 的 Redis Lua 脚本。
     *
     * @param text 脚本文本
     * @return Redis 脚本
     */
    private static DefaultRedisScript<Long> script(String text) {
        DefaultRedisScript<Long> script = new DefaultRedisScript<>();
        script.setScriptText(text);
        script.setResultType(Long.class);
        return script;
    }

    /**
     * 同一幂等操作仍在执行时的冲突异常。
     */
    public static final class IdempotentOperationInProgressException extends IllegalStateException {

        /**
         * 创建幂等执行冲突异常。
         *
         * @param idempotencyKey 冲突的业务幂等键
         */
        public IdempotentOperationInProgressException(String idempotencyKey) {
            super("Idempotent operation is already in progress: " + idempotencyKey);
        }
    }

    /**
     * 同一幂等键被不同负载复用时的冲突异常。
     */
    public static final class IdempotencyPayloadMismatchException extends IllegalStateException {

        /**
         * 创建负载冲突异常。
         *
         * @param idempotencyKey 冲突的业务幂等键
         */
        public IdempotencyPayloadMismatchException(String idempotencyKey) {
            super("Idempotency payload mismatch: " + idempotencyKey);
        }
    }
}
