package tech.easyflow.ai.easyagentsflow.upload;

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.springframework.util.StringUtils;

import java.time.Duration;
import java.util.Arrays;
import java.util.List;
import java.util.Optional;

/**
 * Public Workflow API 临时上传记录的 Redis 存储。
 */
@Component
public class WorkflowApiUploadStore {

    private static final String RECORD_KEY_PREFIX =
            "easyflow:workflow:{api-upload}:record:";
    private static final String EXECUTION_KEY_PREFIX =
            "easyflow:workflow:{api-upload}:execution:";
    private static final String CLEANUP_INDEX =
            "easyflow:workflow:{api-upload}:cleanup";
    private static final Duration EXECUTION_INDEX_TTL =
            Duration.ofDays(30);
    private static final DefaultRedisScript<Long> SAVE_SCHEDULED_SCRIPT =
            longScript(
                    "redis.call('set', KEYS[1], ARGV[1]); "
                            + "redis.call('zadd', KEYS[2], ARGV[2], ARGV[3]); "
                            + "if ARGV[4] == '1' "
                            + "and redis.call('get', KEYS[3]) == ARGV[3] then "
                            + "redis.call('pexpire', KEYS[3], ARGV[5]); end; "
                            + "return 1");
    private static final DefaultRedisScript<Long> BIND_EXECUTION_SCRIPT =
            longScript(
                    "redis.call('set', KEYS[1], ARGV[1]); "
                            + "redis.call('psetex', KEYS[2], ARGV[3], ARGV[2]); "
                            + "if ARGV[4] == '1' and KEYS[3] ~= KEYS[2] "
                            + "and redis.call('get', KEYS[3]) == ARGV[2] then "
                            + "redis.call('del', KEYS[3]); end; "
                            + "return 1");
    private static final DefaultRedisScript<Long>
            REMOVE_EXECUTION_INDEX_SCRIPT =
            longScript(
                    "if redis.call('get', KEYS[1]) == ARGV[1] then "
                            + "return redis.call('del', KEYS[1]); end; "
                            + "return 0");
    private static final DefaultRedisScript<String> CLAIM_EXPIRED_SCRIPT =
            stringScript(
                    "local values = redis.call("
                            + "'zrangebyscore', KEYS[1], '-inf', ARGV[1], "
                            + "'LIMIT', 0, 1); "
                            + "if #values == 0 then return nil; end; "
                            + "redis.call('zadd', KEYS[1], ARGV[2], values[1]); "
                            + "return values[1]");
    private static final DefaultRedisScript<Long> REMOVE_SCRIPT =
            longScript(
                    "redis.call('del', KEYS[1]); "
                            + "if ARGV[2] == '1' "
                            + "and redis.call('get', KEYS[2]) == ARGV[1] then "
                            + "redis.call('del', KEYS[2]); end; "
                            + "redis.call('zrem', KEYS[3], ARGV[1]); "
                            + "return 1");

    private final StringRedisTemplate redisTemplate;
    private final ObjectMapper objectMapper;

    /**
     * 创建临时上传记录存储。
     *
     * @param redisTemplate Redis 模板
     * @param objectMapper JSON 映射器
     */
    public WorkflowApiUploadStore(StringRedisTemplate redisTemplate,
                                  ObjectMapper objectMapper) {
        this.redisTemplate = redisTemplate;
        this.objectMapper = objectMapper;
    }

    /**
     * 新建临时上传记录并登记清理时间。
     *
     * @param record 上传记录
     */
    public void create(WorkflowApiUploadRecord record) {
        requireRecord(record);
        saveScheduled(record);
    }

    /**
     * 保存上传记录的最新内容。
     *
     * @param record 上传记录
     */
    public void save(WorkflowApiUploadRecord record) {
        requireRecord(record);
        redisTemplate.opsForValue().set(
                recordKey(record.getUploadId()),
                serialize(record));
    }

    /**
     * 将上传请求绑定到工作流执行实例。
     *
     * @param uploadId 内部上传 ID
     * @param executeId 工作流执行 ID
     */
    public void bindExecution(String uploadId, String executeId) {
        if (!StringUtils.hasText(executeId)) {
            throw new IllegalArgumentException("工作流执行 ID 不能为空");
        }
        WorkflowApiUploadRecord record = find(uploadId)
                .orElseThrow(() -> new IllegalStateException(
                        "工作流临时上传记录不存在: " + uploadId));
        String previousExecuteId = record.getExecuteId();
        record.setExecuteId(executeId);
        redisTemplate.execute(
                BIND_EXECUTION_SCRIPT,
                Arrays.asList(
                        recordKey(uploadId),
                        executionKey(executeId),
                        StringUtils.hasText(previousExecuteId)
                                ? executionKey(previousExecuteId)
                                : recordKey(uploadId)),
                serialize(record),
                uploadId,
                String.valueOf(EXECUTION_INDEX_TTL.toMillis()),
                StringUtils.hasText(previousExecuteId) ? "1" : "0");
    }

