package com.zhentao.redis;

import com.fasterxml.jackson.databind.ObjectMapper;
import com.zhentao.entity.MatchData;
import lombok.extern.slf4j.Slf4j;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.stereotype.Component;

import java.util.*;
import java.util.concurrent.TimeUnit;
import java.util.stream.Collectors;
import java.util.concurrent.ConcurrentHashMap;

@Slf4j
@Component
public class RedisMatchPool {
    private static final String MATCH_POOL_KEY = "match:pool";
    private static final int MATCH_REQUEST_EXPIRE_MINUTES = 30;
    private static final String UNMATCH_KEY_PREFIX = "match:unmatch:";
    private static final int UNMATCH_EXPIRE_HOURS = 6;

    private final RedisTemplate<String, Object> redisTemplate;
    private final ObjectMapper objectMapper;

    // Local fallback (when Redis is write-protected/MISCONF)
    private final Map<String, MatchData> localUserData = new ConcurrentHashMap<>();
    private final Set<String> localPool = ConcurrentHashMap.newKeySet();
    private final Map<String, Map<String, Long>> localUnmatch = new ConcurrentHashMap<>();

    public RedisMatchPool(RedisTemplate<String, Object> redisTemplate, ObjectMapper objectMapper) {
        this.redisTemplate = redisTemplate;
        this.objectMapper = objectMapper;
    }

    public boolean addToPool(MatchData matchData) {
        try {
            if (isUserInPool(matchData.getUserId())) return true; // 幂等：已在池中视为成功
            String key = userKey(matchData.getUserId());
            redisTemplate.opsForValue().set(key, matchData);
            redisTemplate.expire(key, MATCH_REQUEST_EXPIRE_MINUTES, TimeUnit.MINUTES);
            redisTemplate.opsForSet().add(MATCH_POOL_KEY, matchData.getUserId());
            return true;
        } catch (Exception e) { 
            if (isMisconf(e)) {
                log.warn("addToPool MISCONF, fallback to local (writes disabled)");
            } else {
                log.error("addToPool error, fallback to local", e);
            }
            localUserData.put(matchData.getUserId(), matchData);
            localPool.add(matchData.getUserId());
            return true;
        }
    }

    public boolean removeFromPool(String userId) {
        try {
            redisTemplate.delete(userKey(userId));
            redisTemplate.opsForSet().remove(MATCH_POOL_KEY, userId);
            // also clean local
            localUserData.remove(userId);
            localPool.remove(userId);
            return true;
        } catch (Exception e) { 
            if (isMisconf(e)) {
                log.warn("removeFromPool MISCONF, fallback to local (writes disabled)");
            } else {
                log.error("removeFromPool error, fallback to local", e);
            }
            localUserData.remove(userId);
            localPool.remove(userId);
            return true;
        }
    }

    public List<String> getAllUsersInPool() {
        try {
            Set<Object> ids = redisTemplate.opsForSet().members(MATCH_POOL_KEY);
            if (ids == null) return Collections.emptyList();
            return ids.stream().map(Object::toString).collect(Collectors.toList());
        } catch (Exception e) { 
            if (isMisconf(e)) {
                log.warn("getAllUsersInPool MISCONF, use local pool");
            } else {
                log.error("getAllUsersInPool error, use local pool", e);
            }
            return new ArrayList<>(localPool);
        }
    }

    public MatchData getUserMatchData(String userId) {
        try {
            Object v = redisTemplate.opsForValue().get(userKey(userId));
            if (v == null) return null;
            if (v instanceof MatchData) return (MatchData) v;
            if (v instanceof String) return objectMapper.readValue((String) v, MatchData.class);
            return objectMapper.convertValue(v, MatchData.class);
        } catch (Exception e) { 
            if (isMisconf(e)) {
                log.warn("getUserMatchData MISCONF, use local");
            } else {
                log.error("getUserMatchData error, use local", e);
            }
            return localUserData.get(userId);
        }
    }

    public boolean isUserInPool(String userId) {
        try { return Boolean.TRUE.equals(redisTemplate.opsForSet().isMember(MATCH_POOL_KEY, userId)); }
        catch (Exception e) { 
            if (isMisconf(e)) {
                log.warn("isUserInPool MISCONF, use local");
            } else {
                log.error("isUserInPool error, use local", e);
            }
            return localPool.contains(userId); 
        }
    }

    public boolean recordUnmatch(String u1, String u2) {
        try {
            long expireAt = System.currentTimeMillis() + UNMATCH_EXPIRE_HOURS * 3600_000L;
            String k1 = UNMATCH_KEY_PREFIX + u1;
            String k2 = UNMATCH_KEY_PREFIX + u2;
            redisTemplate.opsForHash().put(k1, u2, String.valueOf(expireAt));
            redisTemplate.expire(k1, UNMATCH_EXPIRE_HOURS, TimeUnit.HOURS);
            redisTemplate.opsForHash().put(k2, u1, String.valueOf(expireAt));
            redisTemplate.expire(k2, UNMATCH_EXPIRE_HOURS, TimeUnit.HOURS);
            return true;
        } catch (Exception e) { 
            if (isMisconf(e)) {
                log.warn("recordUnmatch MISCONF, use local");
            } else {
                log.error("recordUnmatch error, use local", e);
            }
            long expireAt = System.currentTimeMillis() + UNMATCH_EXPIRE_HOURS * 3600_000L;
            localUnmatch.computeIfAbsent(u1, k-> new ConcurrentHashMap<>()).put(u2, expireAt);
            localUnmatch.computeIfAbsent(u2, k-> new ConcurrentHashMap<>()).put(u1, expireAt);
            return true;
        }
    }

    public Set<String> getUserUnmatchedSet(String userId) {
        try {
            Map<Object,Object> map = redisTemplate.opsForHash().entries(UNMATCH_KEY_PREFIX + userId);
            if (map == null || map.isEmpty()) return Collections.emptySet();
            long now = System.currentTimeMillis();
            Set<String> rs = new HashSet<>();
            for (Map.Entry<Object,Object> e : map.entrySet()) {
                long expireAt = Long.parseLong(String.valueOf(e.getValue()));
                if (now < expireAt) rs.add(String.valueOf(e.getKey()));
            }
            return rs;
        } catch (Exception e) { 
            if (isMisconf(e)) {
                log.warn("getUserUnmatchedSet MISCONF, use local");
            } else {
                log.error("getUserUnmatchedSet error, use local", e);
            }
            Map<String, Long> m = localUnmatch.getOrDefault(userId, Collections.emptyMap());
            long now = System.currentTimeMillis();
            Set<String> rs = new HashSet<>();
            for (Map.Entry<String, Long> en : m.entrySet()) {
                if (now < en.getValue()) rs.add(en.getKey());
            }
            return rs;
        }
    }

    private String userKey(String userId) { return "match:user:" + userId; }

    private boolean isMisconf(Exception e) {
        String msg = e == null ? null : e.getMessage();
        return msg != null && msg.toLowerCase().contains("misconf");
    }
}

