package tech.easyflow.ai.easyagentsflow.repository;

import com.easyagents.flow.core.chain.runtime.Trigger;
import com.easyagents.flow.core.chain.runtime.TriggerClaimLostException;
import com.easyagents.flow.core.chain.runtime.TriggerStore;
import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.databind.ObjectMapper;
import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.data.redis.core.script.DefaultRedisScript;
import org.springframework.stereotype.Component;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import tech.easyflow.common.constant.CacheKey;

import java.time.Duration;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.IdentityHashMap;
import java.util.List;
import java.util.Map;
import java.util.UUID;

/**
 * 基于 Redis 有序集合和租约认领的工作流触发器仓储。
 *
 * <p>当前实现的待执行集合与触发器数据使用同一 Lua 操作，派生触发器保存时会同时
 * 校验实例锁 fencing token 和父触发器认领代际。部署约束为 Redis Standalone 或
 * Sentinel；不支持 Redis Cluster。</p>
 */
@Component
public class RedisTriggerStore implements TriggerStore {

    private static final Logger log = LoggerFactory.getLogger(RedisTriggerStore.class);
    private static final Duration TRIGGER_TTL = Duration.ofDays(3);
    private static final Duration CLAIM_GENERATION_TTL = Duration.ofDays(4);
    /**
     * 与底层调度器本地 Future 容量对齐，避免已预热任务反复占据扫描窗口。
     */
    private static final int DUE_BATCH_SIZE = 1024;
    private static final int RECOVERY_BATCH_SIZE = 1000;
    private static final DefaultRedisScript<Long> SAVE_SCRIPT = longScript(
            "redis.call('psetex', KEYS[1], ARGV[1], ARGV[2]); "
                    + "redis.call('zadd', KEYS[2], ARGV[3], ARGV[4]); return 1");
    private static final DefaultRedisScript<Long> SAVE_IF_ABSENT_SCRIPT =
            longScript(
                    "if redis.call('exists', KEYS[1]) == 1 then return 0 end; "
                            + "redis.call('psetex', KEYS[1], ARGV[1], ARGV[2]); "
                            + "redis.call('zadd', KEYS[2], ARGV[3], ARGV[4]); return 1");
    private static final DefaultRedisScript<Long> LOCK_GUARDED_SAVE_SCRIPT = longScript(
            "local fence = redis.call('hget', KEYS[3], 'version'); "
                    + "if not fence or fence ~= ARGV[5] then return 0 end; "
                    + "redis.call('psetex', KEYS[1], ARGV[1], ARGV[2]); "
                    + "redis.call('zadd', KEYS[2], ARGV[3], ARGV[4]); return 1");
    private static final DefaultRedisScript<Long>
            LOCK_GUARDED_SAVE_IF_ABSENT_SCRIPT =
            longScript(
                    "local fence = redis.call('hget', KEYS[3], 'version'); "
                            + "if not fence or fence ~= ARGV[5] then return -1 end; "
                            + "if redis.call('exists', KEYS[1]) == 1 then return 0 end; "
                            + "redis.call('psetex', KEYS[1], ARGV[1], ARGV[2]); "
                            + "redis.call('zadd', KEYS[2], ARGV[3], ARGV[4]); return 1");
    private static final DefaultRedisScript<Long> DOUBLE_GUARDED_SAVE_SCRIPT = longScript(
            "local fence = redis.call('hget', KEYS[3], 'version'); "
                    + "local claim = redis.call('hget', KEYS[4], 'version'); "
                    + "if not fence or fence ~= ARGV[5] "
                    + "or not claim or claim ~= ARGV[6] then return 0 end; "
                    + "redis.call('psetex', KEYS[1], ARGV[1], ARGV[2]); "
                    + "redis.call('zadd', KEYS[2], ARGV[3], ARGV[4]); return 1");
    private static final DefaultRedisScript<Long>
            DOUBLE_GUARDED_SAVE_IF_ABSENT_SCRIPT =
            longScript(
                    "local fence = redis.call('hget', KEYS[3], 'version'); "
                            + "local claim = redis.call('hget', KEYS[4], 'version'); "
                            + "if not fence or fence ~= ARGV[5] "
                            + "or not claim or claim ~= ARGV[6] then return -1 end; "
                            + "if redis.call('exists', KEYS[1]) == 1 then return 0 end; "
                            + "redis.call('psetex', KEYS[1], ARGV[1], ARGV[2]); "
                            + "redis.call('zadd', KEYS[2], ARGV[3], ARGV[4]); return 1");
    private static final DefaultRedisScript<String> CLAIM_SCRIPT = stringScript(
            "if redis.call('exists', KEYS[1]) == 0 then "
                    + "redis.call('zrem', KEYS[3], ARGV[3]); return nil end; "
                    + "local claimed = redis.call('set', KEYS[2], ARGV[1], 'PX', ARGV[2], 'NX'); "
                    + "if not claimed then return nil end; "
                    + "local payload = redis.call('get', KEYS[1]); "
                    + "local generation = redis.call('hincrby', KEYS[5], 'version', 1); "
                    + "redis.call('pexpire', KEYS[5], ARGV[5]); "
                    + "redis.call('hset', KEYS[4], 'version', generation); "
                    + "redis.call('pexpire', KEYS[4], ARGV[2]); "
                    + "redis.call('zadd', KEYS[3], ARGV[4], ARGV[3]); "
                    + "return tostring(generation) .. '\\n' .. payload");
    private static final DefaultRedisScript<Long> ACK_SCRIPT = longScript(
            "if redis.call('get', KEYS[1]) == ARGV[1] then "
                    + "redis.call('del', KEYS[1]); redis.call('del', KEYS[2]); "
                    + "redis.call('del', KEYS[4]); "
                    + "redis.call('zrem', KEYS[3], ARGV[2]); return 1 else return 0 end");
    private static final DefaultRedisScript<Long> RELEASE_SCRIPT = longScript(
            "if redis.call('get', KEYS[1]) == ARGV[1] then "
                    + "redis.call('del', KEYS[1]); redis.call('del', KEYS[4]); "
                    + "redis.call('psetex', KEYS[3], ARGV[4], ARGV[5]); "
                    + "redis.call('zadd', KEYS[2], ARGV[3], ARGV[2]); "
                    + "return 1 else return 0 end");
    private static final DefaultRedisScript<Long> MARK_DEAD_LETTER_PENDING_SCRIPT =
            longScript(
                    "if redis.call('get', KEYS[1]) == ARGV[1] then "
                            + "redis.call('psetex', KEYS[2], ARGV[2], ARGV[3]); "
                            + "return 1 else return 0 end");
    private static final DefaultRedisScript<Long> DEAD_LETTER_SCRIPT = longScript(
            "if redis.call('get', KEYS[1]) == ARGV[1] then "
                    + "redis.call('psetex', KEYS[4], ARGV[3], ARGV[4]); "
                    + "redis.call('del', KEYS[5]); "
                    + "redis.call('del', KEYS[1]); redis.call('del', KEYS[2]); "
                    + "redis.call('zrem', KEYS[3], ARGV[2]); return 1 else return 0 end");
    private static final DefaultRedisScript<Long> RENEW_SCRIPT = longScript(
            "local guard = redis.call('hget', KEYS[3], 'version'); "
                    + "if redis.call('get', KEYS[1]) == ARGV[1] "
                    + "and guard and guard == ARGV[5] then "
                    + "redis.call('pexpire', KEYS[1], ARGV[2]); "
                    + "redis.call('pexpire', KEYS[3], ARGV[2]); "
                    + "redis.call('zadd', KEYS[2], ARGV[4], ARGV[3]); "
                    + "return 1 else return 0 end");
    private static final DefaultRedisScript<Long> REMOVE_SCRIPT = longScript(
            "redis.call('del', KEYS[1]); redis.call('del', KEYS[2]); "
                    + "return redis.call('zrem', KEYS[3], ARGV[1])");

