package tech.easyflow.chatlog.cache;

import com.fasterxml.jackson.core.type.TypeReference;
import com.fasterxml.jackson.databind.ObjectMapper;
import org.springframework.dao.DataAccessException;
import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.stereotype.Service;
import tech.easyflow.chatlog.config.ChatCacheProperties;
import tech.easyflow.chatlog.domain.command.ChatAppendMessageCommand;
import tech.easyflow.chatlog.domain.command.ChatRoundSelectCommand;
import tech.easyflow.chatlog.domain.command.ChatRoundUpsertCommand;
import tech.easyflow.chatlog.domain.command.ChatSessionUpsertCommand;
import tech.easyflow.chatlog.domain.dto.ChatMessageRecord;
import tech.easyflow.chatlog.domain.dto.ChatRoundRecord;
import tech.easyflow.chatlog.domain.dto.ChatSessionSummary;
import tech.easyflow.chatlog.support.ChatConstants;

import java.io.IOException;
import java.math.BigInteger;
import java.time.Duration;
import java.util.ArrayList;
import java.util.Collections;
import java.util.Date;
import java.util.LinkedHashMap;
import java.util.LinkedHashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;

@Service
public class ChatHotStateService {

    private static final TypeReference<List<ChatMessageRecord>> TAIL_TYPE = new TypeReference<>() {
    };

    private final StringRedisTemplate stringRedisTemplate;
    private final ObjectMapper objectMapper;
    private final ChatCacheProperties properties;

    public ChatHotStateService(StringRedisTemplate stringRedisTemplate,
                               ObjectMapper objectMapper,
                               ChatCacheProperties properties) {
        this.stringRedisTemplate = stringRedisTemplate;
        this.objectMapper = objectMapper;
        this.properties = properties;
    }

    public ChatSessionSummary touchSession(ChatSessionUpsertCommand command) {
        ChatSessionSummary summary = getSessionSummary(command.getSessionId());
        if (summary == null) {
            summary = new ChatSessionSummary();
            summary.setId(command.getSessionId());
            summary.setTenantId(command.getTenantId());
            summary.setDeptId(command.getDeptId());
            summary.setUserId(command.getUserId());
            summary.setCreated(command.getOperateAt());
            summary.setCreatedBy(command.getOperatorId());
            summary.setMessageCount(0);
            summary.setLastMessagePreview("");
        }
        summary.setUserAccount(command.getUserAccount());
        summary.setAssistantId(command.getAssistantId());
        summary.setAssistantCode(command.getAssistantCode());
        summary.setAssistantName(command.getAssistantName());
        if (command.getTitle() != null && !command.getTitle().isBlank()) {
            summary.setTitle(command.getTitle());
        }
        if (command.getExtJson() != null && !command.getExtJson().isBlank()) {
            summary.setExtJson(command.getExtJson());
        }
        summary.setAccessAt(defaultDate(command.getOperateAt()));
        summary.setModified(defaultDate(command.getOperateAt()));
        summary.setModifiedBy(command.getOperatorId());
        summary.setIsDeleted(0);
        cacheSessionSummaryStrict(summary);
        return summary;
    }

    public void renameSession(BigInteger sessionId, BigInteger userId, String title, BigInteger operatorId, Date operateAt) {
        ChatSessionSummary summary = getSessionSummary(sessionId);
        if (summary == null) {
            summary = new ChatSessionSummary();
            summary.setId(sessionId);
            summary.setUserId(userId);
            summary.setCreated(defaultDate(operateAt));
            summary.setCreatedBy(operatorId);
            summary.setMessageCount(0);
        }
        summary.setTitle(title);
        summary.setModified(defaultDate(operateAt));
        summary.setModifiedBy(operatorId);
        summary.setIsDeleted(0);
        cacheSessionSummaryStrict(summary);
    }

    public void deleteSession(BigInteger sessionId, BigInteger userId, BigInteger operatorId, Date operateAt) {
        ChatSessionSummary summary = getSessionSummary(sessionId);
        if (summary != null) {
            summary.setIsDeleted(1);
            summary.setModified(defaultDate(operateAt));
            summary.setModifiedBy(operatorId);
            writeValueStrict(keySessionSummary(sessionId), summary, properties.getSessionSummaryTtl());
            removeFromSessionIndexStrict(userId, sessionId);
        } else {
            removeFromSessionIndexStrict(userId, sessionId);
        }
        deleteStrict(keySessionTail(sessionId));
    }

