package ru.yandex.webmaster3.storage.settings;

import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.ConcurrentMap;
import java.util.concurrent.TimeUnit;
import java.util.function.Function;

import org.jetbrains.annotations.NotNull;
import org.joda.time.DateTime;
import org.joda.time.Duration;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Required;

import ru.yandex.webmaster3.core.WebmasterException;
import ru.yandex.webmaster3.core.http.WebmasterErrorResponse;
import ru.yandex.webmaster3.storage.host.CommonDataState;
import ru.yandex.webmaster3.storage.host.CommonDataType;
import ru.yandex.webmaster3.storage.settings.dao.CommonDataStateYDao;
import ru.yandex.webmaster3.storage.util.ydb.exception.WebmasterYdbException;

/**
 * @author aherman
 */
public class SettingsService {
    private static final Logger log = LoggerFactory.getLogger(SettingsService.class);
    private static final Map<CommonDataType, String> DEFAULTS = new HashMap<>();

    static {
        DEFAULTS.put(CommonDataType.DELURL_URL_QUOTA_PER_DAY, "500");
        DEFAULTS.put(CommonDataType.DELURL_PREFIX_QUOTA_PER_DAY, "20");
        DEFAULTS.put(CommonDataType.RECRAWL_URL_QUOTA_PER_DAY, "20");
        DEFAULTS.put(CommonDataType.COUNT_OF_HITS_QUOTA_PER_MINUTE, "1000");
        DEFAULTS.put(CommonDataType.COUNT_OF_HITS_QUOTA_PER_TEN_MINUTES, "5000");
        DEFAULTS.put(CommonDataType.DOWNLOAD_INTERVAL_BETWEEN_INTERNAL_ERRORS_IN_MINUTES, "5");
        DEFAULTS.put(CommonDataType.NEW_SEARCH_BASE_MAIL_ENABLED, "false");
        DEFAULTS.put(CommonDataType.CRAWL_DELAY_DEFAULT_RPS, "3");
        DEFAULTS.put(CommonDataType.CRAWL_DELAY_IMPORT_DATE, "1970-01-01T00:00:00.000000Z");
        DEFAULTS.put(CommonDataType.BETA_USERS_LAST_IMPORT_DATE, "1970-01-01T00:00:00.000000Z");
        DEFAULTS.put(CommonDataType.DOMAINS_WITH_DIRECT_DYN_ADS_GENERATION, "e571f330-6b0c-11e8-8d42-f1e687b58f4a"); // какой-то случайный uuid
        DEFAULTS.put(CommonDataType.DIALOGS_HOSTS_GENERATION, "e571f330-6b0c-11e8-8d42-f1e687b58f4a"); // какой-то случайный uuid
        DEFAULTS.put(CommonDataType.COMMERCIAL_OWNERS_GENERATION, "e571f330-6b0c-11e8-8d42-f1e687b58f4a"); // какой-то случайный uuid
        DEFAULTS.put(CommonDataType.CHAT_STATS_GENERATION, "e571f330-6b0c-11e8-8d42-f1e687b58f4a"); // какой-то случайный uuid
        DEFAULTS.put(CommonDataType.ROBOTS_TXT_LAST_TABLE, "2018-11-01");
        DEFAULTS.put(CommonDataType.TURBO_HOST_BAN, "0");
        DEFAULTS.put(CommonDataType.MDB_IMPORT_MIRRORS, "0");
        DEFAULTS.put(CommonDataType.IMPORT_MIRRORS, "0");
        DEFAULTS.put(CommonDataType.LAST_EXPORTED_HOSTS, "0");
        DEFAULTS.put(CommonDataType.LAST_EU_EMAILS_UPDATE, "1970-01-01");
        DEFAULTS.put(CommonDataType.THRESHOLD_BAD_URLS, "0.1");
    }

    private CommonDataStateYDao commonDataStateYDao;
    private boolean allowConsistencyDowngradeForReads;
    private volatile long lastUpdateTime = 0L;
    private long ttlMs = TimeUnit.MINUTES.toMillis(1);
    private final ConcurrentMap<CommonDataType, CommonDataState> cache = new ConcurrentHashMap<>();

    public void init() {
        loadSettings();
    }

    private void loadSettings() {
        try {
            List<CommonDataState> commonDataStates;
            try {
                commonDataStates = commonDataStateYDao.listValues();
            } catch (WebmasterYdbException e) {
                if (allowConsistencyDowngradeForReads) {
                    commonDataStates = commonDataStateYDao.listValues();
                } else {
                    throw e;
                }
            }

            for (CommonDataState commonDataState : commonDataStates) {
                cache.put(commonDataState.getType(), commonDataState);
            }
        } catch (WebmasterYdbException e) {
            throw new RuntimeException("Unable to read settings", e);
        }
        for (Map.Entry<CommonDataType, String> entry : DEFAULTS.entrySet()) {
            cache.computeIfAbsent(entry.getKey(), k -> new CommonDataState(k, entry.getValue(), DateTime.now()));
        }
    }

    public CommonDataState getSettingUncached(CommonDataType type) {
        CommonDataState value = commonDataStateYDao.getValue(type);

        if (value == null) {
            String data = DEFAULTS.get(type);
            if (data != null) {
                return new CommonDataState(type, data, DateTime.now());
            }
        }

        return value;
    }

    @NotNull
    public CommonDataState getSettingCached(CommonDataType type) {
        updateCache(false);
        CommonDataState result = cache.get(type);
        if (result == null) {
            throw new WebmasterException("Unable to find setting: " + type,
                    new WebmasterErrorResponse.InternalUnknownErrorResponse(this.getClass(),
                            "Unable to find setting: " + type));
        }
        return result;
    }

    public CommonDataState getSettingOrNull(CommonDataType type) {
        updateCache(false);
        return cache.get(type);
    }

    public <T> T getSettingCached(CommonDataType type, Function<String, T> mapper) {
        return mapper.apply(getSettingCached(type).getValue());
    }

    public void update(CommonDataType type, String value) {
        CommonDataState cds = new CommonDataState(type, value, DateTime.now());
        try {
            commonDataStateYDao.update(cds);
        } catch (WebmasterYdbException e) {
            throw new WebmasterException("Unable to update setting: " + type,
                    new WebmasterErrorResponse.YDBErrorResponse(this.getClass(), e), e);
        }
        cache.put(cds.getType(), cds);
    }

    private void updateCache(boolean force) {
        long now = System.currentTimeMillis();
        if (force || now - lastUpdateTime > ttlMs) {
            loadSettings();
            lastUpdateTime = now;
        }
    }

    public void setTtl(Duration ttl) {
        this.ttlMs = ttl.getMillis();
    }

    @Required
    public void setCommonDataStateYDao(CommonDataStateYDao commonDataStateYDao) {
        this.commonDataStateYDao = commonDataStateYDao;
    }

    @Required
    public void setAllowConsistencyDowngradeForReads(boolean allowConsistencyDowngradeForReads) {
        this.allowConsistencyDowngradeForReads = allowConsistencyDowngradeForReads;
    }
}