    private final StringRedisTemplate redisTemplate;
    private final ObjectMapper objectMapper;
    private final Map<Trigger, ClaimContext> claimContexts =
            Collections.synchronizedMap(new IdentityHashMap<>());

    /**
     * 创建 Redis 触发器仓储。
     *
     * @param redisTemplate Redis 字符串模板
     * @param objectMapper JSON 序列化器
     */
    public RedisTriggerStore(StringRedisTemplate redisTemplate,
                             ObjectMapper objectMapper) {
        this.redisTemplate = redisTemplate;
        this.objectMapper = objectMapper;
    }

    /**
     * {@inheritDoc}
     */
    @Override
    public Trigger save(Trigger trigger) {
        if (trigger.getId() == null) {
            trigger.setId(UUID.randomUUID().toString());
        }
        long ttlMillis = Math.max(
                TRIGGER_TTL.toMillis(),
                Math.max(0L, trigger.getTriggerAt() - System.currentTimeMillis())
                        + TRIGGER_TTL.toMillis());
        List<String> keys = new ArrayList<>();
        keys.add(dataKey(trigger.getId()));
        keys.add(CacheKey.TRIGGER_PENDING_KEY);
        Long saved;
        long requiredLockFencingToken = trigger.getRequiredLockFencingToken();
        long requiredClaimGeneration = trigger.getRequiredFencingToken();
        if (requiredLockFencingToken > 0L && requiredClaimGeneration > 0L) {
            String requiredClaimId = requireText(
                    trigger.getRequiredFencingClaimId(),
                    "required fencing claim ID");
            keys.add(lockFenceKey(trigger.getStateInstanceId()));
            keys.add(executionGuardKey(trigger.getStateInstanceId(), requiredClaimId));
            saved = redisTemplate.execute(
                    DOUBLE_GUARDED_SAVE_SCRIPT,
                    keys,
                    String.valueOf(ttlMillis),
                    serialize(trigger),
                    String.valueOf(trigger.getTriggerAt()),
                    trigger.getId(),
                    String.valueOf(requiredLockFencingToken),
                    String.valueOf(requiredClaimGeneration));
        } else if (requiredLockFencingToken > 0L) {
            keys.add(lockFenceKey(trigger.getStateInstanceId()));
            saved = redisTemplate.execute(
                    LOCK_GUARDED_SAVE_SCRIPT,
                    keys,
                    String.valueOf(ttlMillis),
                    serialize(trigger),
                    String.valueOf(trigger.getTriggerAt()),
                    trigger.getId(),
                    String.valueOf(requiredLockFencingToken));
        } else if (requiredClaimGeneration > 0L) {
            throw new IllegalArgumentException(
                    "required lock fencing token is required with claim generation");
        } else {
            saved = redisTemplate.execute(
                    SAVE_SCRIPT,
                    keys,
                    String.valueOf(ttlMillis),
                    serialize(trigger),
                    String.valueOf(trigger.getTriggerAt()),
                    trigger.getId());
        }
        if (!Long.valueOf(1L).equals(saved)) {
            throw new TriggerClaimLostException(trigger.getId());
        }
        return trigger;
    }