    public void appendMessage(ChatAppendMessageCommand command) {
        ChatSessionSummary summary = getSessionSummary(command.getSessionId());
        if (summary == null) {
            summary = new ChatSessionSummary();
            summary.setId(command.getSessionId());
            summary.setUserId(command.getUserId());
            summary.setAssistantId(command.getAssistantId());
            summary.setCreated(defaultDate(command.getCreated()));
            summary.setCreatedBy(command.getCreatedBy());
            summary.setMessageCount(0);
        }
        summary.setLastSenderId(command.getSenderId());
        summary.setLastSenderName(command.getSenderName());
        summary.setLastMessagePreview(trimPreview(command.getContentText()));
        summary.setLastMessageAt(defaultDate(command.getCreated()));
        summary.setAccessAt(defaultDate(command.getCreated()));
        summary.setModified(defaultDate(command.getCreated()));
        summary.setModifiedBy(command.getCreatedBy());
        summary.setIsDeleted(0);
        summary.setMessageCount((summary.getMessageCount() == null ? 0 : summary.getMessageCount())
                + resolveVisibleMessageIncrement(command));
        cacheSessionSummaryStrict(summary);
        appendVisibleTail(toMessageRecord(command));
    }

    public ChatRoundRecord createOrTouchRound(ChatRoundUpsertCommand command) {
        if (command == null || command.getSessionId() == null || command.getRoundId() == null) {
            return null;
        }
        ChatRoundRecord record = getRound(command.getSessionId(), command.getRoundId());
        if (record == null) {
            record = new ChatRoundRecord();
            record.setId(command.getRoundId());
            record.setSessionId(command.getSessionId());
            record.setCreated(defaultDate(command.getOperateAt()));
        }
        if (command.getRoundNo() != null) {
            record.setRoundNo(command.getRoundNo());
        }
        if (command.getUserMessageId() != null) {
            record.setUserMessageId(command.getUserMessageId());
        }
        if (command.getSelectedAssistantMessageId() != null) {
            record.setSelectedAssistantMessageId(command.getSelectedAssistantMessageId());
        }
        if (command.getSelectedVariantIndex() != null) {
            record.setSelectedVariantIndex(command.getSelectedVariantIndex());
        }
        if (command.getVariantCount() != null) {
            record.setVariantCount(command.getVariantCount());
        }
        if (command.getStatus() != null && !command.getStatus().isBlank()) {
            record.setStatus(command.getStatus());
        }
        record.setModified(defaultDate(command.getOperateAt()));
        cacheRoundStrict(record);
        syncTailRoundMeta(record);
        return record;
    }

    public void selectVariant(ChatRoundSelectCommand command) {
        if (command == null || command.getSessionId() == null || command.getRoundId() == null) {
            return;
        }
        ChatRoundRecord record = getRound(command.getSessionId(), command.getRoundId());
        if (record == null) {
            return;
        }
        record.setSelectedAssistantMessageId(command.getSelectedAssistantMessageId());
        record.setSelectedVariantIndex(command.getSelectedVariantIndex());
        record.setModified(defaultDate(command.getOperateAt()));
        cacheRoundStrict(record);
        if (command.getSelectedAssistantMessage() != null) {
            applyRoundMeta(command.getSelectedAssistantMessage(), record);
            replaceSelectedAssistant(record, command.getSelectedAssistantMessage());
        }
    }

    public ChatRoundRecord getLatestRound(BigInteger sessionId) {
        return readValue(keyLatestRound(sessionId), ChatRoundRecord.class);
    }

    public ChatRoundRecord getRound(BigInteger sessionId, BigInteger roundId) {
        return readValue(keyRound(sessionId, roundId), ChatRoundRecord.class);
    }

    public void cacheRound(ChatRoundRecord record) {
        try {
            cacheRoundStrict(record);
        } catch (IllegalStateException ignored) {
        }
    }