    /**
     * 按内部上传 ID 查找记录。
     *
     * @param uploadId 内部上传 ID
     * @return 上传记录
     */
    public Optional<WorkflowApiUploadRecord> find(String uploadId) {
        if (!StringUtils.hasText(uploadId)) {
            return Optional.empty();
        }
        String value = redisTemplate.opsForValue().get(recordKey(uploadId));
        if (!StringUtils.hasText(value)) {
            return Optional.empty();
        }
        try {
            return Optional.of(objectMapper.readValue(
                    value,
                    WorkflowApiUploadRecord.class));
        } catch (JsonProcessingException error) {
            throw new IllegalStateException(
                    "读取工作流临时上传记录失败: " + uploadId,
                    error);
        }
    }

    /**
     * 按工作流执行 ID 查找上传记录。
     *
     * @param executeId 工作流执行 ID
     * @return 上传记录
     */
    public Optional<WorkflowApiUploadRecord> findByExecutionId(
            String executeId) {
        if (!StringUtils.hasText(executeId)) {
            return Optional.empty();
        }
        String uploadId = redisTemplate.opsForValue().get(
                executionKey(executeId));
        Optional<WorkflowApiUploadRecord> record = find(uploadId);
        if (StringUtils.hasText(uploadId)
                && (record.isEmpty()
                || !executeId.equals(record.get().getExecuteId()))) {
            redisTemplate.execute(
                    REMOVE_EXECUTION_INDEX_SCRIPT,
                    List.of(executionKey(executeId)),
                    uploadId);
            return Optional.empty();
        }
        return record;
    }

    /**
     * 更新记录的下次清理检查时间。
     *
     * @param record 上传记录
     * @param cleanupAt Unix 毫秒时间戳
     */
    public void schedule(WorkflowApiUploadRecord record, long cleanupAt) {
        requireRecord(record);
        record.setCleanupAt(cleanupAt);
        saveScheduled(record);
    }

    /**
     * 原子领取一条到期上传请求，并提前设置失败重试时间。
     *
     * @param now 当前 Unix 毫秒时间戳
     * @param retryAt 领取后默认重试时间
     * @return 领取到的内部上传 ID
     */
    public Optional<String> claimExpired(
            long now,
            long retryAt) {
        String uploadId = redisTemplate.execute(
                CLAIM_EXPIRED_SCRIPT,
                List.of(CLEANUP_INDEX),
                String.valueOf(now),
                String.valueOf(retryAt));
        return Optional.ofNullable(uploadId);
    }

    /**
     * 删除上传记录、执行索引和清理索引。
     *
     * @param record 上传记录
     */
    public void remove(WorkflowApiUploadRecord record) {
        requireRecord(record);
        boolean hasExecuteId =
                StringUtils.hasText(record.getExecuteId());
        redisTemplate.execute(
                REMOVE_SCRIPT,
                Arrays.asList(
                        recordKey(record.getUploadId()),
                        hasExecuteId
                                ? executionKey(record.getExecuteId())
                                : recordKey(record.getUploadId()),
                        CLEANUP_INDEX),
                record.getUploadId(),
                hasExecuteId ? "1" : "0");
    }

    /**
     * 删除已经缺少详情记录的残留清理索引。
     *
     * @param uploadId 内部上传 ID
     */
    public void removeMissingIndex(String uploadId) {
        if (StringUtils.hasText(uploadId)) {
            redisTemplate.opsForZSet().remove(CLEANUP_INDEX, uploadId);
        }
    }

    /**
     * 序列化上传记录。
     *
     * @param record 上传记录
     * @return JSON 文本
     */
    private String serialize(WorkflowApiUploadRecord record) {
        try {
            return objectMapper.writeValueAsString(record);
        } catch (JsonProcessingException error) {
            throw new IllegalStateException(
                    "写入工作流临时上传记录失败: "
                            + record.getUploadId(),
                    error);
        }
    }

    /**
     * 原子保存记录与清理索引，并续期执行索引。
     *
     * @param record 上传记录
     */
    private void saveScheduled(WorkflowApiUploadRecord record) {
        boolean hasExecuteId =
                StringUtils.hasText(record.getExecuteId());
        redisTemplate.execute(
                SAVE_SCHEDULED_SCRIPT,
                Arrays.asList(
                        recordKey(record.getUploadId()),
                        CLEANUP_INDEX,
                        hasExecuteId
                                ? executionKey(record.getExecuteId())
                                : recordKey(record.getUploadId())),
                serialize(record),
                String.valueOf(record.getCleanupAt()),
                record.getUploadId(),
                hasExecuteId ? "1" : "0",
                String.valueOf(EXECUTION_INDEX_TTL.toMillis()));
    }

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

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

    /**
     * 校验记录主键。
     *
     * @param record 上传记录
     */
    private void requireRecord(WorkflowApiUploadRecord record) {
        if (record == null || !StringUtils.hasText(record.getUploadId())) {
            throw new IllegalArgumentException("工作流内部上传 ID 不能为空");
        }
    }

    /**
     * 构建记录 Redis Key。
     *
     * @param uploadId 内部上传 ID
     * @return Redis Key
     */
    private String recordKey(String uploadId) {
        return RECORD_KEY_PREFIX + uploadId;
    }

    /**
     * 构建执行实例 Redis Key。
     *
     * @param executeId 工作流执行 ID
     * @return Redis Key
     */
    private String executionKey(String executeId) {
        return EXECUTION_KEY_PREFIX + executeId;
    }
}
