package ru.yandex.webmaster3.storage.host;

import java.util.ArrayList;
import java.util.List;
import java.util.UUID;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;
import java.util.function.Consumer;

import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.lang3.mutable.MutableLong;
import org.apache.commons.lang3.mutable.MutableObject;
import org.apache.commons.lang3.tuple.Pair;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;

import ru.yandex.webmaster3.core.WebmasterException;
import ru.yandex.webmaster3.core.data.WebmasterHostId;
import ru.yandex.webmaster3.core.util.trie.*;
import ru.yandex.webmaster3.storage.host.dao.HostsYDao;
import ru.yandex.webmaster3.storage.spam.ISpamHostFilter;
import ru.yandex.webmaster3.storage.util.ydb.exception.WebmasterYdbException;

/**
 * @author aherman
 */
@Slf4j
@Service
@RequiredArgsConstructor(onConstructor_ = @Autowired)
public class AllHostsCacheService {
    private static final long MAX_CACHE_TIME_MS = TimeUnit.HOURS.toMillis(2);
    private static final UUID DUMMY_UUID = UUID.fromString("00000000-0000-4000-a000-000000000000");

    private final HostsYDao hostsYDao;
    private final ISpamHostFilter fastSpamHostFilter;

    private volatile long updateTime;
    private final Lock updateLock = new ReentrantLock();

    private volatile CompactTrieMap<WebmasterHostId, UUID> trie;
    private static final UUIDDriver UUID_DRIVER = new UUIDDriver();

    private static final long TRIE_BATCH_SIZE = 200000;

    public void resolveTrie() {
        long now = System.currentTimeMillis();
        if (trie != null && now - updateTime < MAX_CACHE_TIME_MS) {
            return;
        }
        updateLock.lock();
        try {
            if (trie != null && now - updateTime < MAX_CACHE_TIME_MS) {
                return;
            }
            trie = null; // освобождаем память
            log.info("Hosts trie is stale, update...");
            trie = new CompactTrieMap<>(buildTrie(), hostConverter);
            updateTime = System.currentTimeMillis();
            log.info("Hosts loaded: {} bytes, load time {}", trie.getDataSizeBytes(), updateTime - now);
        } finally {
            updateLock.unlock();
        }
    }

    public ArrayList<WebmasterHostId> getAllHostsList() {
        resolveTrie();
        ArrayList<WebmasterHostId> hosts = new ArrayList<>();
        trie.foreachKey(hosts::add);
        return hosts;
    }

    public void processEntryBatch(Consumer<List<Pair<WebmasterHostId, UUID>>> batchConsumer, int batchSize) {
        resolveTrie();
        List<Pair<WebmasterHostId, UUID>> hosts = new ArrayList<>();
        trie.foreachEntry((host, uid) -> {
                hosts.add(Pair.of(host, uid));
                if (hosts.size() == batchSize) {
                    batchConsumer.accept(new ArrayList<>(hosts));
                    hosts.clear();
                }
        });
        if (!hosts.isEmpty()) {
            batchConsumer.accept(new ArrayList<>(hosts));
        }
    }

    public void processKeyBatch(Consumer<List<WebmasterHostId>> batchConsumer, int batchSize) {
        resolveTrie();
        ArrayList<WebmasterHostId> hosts = new ArrayList<>();
        trie.foreachKey(host -> {
            hosts.add(host);
            if (hosts.size() == batchSize) {
                batchConsumer.accept(new ArrayList<>(hosts));
                hosts.clear();
            }
        });
        if (!hosts.isEmpty()) {
            batchConsumer.accept(new ArrayList<>(hosts));
        }
    }

    public boolean contains(WebmasterHostId hostId) {
        resolveTrie();
        return trie.contains(hostId);
    }

    public UUID getGenerationId(WebmasterHostId hostId) {
        resolveTrie();
        return trie.get(hostId);
    }

    public CompactTrie<UUID> buildTrie() throws WebmasterException {
        MutableObject<CompactTrie<UUID>> compactTrie = new MutableObject<>();
        MutableObject<CompactTrieBuilder<UUID>> trie = new MutableObject<>(new CompactTrieBuilder<>());
        MutableLong hosts = new MutableLong();
        MutableLong sumLen = new MutableLong();
        MutableLong spamHosts = new MutableLong();
        long start = System.currentTimeMillis();
        try {
            hostsYDao.forEach(pair -> {
                WebmasterHostId hostId = pair.getLeft();
                if (fastSpamHostFilter.checkHost(hostId)) {
                    spamHosts.increment();
                }
                if (hosts.incrementAndGet() % TRIE_BATCH_SIZE == 0) {
                    log.info("Loaded {} hosts", hosts.getValue());
                    long startMerge = System.currentTimeMillis();
                    CompactTrie<UUID> newTrie;
                    if (compactTrie.getValue() == null) {
                        newTrie = CompactTrie.fromNode(trie.getValue(), UUID_DRIVER);
                    } else {
                        newTrie = CompactTrie.fromNode(MergedNodeImpl.createMerged(compactTrie.getValue().getRootNode(), trie.getValue().getRoot(), (a, b) -> b), UUID_DRIVER);
                    }
                    trie.setValue(new CompactTrieBuilder<>());
                    compactTrie.setValue(null);
                    newTrie = newTrie.shrink();
                    compactTrie.setValue(newTrie);
                    log.info("Merged in {}ms", System.currentTimeMillis() - startMerge);
                }

                byte[] key = hostConverter.toRawData(hostId);
                sumLen.add(key.length);
                trie.getValue().insert(key, DUMMY_UUID);
            });
            log.info("Spam hosts filtered {}", spamHosts.getValue());
            if (compactTrie.getValue() == null) {
                compactTrie.setValue(CompactTrie.fromNode(trie.getValue(), UUID_DRIVER).shrink());
            } else {
                compactTrie.setValue(CompactTrie.fromNode(MergedNodeImpl.createMerged(compactTrie.getValue().getRootNode(), trie.getValue().getRoot(), (a, b) -> b), UUID_DRIVER).shrink());
            }
            long loaded = System.currentTimeMillis();
            log.info("Loaded in {} seconds, hostnames sum length {}", (loaded - start) / 1000, sumLen.getValue());
        } catch (WebmasterYdbException e) {
            throw new RuntimeException();
        }
        return compactTrie.getValue();
    }

    public void foreachHost(Consumer<WebmasterHostId> consumer) {
        resolveTrie();
        trie.foreachKey(consumer);
    }

    public void foreachEntry(CompactTrieMap.EntryConsumer<WebmasterHostId, UUID> consumer) {
        resolveTrie();
        trie.foreachEntry(consumer);
    }

    private static IDataConverter<WebmasterHostId> hostConverter = new IDataConverter<WebmasterHostId>() {
        @Override
        public byte[] toRawData(WebmasterHostId hostId) {
            return EncodeHostUtil.hostToByteArray(hostId);
        }

        @Override
        public WebmasterHostId fromRawData(byte[] data, int len) {
            return EncodeHostUtil.fromByteArray(data, len);
        }
    };
}