    /**
     * {@inheritDoc}
     */
    @Override
    public boolean saveIfAbsent(Trigger trigger) {
        String triggerId =
                requireText(trigger.getId(),
                        "stable trigger ID");
        long ttlMillis = Math.max(
                TRIGGER_TTL.toMillis(),
                Math.max(0L,
                        trigger.getTriggerAt()
                                - System.currentTimeMillis())
                        + TRIGGER_TTL.toMillis());
        List<String> keys = new ArrayList<>();
        keys.add(dataKey(triggerId));
        keys.add(CacheKey.TRIGGER_PENDING_KEY);
        Long saved;
        long requiredLockFencingToken =
                trigger.getRequiredLockFencingToken();
        long requiredClaimGeneration =
                trigger.getRequiredFencingToken();
        if (requiredLockFencingToken > 0L
                && requiredClaimGeneration > 0L) {
            String requiredClaimId = requireText(
                    trigger.getRequiredFencingClaimId(),
                    "required fencing claim ID");
            keys.add(lockFenceKey(
                    trigger.getStateInstanceId()));
            keys.add(executionGuardKey(
                    trigger.getStateInstanceId(),
                    requiredClaimId));
            saved = redisTemplate.execute(
                    DOUBLE_GUARDED_SAVE_IF_ABSENT_SCRIPT,
                    keys,
                    String.valueOf(ttlMillis),
                    serialize(trigger),
                    String.valueOf(trigger.getTriggerAt()),
                    triggerId,
                    String.valueOf(requiredLockFencingToken),
                    String.valueOf(requiredClaimGeneration));
        } else if (requiredLockFencingToken > 0L) {
            keys.add(lockFenceKey(
                    trigger.getStateInstanceId()));
            saved = redisTemplate.execute(
                    LOCK_GUARDED_SAVE_IF_ABSENT_SCRIPT,
                    keys,
                    String.valueOf(ttlMillis),
                    serialize(trigger),
                    String.valueOf(trigger.getTriggerAt()),
                    triggerId,
                    String.valueOf(requiredLockFencingToken));
        } else if (requiredClaimGeneration > 0L) {
            throw new IllegalArgumentException(
                    "required lock fencing token is required with claim generation");
        } else {
            saved = redisTemplate.execute(
                    SAVE_IF_ABSENT_SCRIPT,
                    keys,
                    String.valueOf(ttlMillis),
                    serialize(trigger),
                    String.valueOf(trigger.getTriggerAt()),
                    triggerId);
        }
        if (Long.valueOf(1L).equals(saved)) {
            return true;
        }
        if (Long.valueOf(0L).equals(saved)) {
            return false;
        }
        if (Long.valueOf(-1L).equals(saved)) {
            throw new TriggerClaimLostException(triggerId);
        }
        throw new IllegalStateException(
                "Trigger create returned no result: "
                        + triggerId);
    }

