package com.nanri.aiimage.modules.brand.service;

import com.fasterxml.jackson.core.type.TypeReference;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.nanri.aiimage.common.exception.BusinessException;
import com.nanri.aiimage.config.BrandProgressProperties;
import com.nanri.aiimage.modules.brand.model.dto.BrandCrawlResultFileDto;
import lombok.RequiredArgsConstructor;
import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.stereotype.Service;

import java.time.Duration;
import java.time.Instant;
import java.util.ArrayList;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;

@Service
@RequiredArgsConstructor
public class BrandTaskProgressCacheService {

    public static final String PHASE_CRAWLING = "crawling";
    public static final String PHASE_ASSEMBLING = "assembling";
    public static final String PHASE_UPLOADING = "uploading";
    public static final String PHASE_FAILED = "failed";

    private final StringRedisTemplate stringRedisTemplate;
    private final BrandProgressProperties brandProgressProperties;
    private final ObjectMapper objectMapper;

    public void saveProgressFromResult(Long taskId,
                                       String fileUrl,
                                       int fileIndex,
                                       int fileTotal,
                                       String fileName,
                                       int currentLine,
                                       int totalLines,
                                       int finishedFiles) {
        String key = buildKey(taskId);
        String now = String.valueOf(Instant.now().toEpochMilli());
        Map<String, String> values = new LinkedHashMap<>();
        values.put("phase", PHASE_CRAWLING);
        values.put("file_index", String.valueOf(Math.max(fileIndex, 0)));
        values.put("file_total", String.valueOf(Math.max(fileTotal, 0)));
        values.put("file_name", blankToEmpty(fileName));
        values.put("file_url", blankToEmpty(fileUrl));
        values.put("current_line", String.valueOf(Math.max(Math.min(currentLine, totalLines), 0)));
        values.put("total_lines", String.valueOf(Math.max(totalLines, 0)));
        values.put("finished_files", String.valueOf(Math.max(finishedFiles, 0)));
        values.put("updated_at", now);
        values.put("last_heartbeat_at", now);
        stringRedisTemplate.opsForHash().putAll(key, values);
        stringRedisTemplate.expire(key, Duration.ofHours(brandProgressProperties.getTtlHours()));
    }

    public void updatePhase(Long taskId, String phase, int finishedFiles, int fileTotal) {
        String key = buildKey(taskId);
        String now = String.valueOf(Instant.now().toEpochMilli());
        Map<String, String> values = new LinkedHashMap<>();
        values.put("phase", normalizePhase(phase));
        values.put("finished_files", String.valueOf(Math.max(finishedFiles, 0)));
        values.put("file_total", String.valueOf(Math.max(fileTotal, 0)));
        values.put("updated_at", now);
        values.put("last_heartbeat_at", now);
        stringRedisTemplate.opsForHash().putAll(key, values);
        stringRedisTemplate.expire(key, Duration.ofHours(brandProgressProperties.getTtlHours()));
    }

    public void markFailed(Long taskId, String message) {
        String key = buildKey(taskId);
        String now = String.valueOf(Instant.now().toEpochMilli());
        Map<String, String> values = new LinkedHashMap<>();
        values.put("phase", PHASE_FAILED);
        values.put("updated_at", now);
        values.put("error_message", blankToEmpty(message));
        stringRedisTemplate.opsForHash().putAll(key, values);
        stringRedisTemplate.expire(key, Duration.ofHours(brandProgressProperties.getFailedTtlHours()));
    }

    public Map<Object, Object> getProgress(Long taskId) {
        return stringRedisTemplate.opsForHash().entries(buildKey(taskId));
    }

    public void saveParsedPayload(Long taskId, Object payload) {
        try {
            stringRedisTemplate.opsForValue().set(buildPayloadKey(taskId), objectMapper.writeValueAsString(payload), Duration.ofHours(brandProgressProperties.getTtlHours()));
        } catch (Exception ex) {
            throw new BusinessException("暂存品牌原始数据失败");
        }
    }

    public <T> T getParsedPayload(Long taskId, TypeReference<T> typeReference) {
        String raw = stringRedisTemplate.opsForValue().get(buildPayloadKey(taskId));
        if (raw == null || raw.isBlank()) {
            return null;
        }
        try {
            return objectMapper.readValue(raw, typeReference);
        } catch (Exception ex) {
            throw new BusinessException("读取品牌原始数据失败");
        }
    }

    public void delete(Long taskId) {
        stringRedisTemplate.delete(buildKey(taskId));
        stringRedisTemplate.delete(buildResultKey(taskId));
        stringRedisTemplate.delete(buildPayloadKey(taskId));
    }

    public void mergeResultChunks(Long taskId, List<BrandCrawlResultFileDto> incomingFiles) {
        String resultKey = buildResultKey(taskId);
        for (BrandCrawlResultFileDto file : incomingFiles) {
            if (file.getChunkIndex() == null || file.getChunkTotal() == null || file.getChunkIndex() <= 0 || file.getChunkTotal() <= 0 || file.getChunkIndex() > file.getChunkTotal()) {
                throw new BusinessException("分片参数不合法");
            }
            String field = buildChunkField(file.getFileUrl(), file.getChunkIndex());
            try {
                stringRedisTemplate.opsForHash().put(resultKey, field, objectMapper.writeValueAsString(file));
            } catch (Exception ex) {
                throw new BusinessException("暂存结果分片失败");
            }
        }
        stringRedisTemplate.expire(resultKey, Duration.ofHours(brandProgressProperties.getTtlHours()));
    }

    public Map<String, List<BrandCrawlResultFileDto>> groupResultChunksByFile(Long taskId) {
        Map<Object, Object> stored = stringRedisTemplate.opsForHash().entries(buildResultKey(taskId));
        Map<String, List<BrandCrawlResultFileDto>> grouped = new LinkedHashMap<>();
        for (Object value : stored.values()) {
            if (!(value instanceof String raw) || raw.isBlank()) {
                continue;
            }
            try {
                BrandCrawlResultFileDto file = objectMapper.readValue(raw, BrandCrawlResultFileDto.class);
                grouped.computeIfAbsent(file.getFileUrl(), ignored -> new ArrayList<>()).add(file);
            } catch (Exception ignored) {
            }
        }
        grouped.values().forEach(list -> list.sort(java.util.Comparator.comparing(BrandCrawlResultFileDto::getChunkIndex)));
        return grouped;
    }

    public void clearResultChunks(Long taskId) {
        stringRedisTemplate.delete(buildResultKey(taskId));
    }

    public String buildKey(Long taskId) {
        return "brand:task:progress:" + taskId;
    }

    private String buildResultKey(Long taskId) {
        return "brand:task:result-chunks:" + taskId;
    }

    private String buildChunkField(String fileUrl, Integer chunkIndex) {
        return fileUrl + "#" + chunkIndex;
    }

    private String buildPayloadKey(Long taskId) {
        return "brand:task:parsed-payload:" + taskId;
    }

    public long getHeartbeatTimeoutMinutes() {
        return brandProgressProperties.getHeartbeatTimeoutMinutes();
    }

    private String normalizePhase(String phase) {
        if (phase == null || phase.isBlank()) {
            return PHASE_CRAWLING;
        }
        String value = phase.trim().toLowerCase();
        return switch (value) {
            case PHASE_ASSEMBLING, PHASE_UPLOADING, PHASE_FAILED -> value;
            default -> PHASE_CRAWLING;
        };
    }

    private String blankToEmpty(String value) {
        return value == null ? "" : value.trim();
    }
}
