package qunar.tc.qconfig.server.config.impl;

import com.google.common.base.Objects;
import com.google.common.base.Optional;
import com.google.common.base.Supplier;
import com.google.common.cache.CacheBuilder;
import com.google.common.cache.CacheLoader;
import com.google.common.cache.LoadingCache;
import com.google.common.collect.ImmutableList;
import com.google.common.collect.Maps;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.stereotype.Service;
import qunar.tc.qconfig.common.metrics.Metrics;
import qunar.tc.qconfig.common.util.Environment;
import qunar.tc.qconfig.common.util.RefType;
import qunar.tc.qconfig.server.config.ConfigStore;
import qunar.tc.qconfig.server.config.cache.CacheConfigVersionService;
import qunar.tc.qconfig.server.config.cache.CacheFixedVersionConsumerService;
import qunar.tc.qconfig.server.config.cache.CachePushConfigVersionService;
import qunar.tc.qconfig.server.config.cache.CacheService;
import qunar.tc.qconfig.server.dao.ConfigDao;
import qunar.tc.qconfig.server.dao.FileConfigDao;
import qunar.tc.qconfig.server.domain.ReferenceInfo;
import qunar.tc.qconfig.server.domain.UpdateType;
import qunar.tc.qconfig.server.exception.ChecksumFailedException;
import qunar.tc.qconfig.server.exception.ConfigNotFoundException;
import qunar.tc.qconfig.server.exception.FileDaoProcessException;
import qunar.tc.qconfig.server.support.AddressUtil;
import qunar.tc.qconfig.server.support.context.ClientInfoService;
import qunar.tc.qconfig.server.support.monitor.Monitor;
import qunar.tc.qconfig.servercommon.bean.ChecksumData;
import qunar.tc.qconfig.servercommon.bean.ConfigMeta;
import qunar.tc.qconfig.servercommon.bean.RefChangeType;
import qunar.tc.qconfig.servercommon.bean.VersionData;

import javax.annotation.PostConstruct;
import javax.annotation.Resource;
import java.util.List;
import java.util.Map;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.TimeUnit;

/**
 * Created by IntelliJ IDEA.
 * User: liuzz
 * Date: 14-5-7
 * Time: 下午2:56
 */
@Service
public class ConfigStoreImpl implements ConfigStore {

    private static final Logger log = LoggerFactory.getLogger(ConfigStoreImpl.class);

    @Resource
    private ConfigDao configDao;

    @Resource
    private FileConfigDao fileConfigDao;

    @Resource
    private CacheService cacheService;

    @Resource
    private CacheConfigVersionService cacheConfigVersionService;

    @Resource
    private CacheConfigInfoService cacheConfigInfoService;

    @Resource
    private DbConfigInfoService dbConfigInfoService;

    @Resource
    private CacheFixedVersionConsumerService cacheFixedVersionConsumerService;

    @Resource
    private CachePushConfigVersionService cachePushConfigVersionService;

    @Resource
    private ClientInfoService clientInfoService;

    private LoadingCache<VersionData<ConfigMeta>, ChecksumData<String>> configCache;

    @PostConstruct
    private void init() {
        configCache = CacheBuilder.newBuilder()
                .maximumSize(5000)
                .expireAfterAccess(10, TimeUnit.SECONDS)
                .recordStats()
                .build(new CacheLoader<VersionData<ConfigMeta>, ChecksumData<String>>() {
                    @Override
                    public ChecksumData<String> load(VersionData<ConfigMeta> configId) throws ConfigNotFoundException {
                        return loadConfig(configId);
                    }
                });

        Metrics.gauge("configFile_notFound_cache_hitRate", new Supplier<Double>() {
            @Override
            public Double get() {
                return configCache.stats().hitRate();
            }
        });
    }

    @Override
    public ChecksumData<String> findConfig(VersionData<ConfigMeta> configId) throws ConfigNotFoundException {
        try {
            return configCache.get(configId);
        } catch (ExecutionException e) {
            if (e.getCause() instanceof ConfigNotFoundException) {
                throw (ConfigNotFoundException) e.getCause();
            } else {
                log.error("find config error, configId:{}", configId, e);
                throw new RuntimeException(e.getCause());
            }
        }
    }

    private ChecksumData<String> loadConfig(VersionData<ConfigMeta> configId) throws ConfigNotFoundException {
        ChecksumData<String> config = findFromDisk(configId);
        if (config != null) {
            return config;
        }

        String groupId = configId.getData().getGroup();
        Monitor.notFoundConfigFileFromDiskCounterInc(groupId);
        log.warn("config not found from disk: {}", configId);
        config = findFromDb(configId);
        if (config != null) {
            return config;
        }
        Monitor.notFoundConfigFileFromDbCounterInc(groupId);

        throw new ConfigNotFoundException();
    }

    private void saveToFile(VersionData<ConfigMeta> configId, ChecksumData<String> config) {
        try {
            fileConfigDao.save(configId, config);
        } catch (FileDaoProcessException e) {
            Monitor.syncConfigFileFailCounter.inc();
            log.error("write config to file error. {}", e.getConfigId(), e);
        }
    }

    private ChecksumData<String> findFromDb(VersionData<ConfigMeta> configId) {
        ChecksumData<String> config = configDao.loadFromCandidateSnapshot(configId);
        if (config != null) {
            saveToFile(configId, config);
        }
        return config;
    }