    /**
     * {@inheritDoc}
     */
    @Override
    public boolean remove(String triggerId) {
        List<String> guardKeys = removeLocalClaims(triggerId);
        if (!guardKeys.isEmpty()) {
            redisTemplate.delete(guardKeys);
        }
        Long removed = redisTemplate.execute(
                REMOVE_SCRIPT,
                java.util.Arrays.asList(
                        dataKey(triggerId), claimKey(triggerId), CacheKey.TRIGGER_PENDING_KEY),
                triggerId);
        return Long.valueOf(1L).equals(removed);
    }

    /**
     * {@inheritDoc}
     */
    @Override
    public Trigger find(String triggerId) {
        return deserialize(redisTemplate.opsForValue().get(dataKey(triggerId)));
    }

    /**
     * {@inheritDoc}
     */
    @Override
    public List<Trigger> findDue(long uptoTimestamp) {
        return findByScore(0L, uptoTimestamp, DUE_BATCH_SIZE);
    }

    /**
     * {@inheritDoc}
     */
    @Override
    public List<Trigger> findAllPending() {
        return findByScore(0L, Long.MAX_VALUE, RECOVERY_BATCH_SIZE);
    }

    /**
     * {@inheritDoc}
     */
    @Override
    public Trigger claim(String triggerId, long leaseMillis) {
        return claim(find(triggerId), leaseMillis);
    }

    /**
     * {@inheritDoc}
     */
    @Override
    public Trigger claim(Trigger candidate, long leaseMillis) {
        if (candidate == null) {
            return null;
        }
        String triggerId = requireText(candidate.getId(), "trigger ID");
        String instanceId = requireText(candidate.getStateInstanceId(), "state instance ID");
        String claimToken = UUID.randomUUID().toString();
        long lease = Math.max(1L, leaseMillis);
        String guardKey = executionGuardKey(instanceId, triggerId);
        String payload = redisTemplate.execute(
                CLAIM_SCRIPT,
                Arrays.asList(
                        dataKey(triggerId),
                        claimKey(triggerId),
                        CacheKey.TRIGGER_PENDING_KEY,
                        guardKey,
                        claimGenerationKey(instanceId)),
                claimToken,
                String.valueOf(lease),
                triggerId,
                String.valueOf(System.currentTimeMillis() + lease),
                String.valueOf(CLAIM_GENERATION_TTL.toMillis()));
        int separator = payload == null ? -1 : payload.indexOf('\n');
        if (payload != null && separator <= 0) {
            redisTemplate.delete(guardKey);
            throw new IllegalStateException(
                    "Claim result is missing generation: " + triggerId);
        }
        long claimGeneration = payload == null
                ? 0L
                : Long.parseLong(payload.substring(0, separator));
        Trigger trigger = deserialize(
                payload == null ? null : payload.substring(separator + 1));
        if (trigger != null) {
            if (!triggerId.equals(trigger.getId())
                    || !instanceId.equals(trigger.getStateInstanceId())) {
                redisTemplate.delete(guardKey);
                throw new IllegalStateException("Claimed trigger identity changed: " + triggerId);
            }
            trigger.setFencingToken(claimGeneration);
            claimContexts.put(
                    trigger,
                    new ClaimContext(claimToken, guardKey, claimGeneration));
        }
        return trigger;
    }

