package com.zcloud.personnel.positioning.integration.finds;

import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;
import com.fasterxml.jackson.core.type.TypeReference;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.zcloud.personnel.positioning.persistence.dataobject.OpenapiConfigDO;
import com.zcloud.personnel.positioning.persistence.repository.OpenapiConfigRepository;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.ObjectProvider;
import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.stereotype.Component;

import java.time.Duration;
import java.util.Map;
import java.util.Set;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.ConcurrentMap;
import java.util.function.Supplier;

@Component
public class OpenapiConfigCache {
    private static final Logger LOGGER = LoggerFactory.getLogger(OpenapiConfigCache.class);
    private static final String REDIS_KEY_PREFIX = "personnel-positioning:finds:openapi-config:";
    private static final String DEFAULT_KEY = "default";
    private static final String NULL_VALUE = "__NULL__";
    private static final String BUILT_IN_HOST_URL = "http://192.168.193.23/";
    private static final String BUILT_IN_EXTERNAL_PREFIX = "/rest/";
    private static final String BUILT_IN_INTERNAL_PREFIX = "/rest/";
    private static final String BUILT_IN_SIK = "e7de99fd282e4e97f2aed8f0429edcee";
    private static final String BUILT_IN_SIS = "08a8458541e06b825093738fb572d0277b3919ff4bd8cd59a3ace96627e9bde8";
    private static final long LOCAL_TTL_MILLIS = Duration.ofMinutes(5).toMillis();
    private static final Duration REDIS_TTL = Duration.ofMinutes(30);
    private static final Duration REDIS_NULL_TTL = Duration.ofMinutes(2);

    private final OpenapiConfigRepository openapiConfigRepository;
    private final ObjectMapper objectMapper;
    private final StringRedisTemplate stringRedisTemplate;
    private final ConcurrentMap<String, CacheEntry> localCache = new ConcurrentHashMap<>();

    public OpenapiConfigCache(OpenapiConfigRepository openapiConfigRepository,
                              ObjectMapper objectMapper,
                              ObjectProvider<StringRedisTemplate> stringRedisTemplateProvider) {
        this.openapiConfigRepository = openapiConfigRepository;
        this.objectMapper = objectMapper;
        this.stringRedisTemplate = stringRedisTemplateProvider.getIfAvailable();
    }

    public OpenapiConfigDO getEnabledConfig(String companyId) {
        OpenapiConfigDO companyConfig = companyId == null ? null
                : getOrLoad(companyKey(companyId), () -> queryConfigByCorpinfo(companyId));
        if (companyConfig != null) {
            return companyConfig;
        }
        OpenapiConfigDO defaultConfig = getOrLoad(DEFAULT_KEY, this::queryDefaultConfig);
        return defaultConfig == null ? builtInConfig() : defaultConfig;
    }

    /**
     * Keeps FindS available before an environment-specific configuration is persisted.
     * Enabled database records always take precedence over this built-in fallback.
     */
    private OpenapiConfigDO builtInConfig() {
        OpenapiConfigDO config = new OpenapiConfigDO();
        config.setConfigCode("FINDS_BUILT_IN");
        config.setConfigName("FindS built-in configuration");
        config.setVendorName("FindS");
        config.setHostUrl(BUILT_IN_HOST_URL);
        config.setExternalPrefix(BUILT_IN_EXTERNAL_PREFIX);
        config.setInternalPrefix(BUILT_IN_INTERNAL_PREFIX);
        config.setSik(BUILT_IN_SIK);
        config.setSisCipherText(BUILT_IN_SIS);
        config.setEnabled(1);
        return config;
    }

    public void evictAll() {
        localCache.clear();
        if (stringRedisTemplate == null) {
            return;
        }
        try {
            Set<String> keys = stringRedisTemplate.keys(REDIS_KEY_PREFIX + "*");
            if (keys != null && !keys.isEmpty()) {
                stringRedisTemplate.delete(keys);
            }
        } catch (Exception e) {
            // Redis缓存清理允许失败，本地缓存此时已经清理完成。
            LOGGER.warn("Evict FindS OpenAPI config Redis cache failed", e);
        }
    }

    private OpenapiConfigDO getOrLoad(String key, Supplier<OpenapiConfigDO> loader) {
        long now = System.currentTimeMillis();
        CacheEntry local = localCache.get(key);
        if (local != null && local.expireAt > now) {
            return local.config;
        }
        OpenapiConfigDO redisConfig = readRedis(key);
        if (redisConfig != null) {
            putLocal(key, redisConfig);
            return redisConfig;
        }
        if (isRedisNull(key)) {
            putLocal(key, null);
            return null;
        }
        OpenapiConfigDO dbConfig = loader.get();
        putLocal(key, dbConfig);
        writeRedis(key, dbConfig);
        return dbConfig;
    }

    private OpenapiConfigDO queryConfigByCorpinfo(String companyId) {
        QueryWrapper<OpenapiConfigDO> queryWrapper = baseConfigQuery();
        queryWrapper.eq("corpinfo_id", companyId);
        queryWrapper.last("LIMIT 1");
        return openapiConfigRepository.getOne(queryWrapper, false);
    }

    private OpenapiConfigDO queryDefaultConfig() {
        QueryWrapper<OpenapiConfigDO> queryWrapper = baseConfigQuery();
        queryWrapper.and(wrapper -> wrapper.isNull("corpinfo_id").or().eq("corpinfo_id", 0));
        queryWrapper.last("LIMIT 1");
        return openapiConfigRepository.getOne(queryWrapper, false);
    }