    private ChecksumData<String> findFromDisk(VersionData<ConfigMeta> configId) {
        try {
            return fileConfigDao.find(configId);
        } catch (FileDaoProcessException e) {
            log.error("read config file error. {}", e.getConfigId());
        } catch (ChecksumFailedException e) {
            log.error("checksum failed, {}", configId, e);
            Monitor.checkSumFailCounter.inc();
        }
        return null;
    }

    @Override
    public VersionData<ChecksumData<String>> forceLoad(String ip, VersionData<ConfigMeta> configId) throws ConfigNotFoundException {
        // 无效ip无须进行版本锁定检查
        if (!AddressUtil.INVALID_IP.equals(ip)) {
            VersionData<ChecksumData<String>> fixedVersionData = forceloadWithFixedVersion(configId, ip);
            if (fixedVersionData != null) {
                return fixedVersionData;
            }
        }

        VersionData<ChecksumData<String>> cacheData = forceloadFromCache(ip, configId);
        if (cacheData != null) {
            return cacheData;
        }

        Monitor.notFoundConfigFileFromDiskCounterInc(configId.getData().getGroup());
        VersionData<ChecksumData<String>> dbData = forceloadFromDb(ip, configId);
        if (dbData != null) {
            return dbData;
        }
        Monitor.notFoundConfigFileFromDbCounterInc(configId.getData().getGroup());

        throw new ConfigNotFoundException();
    }

    private VersionData<ChecksumData<String>> forceloadFromDb(String ip, VersionData<ConfigMeta> configId) throws ConfigNotFoundException {
        Optional<Long> version = dbConfigInfoService.getVersion(configId.getData(), ip);
        return doForceLoad(configId, version);
    }

    private VersionData<ChecksumData<String>> doForceLoad(VersionData<ConfigMeta> configId, Optional<Long> version) throws ConfigNotFoundException {
        if (version.isPresent() && version.get() >= configId.getVersion()) {
            return VersionData.of(version.get(), findConfig(VersionData.of(version.get(), configId.getData())));
        } else {
            return null;
        }
    }

    private VersionData<ChecksumData<String>> forceloadFromCache(String ip, VersionData<ConfigMeta> configId) throws ConfigNotFoundException {
        Optional<Long> version = cacheConfigInfoService.getVersion(configId.getData(), ip);
        return doForceLoad(configId, version);
    }

    private VersionData<ChecksumData<String>> forceloadWithFixedVersion(VersionData<ConfigMeta> configId, String ip) throws ConfigNotFoundException {
        Monitor.forceLoadFixedVersionCounter.inc();
        Optional<Long> fixedVersion = cacheFixedVersionConsumerService.getFixedVersion(configId.getData(), ip);
        if (fixedVersion.isPresent()) {
            if (fixedVersion.get() < configId.getVersion()) {
                throw new ConfigNotFoundException();
            }
            return VersionData.of(fixedVersion.get(), findConfig(VersionData.of(fixedVersion.get(), configId.getData())));
        }
        return null;
    }

    @Override
    public void update(ConfigMeta configMeta) {
        VersionData<ConfigMeta> configId = configDao.load(configMeta);
        if (configId == null) {
            cacheConfigVersionService.update(VersionData.of(0, configMeta), UpdateType.DELETE);
            return;
        }

        // todo: 这个东西放到这里不太合力，不过得仔细看看admin那边一起改
        Optional<ConfigMeta> parentConfigMeta = configDao.loadReference(configMeta, RefType.INHERIT);
        if(parentConfigMeta.isPresent()) {
            cacheService.updateReferenceCache(new ReferenceInfo(configMeta, parentConfigMeta.get(), RefType.INHERIT), RefChangeType.ADD);
        }

        // 上一个版本：先通知客户端，再做存盘操作
        // 但server端比较慢的时候，会出现大量客户端直接查询db
        // 因此还是先存盘，再通知
        ChecksumData<String> config = configDao.loadFromCandidateSnapshot(configId);
        saveToFile(configId, config);

        cacheConfigVersionService.update(configId, UpdateType.UPDATE);
    }

    /**
     * 加载子环境和环境的配置文件信息，子环境的文件优先级高于环境。
     * 为了减少文件数量，不加载resource的配置文件，因为是可扩展的，所以暂时不考虑resource的配置文件
     * @param group
     * @param profile
     * @return
     */
    @Override
    public List<VersionData<ConfigMeta>> loadByGroupAndProfile(String group, String profile) {
        List<VersionData<ConfigMeta>> filesInCurrentProfile = configDao.loadByGroupAndProfile(group, profile);

        String defaultProfile = Environment.extractDefaultProfile(profile).defaultProfile();
        if (Objects.equal(defaultProfile, profile)) {
            return filesInCurrentProfile;
        }

        List<VersionData<ConfigMeta>> filesInDefaultProfile = configDao.loadByGroupAndProfile(group, defaultProfile);
        return merge(filesInCurrentProfile, filesInDefaultProfile);
    }

    private List<VersionData<ConfigMeta>> merge(List<VersionData<ConfigMeta>> filesInCurrentProfile, List<VersionData<ConfigMeta>> filesInDefaultProfile) {
        Map<String, VersionData<ConfigMeta>> result = Maps.newHashMap();
        for (VersionData<ConfigMeta> fileInDefaultProfile : filesInDefaultProfile) {
            result.put(fileInDefaultProfile.getData().getDataId(), fileInDefaultProfile);
        }
        for (VersionData<ConfigMeta> fileInCurrentProfile : filesInCurrentProfile) {
            result.put(fileInCurrentProfile.getData().getDataId(), fileInCurrentProfile);
        }
        return ImmutableList.copyOf(result.values());
    }
}