    /**
     * {@inheritDoc}
     */
    @Override
    public boolean renewClaim(Trigger trigger, long leaseMillis) {
        ClaimContext claim = claimContexts.get(trigger);
        if (claim == null) {
            return false;
        }
        long lease = Math.max(1L, leaseMillis);
        Long renewed = redisTemplate.execute(
                RENEW_SCRIPT,
                Arrays.asList(
                        claimKey(trigger.getId()),
                        CacheKey.TRIGGER_PENDING_KEY,
                        claim.guardKey),
                claim.ownerToken,
                String.valueOf(lease),
                trigger.getId(),
                String.valueOf(System.currentTimeMillis() + lease),
                String.valueOf(claim.claimGeneration));
        return Long.valueOf(1L).equals(renewed);
    }

    /**
     * {@inheritDoc}
     */
    @Override
    public void acknowledge(Trigger trigger) {
        ClaimContext claim = claimContexts.get(trigger);
        if (claim == null) {
            throw new TriggerClaimLostException(
                    trigger.getId());
        }
        Long acknowledged = redisTemplate.execute(
                ACK_SCRIPT,
                Arrays.asList(
                        claimKey(trigger.getId()),
                        dataKey(trigger.getId()),
                        CacheKey.TRIGGER_PENDING_KEY,
                        claim.guardKey),
                claim.ownerToken,
                trigger.getId());
        finishClaimMutation(
                trigger, claim, acknowledged);
    }

    /**
     * {@inheritDoc}
     */
    @Override
    public void release(Trigger trigger) {
        ClaimContext claim = claimContexts.get(trigger);
        if (claim == null) {
            throw new TriggerClaimLostException(
                    trigger.getId());
        }
        Long released = redisTemplate.execute(
                RELEASE_SCRIPT,
                Arrays.asList(
                        claimKey(trigger.getId()),
                        CacheKey.TRIGGER_PENDING_KEY,
                        dataKey(trigger.getId()),
                        claim.guardKey),
                claim.ownerToken,
                trigger.getId(),
                String.valueOf(trigger.getTriggerAt()),
                String.valueOf(TRIGGER_TTL.toMillis()),
                serialize(trigger));
        finishClaimMutation(
                trigger, claim, released);
    }

    /**
     * {@inheritDoc}
     */
    @Override
    public void markDeadLetterPending(
            Trigger trigger) {
        ClaimContext claim =
                claimContexts.get(trigger);
        if (claim == null) {
            throw new TriggerClaimLostException(
                    trigger.getId());
        }
        Long marked = redisTemplate.execute(
                MARK_DEAD_LETTER_PENDING_SCRIPT,
                Arrays.asList(
                        claimKey(trigger.getId()),
                        dataKey(trigger.getId())),
                claim.ownerToken,
                String.valueOf(
                        TRIGGER_TTL.toMillis()),
                serialize(trigger));
        if (Long.valueOf(1L).equals(marked)) {
            return;
        }
        if (Long.valueOf(0L).equals(marked)) {
            claimContexts.remove(trigger, claim);
            throw new TriggerClaimLostException(
                    trigger.getId());
        }
        throw new IllegalStateException(
                "Dead-letter marker returned no result: "
                        + trigger.getId());
    }