    public List<BigInteger> listSessionIds(BigInteger userId, long offset, long limit) {
        if (userId == null || limit <= 0) {
            return Collections.emptyList();
        }
        try {
            Set<String> values = stringRedisTemplate.opsForZSet()
                    .reverseRange(keySessionIndex(userId), offset, offset + limit - 1);
            if (values == null || values.isEmpty()) {
                return Collections.emptyList();
            }
            List<BigInteger> sessionIds = new ArrayList<>(values.size());
            for (String value : values) {
                if (value != null && !value.isBlank()) {
                    sessionIds.add(new BigInteger(value));
                }
            }
            return sessionIds;
        } catch (DataAccessException ignored) {
            return Collections.emptyList();
        }
    }

    public long countSessions(BigInteger userId) {
        if (userId == null) {
            return 0L;
        }
        try {
            Long count = stringRedisTemplate.opsForZSet().zCard(keySessionIndex(userId));
            return count == null ? 0L : count;
        } catch (DataAccessException ignored) {
            return 0L;
        }
    }

    public boolean hasSessionIndex(BigInteger userId) {
        if (userId == null) {
            return false;
        }
        try {
            Boolean exists = stringRedisTemplate.hasKey(keySessionIndex(userId));
            return Boolean.TRUE.equals(exists);
        } catch (DataAccessException ignored) {
            return false;
        }
    }

    public ChatSessionSummary getSessionSummary(BigInteger sessionId) {
        return readValue(keySessionSummary(sessionId), ChatSessionSummary.class);
    }

    public List<ChatSessionSummary> getSessionSummaries(List<BigInteger> sessionIds) {
        if (sessionIds == null || sessionIds.isEmpty()) {
            return Collections.emptyList();
        }
        try {
            List<String> keys = sessionIds.stream().map(this::keySessionSummary).toList();
            List<String> values = stringRedisTemplate.opsForValue().multiGet(keys);
            if (values == null || values.isEmpty()) {
                return Collections.emptyList();
            }
            Map<BigInteger, ChatSessionSummary> summaryMap = new LinkedHashMap<>();
            for (String value : values) {
                if (value == null || value.isBlank()) {
                    continue;
                }
                ChatSessionSummary summary = objectMapper.readValue(value, ChatSessionSummary.class);
                if (summary != null && summary.getId() != null) {
                    summaryMap.put(summary.getId(), summary);
                }
            }
            List<ChatSessionSummary> results = new ArrayList<>(sessionIds.size());
            for (BigInteger sessionId : sessionIds) {
                ChatSessionSummary summary = summaryMap.get(sessionId);
                if (summary != null) {
                    results.add(summary);
                }
            }
            return results;
        } catch (IOException | DataAccessException ignored) {
            return Collections.emptyList();
        }
    }

    public void cacheSessionSummary(ChatSessionSummary summary) {
        try {
            cacheSessionSummaryStrict(summary);
        } catch (IllegalStateException ignored) {
        }
    }

    private void cacheSessionSummaryStrict(ChatSessionSummary summary) {
        if (summary == null || summary.getId() == null) {
            return;
        }
        writeValueStrict(keySessionSummary(summary.getId()), summary, properties.getSessionSummaryTtl());
        BigInteger userId = summary.getUserId();
        if (userId == null) {
            return;
        }
        if (Integer.valueOf(1).equals(summary.getIsDeleted())) {
            removeFromSessionIndexStrict(userId, summary.getId());
            return;
        }
        Date scoreTime = summary.getAccessAt() != null ? summary.getAccessAt() : summary.getModified();
        double score = scoreTime == null ? System.currentTimeMillis() : scoreTime.getTime();
        try {
            stringRedisTemplate.opsForZSet().add(keySessionIndex(userId), summary.getId().toString(), score);
        } catch (DataAccessException ex) {
            throw new IllegalStateException("写入聊天会话索引失败", ex);
        }
        expireStrict(keySessionIndex(userId), properties.getSessionListTtl());
    }

    public void cacheSessionSummaries(List<ChatSessionSummary> sessions) {
        if (sessions == null || sessions.isEmpty()) {
            return;
        }
        Set<BigInteger> dedup = new LinkedHashSet<>();
        for (ChatSessionSummary session : sessions) {
            if (session == null || session.getId() == null || !dedup.add(session.getId())) {
                continue;
            }
            cacheSessionSummary(session);
        }
    }

