package tech.easyflow.agent.runtime.media;

import com.fasterxml.jackson.databind.ObjectMapper;
import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.stereotype.Service;
import org.springframework.util.StringUtils;
import tech.easyflow.agent.config.AgentMediaProperties;

import java.time.Duration;
import java.time.Instant;
import java.util.ArrayList;
import java.util.List;
import java.util.Optional;
import java.util.Set;
import java.util.concurrent.TimeUnit;

/**
 * Agent 临时图片凭据与过期对象索引的 Redis 存储。
 */
@Service
public class RedisAgentMediaUploadStore {

    private static final String KEY_PREFIX = "easyflow:agent:image-upload:";
    private static final String EXPIRY_INDEX = "easyflow:agent:image-upload:expiry";

    private final StringRedisTemplate redisTemplate;
    private final ObjectMapper objectMapper;
    private final AgentMediaProperties properties;

    /**
     * 创建临时图片凭据存储。
     *
     * @param redisTemplate Redis 模板
     * @param objectMapper JSON 映射器
     * @param properties Agent 媒体配置
     */
    public RedisAgentMediaUploadStore(StringRedisTemplate redisTemplate,
                                      ObjectMapper objectMapper,
                                      AgentMediaProperties properties) {
        this.redisTemplate = redisTemplate;
        this.objectMapper = objectMapper;
        this.properties = properties;
    }

    /**
     * 创建临时图片凭据和清理索引。
     *
     * @param record 上传凭据
     */
    public void create(AgentMediaUploadRecord record) {
        Duration ttl = uploadTtl();
        Instant expiresAt = Instant.now().plus(ttl);
        record.setExpiresAt(expiresAt);
        write(record, ttl);
        redisTemplate.opsForZSet().add(EXPIRY_INDEX, cleanupMember(record), expiresAt.toEpochMilli());
    }

    /**
     * 查找上传凭据，读取操作不延长 TTL。
     *
     * @param uploadId 上传 ID
     * @return 上传凭据
     */
    public Optional<AgentMediaUploadRecord> find(String uploadId) {
        if (!StringUtils.hasText(uploadId)) {
            return Optional.empty();
        }
        String value = redisTemplate.opsForValue().get(key(uploadId));
        return StringUtils.hasText(value) ? Optional.of(read(value)) : Optional.empty();
    }

    /**
     * 续期仍被草稿或试运行会话使用的图片。
     *
     * @param record 上传凭据
     */
    public void renew(AgentMediaUploadRecord record) {
        Duration ttl = uploadTtl();
        String previousMember = cleanupMember(record);
        record.setExpiresAt(Instant.now().plus(ttl));
        write(record, ttl);
        redisTemplate.opsForZSet().remove(EXPIRY_INDEX, previousMember);
        redisTemplate.opsForZSet().add(EXPIRY_INDEX, cleanupMember(record), record.getExpiresAt().toEpochMilli());
    }

    /**
     * 记录临时上传已归档到正式图片，且不延长原有 TTL。
     *
     * @param record 上传凭据
     * @param formalReference 正式图片引用
     */
    public void markFormalBinding(AgentMediaUploadRecord record, String formalReference) {
        if (record == null || !StringUtils.hasText(formalReference)) {
            throw new IllegalArgumentException("正式图片绑定信息不能为空");
        }
        String previousMember = cleanupMember(record);
        Instant expiresAt = record.getExpiresAt();
        if (expiresAt == null) {
            expiresAt = Instant.now().plus(uploadTtl());
            record.setExpiresAt(expiresAt);
        }
        Duration remaining = Duration.between(Instant.now(), expiresAt);
        if (remaining.isZero() || remaining.isNegative()) {
            throw new IllegalStateException("图片上传凭据已过期");
        }
        record.setFormalReference(formalReference);
        write(record, remaining);
        redisTemplate.opsForZSet().remove(EXPIRY_INDEX, previousMember);
        redisTemplate.opsForZSet().add(EXPIRY_INDEX, cleanupMember(record),
                record.getExpiresAt().toEpochMilli());
    }

    /**
     * 删除已归档的临时凭据和清理索引。
     *
     * @param record 上传凭据
     */
    public void delete(AgentMediaUploadRecord record) {
        if (record == null) {
            return;
        }
        redisTemplate.delete(key(record.getUploadId()));
        redisTemplate.opsForZSet().remove(EXPIRY_INDEX, cleanupMember(record));
    }

    /**
     * 获取已到期的清理索引成员。
     *
     * @param limit 最大数量
     * @return 到期上传凭据
     */
    public List<AgentMediaUploadRecord> expired(int limit) {
        Set<String> members = redisTemplate.opsForZSet()
                .rangeByScore(EXPIRY_INDEX, 0, Instant.now().toEpochMilli(), 0, Math.max(1, limit));
        List<AgentMediaUploadRecord> records = new ArrayList<>();
        if (members == null) {
            return records;
        }
        for (String member : members) {
            try {
                records.add(objectMapper.readValue(member, AgentMediaUploadRecord.class));
            } catch (Exception error) {
                redisTemplate.opsForZSet().remove(EXPIRY_INDEX, member);
            }
        }
        return records;
    }

    /**
     * 删除一个清理索引成员。
     *
     * @param record 上传凭据
     */
    public void removeExpiryIndex(AgentMediaUploadRecord record) {
        redisTemplate.opsForZSet().remove(EXPIRY_INDEX, cleanupMember(record));
    }

    private void write(AgentMediaUploadRecord record, Duration ttl) {
        try {
            redisTemplate.opsForValue().set(key(record.getUploadId()), objectMapper.writeValueAsString(record),
                    Math.max(1L, ttl.toSeconds()), TimeUnit.SECONDS);
        } catch (Exception error) {
            throw new IllegalStateException("写入 Agent 图片上传凭据失败", error);
        }
    }

    private AgentMediaUploadRecord read(String value) {
        try {
            return objectMapper.readValue(value, AgentMediaUploadRecord.class);
        } catch (Exception error) {
            throw new IllegalStateException("读取 Agent 图片上传凭据失败", error);
        }
    }

    private String cleanupMember(AgentMediaUploadRecord record) {
        try {
            return objectMapper.writeValueAsString(record);
        } catch (Exception error) {
            throw new IllegalStateException("写入 Agent 图片清理索引失败", error);
        }
    }

    private String key(String uploadId) { return KEY_PREFIX + uploadId; }

    private Duration uploadTtl() {
        Duration ttl = properties.getUploadTtl();
        return ttl == null || ttl.isNegative() || ttl.isZero() ? Duration.ofHours(24) : ttl;
    }
}