    /**
     * {@inheritDoc}
     */
    @Override
    public void deadLetter(Trigger trigger, String reason) {
        ClaimContext claim = claimContexts.get(trigger);
        if (claim == null) {
            throw new TriggerClaimLostException(
                    trigger.getId());
        }
        Long moved = redisTemplate.execute(
                DEAD_LETTER_SCRIPT,
                Arrays.asList(
                        claimKey(trigger.getId()),
                        dataKey(trigger.getId()),
                        CacheKey.TRIGGER_PENDING_KEY,
                        CacheKey.TRIGGER_DEAD_LETTER_KEY + trigger.getId(),
                        claim.guardKey),
                claim.ownerToken,
                trigger.getId(),
                String.valueOf(TRIGGER_TTL.toMillis()),
                serialize(trigger));
        if (Long.valueOf(1L).equals(moved)) {
            claimContexts.remove(trigger, claim);
            log.error(
                    "Workflow trigger moved to dead letter, triggerId={}, reason={}",
                    trigger.getId(),
                    reason);
            return;
        }
        if (Long.valueOf(0L).equals(moved)) {
            claimContexts.remove(trigger, claim);
            throw new TriggerClaimLostException(
                    trigger.getId());
        }
        throw new IllegalStateException(
                "Dead-letter operation returned no result: "
                        + trigger.getId());
    }

    /**
     * 校验 claim 变更结果，并在 Redis 已完成或确认失去 owner 后清理本地凭证。
     *
     * @param trigger 已认领触发器
     * @param claim 本地认领上下文
     * @param result Redis 原子脚本结果
     */
    private void finishClaimMutation(
            Trigger trigger,
            ClaimContext claim,
            Long result) {
        if (Long.valueOf(1L).equals(result)) {
            claimContexts.remove(trigger, claim);
            return;
        }
        if (Long.valueOf(0L).equals(result)) {
            claimContexts.remove(trigger, claim);
            throw new TriggerClaimLostException(
                    trigger.getId());
        }
        throw new IllegalStateException(
                "Trigger claim mutation returned no result: "
                        + trigger.getId());
    }

    /**
     * 清理指定触发器 ID 的所有进程内认领凭证。
     *
     * @param triggerId 触发器 ID
     */
    private List<String> removeLocalClaims(String triggerId) {
        List<String> guardKeys = new ArrayList<>();
        synchronized (claimContexts) {
            claimContexts.entrySet().removeIf(entry -> {
                if (!triggerId.equals(entry.getKey().getId())) {
                    return false;
                }
                guardKeys.add(entry.getValue().guardKey);
                return true;
            });
        }
        return guardKeys;
    }

    /**
     * 按触发时间范围批量加载触发器。
     *
     * @param minScore 最小触发时间
     * @param maxScore 最大触发时间
     * @param limit 最大返回数
     * @return 保持触发时间顺序的触发器列表
     */
    private List<Trigger> findByScore(long minScore, long maxScore, int limit) {
        java.util.Set<String> triggerIds = redisTemplate.opsForZSet().rangeByScore(
                CacheKey.TRIGGER_PENDING_KEY, minScore, maxScore, 0, limit);
        if (triggerIds == null || triggerIds.isEmpty()) {
            return Collections.emptyList();
        }
        List<String> ids = new ArrayList<>(triggerIds);
        List<String> keys = new ArrayList<>(ids.size());
        for (String triggerId : ids) {
            keys.add(dataKey(triggerId));
        }
        List<String> payloads = redisTemplate.opsForValue().multiGet(keys);
        List<Trigger> triggers = new ArrayList<>(ids.size());
        if (payloads == null) {
            return triggers;
        }
        for (int index = 0; index < payloads.size(); index++) {
            String payload = payloads.get(index);
            Trigger trigger;
            try {
                trigger = deserialize(payload);
            } catch (IllegalStateException error) {
                quarantine(ids.get(index), payload, error);
                continue;
            }
            if (trigger != null) {
                triggers.add(trigger);
            } else {
                redisTemplate.opsForZSet().remove(CacheKey.TRIGGER_PENDING_KEY, ids.get(index));
            }
        }
        return triggers;
    }