    public void evictSessionSummary(BigInteger sessionId) {
        delete(keySessionSummary(sessionId));
    }

    public List<ChatMessageRecord> getSessionTail(BigInteger sessionId) {
        return readList(keySessionTail(sessionId), TAIL_TYPE);
    }

    public void setSessionTail(BigInteger sessionId, List<ChatMessageRecord> records) {
        writeValue(keySessionTail(sessionId), trimTail(records), properties.getSessionTailTtl());
    }

    public void appendTail(ChatMessageRecord record) {
        appendVisibleTail(record);
    }

    public void appendVisibleTail(ChatMessageRecord record) {
        if (record == null || record.getSessionId() == null) {
            return;
        }
        ChatRoundRecord round = record.getRoundId() == null ? null : getRound(record.getSessionId(), record.getRoundId());
        applyRoundMeta(record, round);
        List<ChatMessageRecord> current = getSessionTail(record.getSessionId());
        List<ChatMessageRecord> updated = current == null ? new ArrayList<>() : new ArrayList<>(current);
        removeExistingVisibleRecord(updated, record);
        insertSorted(updated, record);
        if (ChatConstants.MESSAGE_KIND_ASSISTANT_VARIANT.equals(record.getMessageKind())
                && record.getVariantIndex() != null
                && record.getVariantIndex() > 1) {
            removeOlderSelectedAssistant(updated, record);
        }
        writeValueStrict(keySessionTail(record.getSessionId()), trimTail(updated), properties.getSessionTailTtl());
    }

    private void replaceSelectedAssistant(ChatRoundRecord round, ChatMessageRecord record) {
        List<ChatMessageRecord> current = getSessionTail(record.getSessionId());
        List<ChatMessageRecord> updated = current == null ? new ArrayList<>() : new ArrayList<>(current);
        syncRoundMeta(updated, round);
        removeExistingVisibleRecord(updated, record);
        removeOlderSelectedAssistant(updated, record);
        insertSorted(updated, record);
        writeValueStrict(keySessionTail(record.getSessionId()), trimTail(updated), properties.getSessionTailTtl());
    }

    /**
     * 将轮次选中态同步到 Redis tail，避免前端主线过滤读到过期版本号。
     *
     * @param round 轮次记录
     */
    private void syncTailRoundMeta(ChatRoundRecord round) {
        if (round == null || round.getSessionId() == null || round.getId() == null) {
            return;
        }
        List<ChatMessageRecord> current = getSessionTail(round.getSessionId());
        if (current == null || current.isEmpty()) {
            return;
        }
        List<ChatMessageRecord> updated = new ArrayList<>(current);
        syncRoundMeta(updated, round);
        if (round.getSelectedAssistantMessageId() != null) {
            updated.removeIf(item -> item != null
                    && item.getRoundId() != null
                    && item.getRoundId().equals(round.getId())
                    && "assistant".equalsIgnoreCase(item.getSenderRole())
                    && item.getId() != null
                    && !item.getId().equals(round.getSelectedAssistantMessageId()));
        }
        writeValueStrict(keySessionTail(round.getSessionId()), trimTail(updated), properties.getSessionTailTtl());
    }

    /**
     * 批量同步同一轮次的版本元信息。
     *
     * @param records tail 消息列表
     * @param round 轮次记录
     */
    private void syncRoundMeta(List<ChatMessageRecord> records, ChatRoundRecord round) {
        if (records == null || records.isEmpty() || round == null || round.getId() == null) {
            return;
        }
        for (ChatMessageRecord item : records) {
            if (item != null && round.getId().equals(item.getRoundId())) {
                applyRoundMeta(item, round);
            }
        }
    }

    /**
     * 将轮次元信息写入单条消息。
     *
     * @param record 消息记录
     * @param round 轮次记录
     */
    private void applyRoundMeta(ChatMessageRecord record, ChatRoundRecord round) {
        if (record == null || round == null) {
            return;
        }
        if (round.getRoundNo() != null) {
            record.setRoundNo(round.getRoundNo());
        }
        if (round.getVariantCount() != null) {
            record.setVariantCount(round.getVariantCount());
        }
        if (round.getSelectedVariantIndex() != null) {
            record.setSelectedVariantIndex(round.getSelectedVariantIndex());
        }
        if (round.getStatus() != null) {
            record.setSwitchable(!ChatConstants.ROUND_STATUS_LOCKED.equalsIgnoreCase(round.getStatus()));
        }
    }