    private QueryWrapper<OpenapiConfigDO> baseConfigQuery() {
        QueryWrapper<OpenapiConfigDO> queryWrapper = new QueryWrapper<>();
        queryWrapper.eq("enabled", 1);
        queryWrapper.eq("delete_enum", "FALSE");
        queryWrapper.orderByDesc("update_time");
        return queryWrapper;
    }

    private OpenapiConfigDO readRedis(String key) {
        if (stringRedisTemplate == null) {
            return null;
        }
        try {
            String value = stringRedisTemplate.opsForValue().get(redisKey(key));
            if (value == null || NULL_VALUE.equals(value)) {
                return null;
            }
            return fromJson(value);
        } catch (Exception e) {
            LOGGER.warn("Read FindS OpenAPI config Redis cache failed, key={}", key, e);
            return null;
        }
    }

    private boolean isRedisNull(String key) {
        if (stringRedisTemplate == null) {
            return false;
        }
        try {
            return NULL_VALUE.equals(stringRedisTemplate.opsForValue().get(redisKey(key)));
        } catch (Exception e) {
            LOGGER.warn("Read FindS OpenAPI config Redis null marker failed, key={}", key, e);
            return false;
        }
    }

    private void writeRedis(String key, OpenapiConfigDO config) {
        if (stringRedisTemplate == null) {
            return;
        }
        try {
            if (config == null) {
                stringRedisTemplate.opsForValue().set(redisKey(key), NULL_VALUE, REDIS_NULL_TTL);
                return;
            }
            stringRedisTemplate.opsForValue().set(redisKey(key), toJson(config), REDIS_TTL);
        } catch (Exception e) {
            // Redis仅用于性能缓存，数据库回退查询保证业务结果正确。
            LOGGER.warn("Write FindS OpenAPI config Redis cache failed, key={}", key, e);
        }
    }

    private void putLocal(String key, OpenapiConfigDO config) {
        localCache.put(key, new CacheEntry(config, System.currentTimeMillis() + LOCAL_TTL_MILLIS));
    }

    private String companyKey(String companyId) {
        return "corp:" + companyId;
    }

    private String redisKey(String key) {
        return REDIS_KEY_PREFIX + key;
    }

    private String toJson(OpenapiConfigDO config) throws Exception {
        return objectMapper.writeValueAsString(new CachedOpenapiConfig(config));
    }

    private OpenapiConfigDO fromJson(String value) throws Exception {
        Map<String, Object> map = objectMapper.readValue(value, new TypeReference<Map<String, Object>>() {
        });
        OpenapiConfigDO config = new OpenapiConfigDO();
        config.setConfigCode(text(map.get("configCode")));
        config.setConfigName(text(map.get("configName")));
        config.setVendorName(text(map.get("vendorName")));
        config.setHostUrl(text(map.get("hostUrl")));
        config.setExternalPrefix(text(map.get("externalPrefix")));
        config.setInternalPrefix(text(map.get("internalPrefix")));
        config.setSik(text(map.get("sik")));
        config.setSisCipherText(text(map.get("sisCipherText")));
        config.setEnabled(integer(map.get("enabled")));
        config.setCorpinfoId(text(map.get("corpinfoId")));
        config.setCorpinfoName(text(map.get("corpinfoName")));
        return config;
    }

    private String text(Object value) {
        return value == null ? null : String.valueOf(value);
    }

    private Integer integer(Object value) {
        if (value == null) {
            return null;
        }
        if (value instanceof Number) {
            return ((Number) value).intValue();
        }
        return Integer.valueOf(String.valueOf(value));
    }

    private Long longValue(Object value) {
        if (value == null) {
            return null;
        }
        if (value instanceof Number) {
            return ((Number) value).longValue();
        }
        return Long.valueOf(String.valueOf(value));
    }

    private static class CacheEntry {
        private final OpenapiConfigDO config;
        private final long expireAt;

        private CacheEntry(OpenapiConfigDO config, long expireAt) {
            this.config = config;
            this.expireAt = expireAt;
        }
    }

    private static class CachedOpenapiConfig {
        private final String configCode;
        private final String configName;
        private final String vendorName;
        private final String hostUrl;
        private final String externalPrefix;
        private final String internalPrefix;
        private final String sik;
        private final String sisCipherText;
        private final Integer enabled;
        private final String corpinfoId;
        private final String corpinfoName;

        private CachedOpenapiConfig(OpenapiConfigDO config) {
            this.configCode = config.getConfigCode();
            this.configName = config.getConfigName();
            this.vendorName = config.getVendorName();
            this.hostUrl = config.getHostUrl();
            this.externalPrefix = config.getExternalPrefix();
            this.internalPrefix = config.getInternalPrefix();
            this.sik = config.getSik();
            this.sisCipherText = config.getSisCipherText();
            this.enabled = config.getEnabled();
            this.corpinfoId = config.getCorpinfoId();
            this.corpinfoName = config.getCorpinfoName();
        }

        public String getConfigCode() {
            return configCode;
        }

        public String getConfigName() {
            return configName;
        }

        public String getVendorName() {
            return vendorName;
        }

        public String getHostUrl() {
            return hostUrl;
        }

        public String getExternalPrefix() {
            return externalPrefix;
        }

        public String getInternalPrefix() {
            return internalPrefix;
        }

        public String getSik() {
            return sik;
        }

        public String getSisCipherText() {
            return sisCipherText;
        }

        public Integer getEnabled() {
            return enabled;
        }

        public String getCorpinfoId() {
            return corpinfoId;
        }

        public String getCorpinfoName() {
            return corpinfoName;
        }
    }
}