    /**
     * 隔离无法反序列化的触发器，避免毒数据持续阻断批量扫描。
     *
     * @param triggerId 触发器 ID
     * @param payload 原始负载
     * @param error 解析异常
     */
    private void quarantine(String triggerId, String payload, RuntimeException error) {
        if (payload != null) {
            redisTemplate.opsForValue().set(
                    CacheKey.TRIGGER_DEAD_LETTER_KEY + triggerId,
                    payload,
                    TRIGGER_TTL);
        }
        remove(triggerId);
        log.error("Quarantined invalid workflow trigger payload, triggerId={}", triggerId, error);
    }

    /**
     * 序列化触发器。
     *
     * @param trigger 触发器
     * @return JSON 文本
     */
    private String serialize(Trigger trigger) {
        try {
            return objectMapper.writeValueAsString(trigger);
        } catch (JsonProcessingException error) {
            throw new IllegalStateException("Failed to serialize workflow trigger: " + trigger.getId(), error);
        }
    }

    /**
     * 反序列化触发器。
     *
     * @param payload JSON 文本
     * @return 触发器；输入为空时返回 null
     */
    private Trigger deserialize(String payload) {
        if (payload == null) {
            return null;
        }
        try {
            return objectMapper.readValue(payload, Trigger.class);
        } catch (JsonProcessingException error) {
            throw new IllegalStateException("Failed to deserialize workflow trigger", error);
        }
    }

    /**
     * 构建触发器数据键。
     *
     * @param triggerId 触发器 ID
     * @return Redis 键
     */
    private String dataKey(String triggerId) {
        return CacheKey.TRIGGER_DATA_KEY + triggerId;
    }

    /**
     * 构建触发器认领键。
     *
     * @param triggerId 触发器 ID
     * @return Redis 键
     */
    private String claimKey(String triggerId) {
        return CacheKey.TRIGGER_CLAIM_KEY + triggerId;
    }

    /**
     * 构建触发器认领代际分配计数器键。
     *
     * @param instanceId 工作流实例 ID
     * @return 认领代际计数器键
     */
    private String claimGenerationKey(String instanceId) {
        return CacheKey.CHAIN_STATE_CAS_KEY + "{" + instanceId + "}:claim-seq";
    }

    /**
     * 构建实例锁 fencing token 键。
     *
     * @param instanceId 工作流实例 ID
     * @return fencing token 键
     */
    private String lockFenceKey(String instanceId) {
        return CacheKey.CHAIN_STATE_CAS_KEY
                + "{"
                + requireText(instanceId, "state instance ID")
                + "}:fence";
    }

    /**
     * 构建与一次触发器认领绑定的执行守卫键。
     *
     * @param instanceId 工作流实例 ID
     * @param claimId 触发器 ID
     * @return 执行守卫键
     */
    private String executionGuardKey(String instanceId, String claimId) {
        return CacheKey.CHAIN_STATE_CAS_KEY
                + "{"
                + requireText(instanceId, "state instance ID")
                + "}:claim:"
                + requireText(claimId, "claim ID");
    }

    /**
     * 校验必填文本。
     *
     * @param value 原始值
     * @param name 字段名称
     * @return 去除首尾空白后的值
     */
    private String requireText(String value, String name) {
        if (value == null || value.trim().isEmpty()) {
            throw new IllegalArgumentException(name + " must not be blank");
        }
        return value.trim();
    }

    /**
     * 当前进程持有的一次触发器认领上下文。
     */
    private static final class ClaimContext {

        private final String ownerToken;
        private final String guardKey;
        private final long claimGeneration;

        /**
         * 创建认领上下文。
         *
         * @param ownerToken Redis claim owner token
         * @param guardKey 执行守卫键
         * @param claimGeneration 本次认领的单调代际
         */
        private ClaimContext(
                String ownerToken, String guardKey, long claimGeneration) {
            this.ownerToken = ownerToken;
            this.guardKey = guardKey;
            this.claimGeneration = claimGeneration;
        }
    }

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

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