    private void removeOlderSelectedAssistant(List<ChatMessageRecord> records, ChatMessageRecord record) {
        if (record.getRoundId() == null) {
            return;
        }
        records.removeIf(item -> item != null
                && item.getId() != null
                && !item.getId().equals(record.getId())
                && item.getRoundId() != null
                && item.getRoundId().equals(record.getRoundId())
                && "assistant".equalsIgnoreCase(item.getSenderRole()));
    }

    private void removeExistingVisibleRecord(List<ChatMessageRecord> records, ChatMessageRecord record) {
        if (records == null || record == null || record.getId() == null) {
            return;
        }
        records.removeIf(item -> item != null && record.getId().equals(item.getId()));
    }

    private void insertSorted(List<ChatMessageRecord> records, ChatMessageRecord record) {
        int insertIndex = 0;
        while (insertIndex < records.size()) {
            ChatMessageRecord current = records.get(insertIndex);
            if (shouldComeAfter(record, current)) {
                insertIndex++;
                continue;
            }
            break;
        }
        records.add(insertIndex, record);
    }

    private boolean shouldComeAfter(ChatMessageRecord candidate, ChatMessageRecord current) {
        if (candidate == null) {
            return true;
        }
        if (current == null) {
            return false;
        }
        Date candidateCreated = defaultDate(candidate.getCreated());
        Date currentCreated = defaultDate(current.getCreated());
        if (candidateCreated.before(currentCreated)) {
            return true;
        }
        if (candidateCreated.after(currentCreated)) {
            return false;
        }
        BigInteger candidateId = candidate.getId() == null ? BigInteger.ZERO : candidate.getId();
        BigInteger currentId = current.getId() == null ? BigInteger.ZERO : current.getId();
        return candidateId.compareTo(currentId) < 0;
    }

    private void cacheRoundStrict(ChatRoundRecord record) {
        if (record == null || record.getSessionId() == null || record.getId() == null) {
            return;
        }
        writeValueStrict(keyRound(record.getSessionId(), record.getId()), record, properties.getSessionTailTtl());
        ChatRoundRecord latest = getLatestRound(record.getSessionId());
        if (latest == null || latest.getRoundNo() == null
                || (record.getRoundNo() != null && record.getRoundNo() >= latest.getRoundNo())) {
            writeValueStrict(keyLatestRound(record.getSessionId()), record, properties.getSessionTailTtl());
        }
    }

    public void evictSessionTail(BigInteger sessionId) {
        delete(keySessionTail(sessionId));
    }

    public int tailSize() {
        return properties.getTailSize();
    }

    private ChatMessageRecord toMessageRecord(ChatAppendMessageCommand command) {
        ChatMessageRecord record = new ChatMessageRecord();
        record.setId(command.getMessageId());
        record.setSessionId(command.getSessionId());
        record.setUserId(command.getUserId());
        record.setAssistantId(command.getAssistantId());
        record.setSenderId(command.getSenderId());
        record.setSenderName(command.getSenderName());
        record.setSenderRole(command.getSenderRole());
        record.setRoundId(command.getRoundId());
        record.setRoundNo(command.getRoundNo());
        record.setMessageKind(command.getMessageKind());
        record.setVariantIndex(command.getVariantIndex());
        record.setContentType(command.getContentType());
        record.setContentText(command.getContentText());
        record.setContentPayload(command.getContentPayload());
        record.setCreated(defaultDate(command.getCreated()));
        record.setCreatedBy(command.getCreatedBy());
        record.setSyncVersion(record.getCreated().getTime());
        return record;
    }

    private int resolveVisibleMessageIncrement(ChatAppendMessageCommand command) {
        if (command == null) {
            return 0;
        }
        if (ChatConstants.MESSAGE_KIND_ASSISTANT_VARIANT.equals(command.getMessageKind())) {
            Integer variantIndex = command.getVariantIndex();
            return variantIndex != null && variantIndex > 1 ? 0 : 1;
        }
        return 1;
    }

    private List<ChatMessageRecord> trimTail(List<ChatMessageRecord> records) {
        if (records == null || records.isEmpty()) {
            return Collections.emptyList();
        }
        int maxSize = Math.max(properties.getTailSize(), 1);
        int size = Math.min(records.size(), maxSize);
        List<ChatMessageRecord> result = new ArrayList<>(size);
        for (int i = 0; i < size; i++) {
            result.add(records.get(i));
        }
        return result;
    }

    private String keySessionIndex(BigInteger userId) {
        return "chat:session:index:" + userId;
    }

    private String keySessionSummary(BigInteger sessionId) {
        return "chat:session:summary:" + sessionId;
    }

    private String keySessionTail(BigInteger sessionId) {
        return "chat:session:tail:" + sessionId;
    }

    private String keyLatestRound(BigInteger sessionId) {
        return "chat:session:round:latest:" + sessionId;
    }

    private String keyRound(BigInteger sessionId, BigInteger roundId) {
        return "chat:session:round:" + sessionId + ":" + roundId;
    }

    private void removeFromSessionIndex(BigInteger userId, BigInteger sessionId) {
        if (userId == null || sessionId == null) {
            return;
        }
        try {
            stringRedisTemplate.opsForZSet().remove(keySessionIndex(userId), sessionId.toString());
        } catch (DataAccessException ignored) {
        }
    }

    private void removeFromSessionIndexStrict(BigInteger userId, BigInteger sessionId) {
        if (userId == null || sessionId == null) {
            return;
        }
        try {
            stringRedisTemplate.opsForZSet().remove(keySessionIndex(userId), sessionId.toString());
        } catch (DataAccessException ex) {
            throw new IllegalStateException("移除聊天会话索引失败", ex);
        }
    }

    private void expire(String key, Duration ttl) {
        try {
            stringRedisTemplate.expire(key, ttl);
        } catch (DataAccessException ignored) {
        }
    }

    private void expireStrict(String key, Duration ttl) {
        try {
            Boolean success = stringRedisTemplate.expire(key, ttl);
            if (Boolean.FALSE.equals(success)) {
                throw new IllegalStateException("设置聊天缓存过期时间失败");
            }
        } catch (DataAccessException ex) {
            throw new IllegalStateException("设置聊天缓存过期时间失败", ex);
        }
    }

    private void delete(String key) {
        try {
            stringRedisTemplate.delete(key);
        } catch (DataAccessException ignored) {
        }
    }

    private void deleteStrict(String key) {
        try {
            stringRedisTemplate.delete(key);
        } catch (DataAccessException ex) {
            throw new IllegalStateException("删除聊天缓存失败", ex);
        }
    }

    private void writeValue(String key, Object value, Duration ttl) {
        try {
            stringRedisTemplate.opsForValue().set(key, objectMapper.writeValueAsString(value), ttl);
        } catch (IOException | DataAccessException ignored) {
        }
    }

    private void writeValueStrict(String key, Object value, Duration ttl) {
        try {
            stringRedisTemplate.opsForValue().set(key, objectMapper.writeValueAsString(value), ttl);
        } catch (IOException | DataAccessException ex) {
            throw new IllegalStateException("写入聊天缓存失败", ex);
        }
    }

    private <T> T readValue(String key, Class<T> clazz) {
        try {
            String value = stringRedisTemplate.opsForValue().get(key);
            if (value == null || value.isBlank()) {
                return null;
            }
            return objectMapper.readValue(value, clazz);
        } catch (IOException | DataAccessException ignored) {
            return null;
        }
    }

    private <T> T readList(String key, TypeReference<T> typeReference) {
        try {
            String value = stringRedisTemplate.opsForValue().get(key);
            if (value == null || value.isBlank()) {
                return null;
            }
            return objectMapper.readValue(value, typeReference);
        } catch (IOException | DataAccessException ignored) {
            return null;
        }
    }

    private String trimPreview(String text) {
        if (text == null) {
            return "";
        }
        return text.length() <= 200 ? text : text.substring(0, 200);
    }

    private Date defaultDate(Date value) {
        return value == null ? new Date() : value;
    }
}
