package ru.yandex.webmaster.common.host;

import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.Future;

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Required;

import ru.yandex.common.framework.pager.Pager;
import ru.yandex.common.util.collections.Pair;
import ru.yandex.common.util.concurrent.CommonThreadFactory;
import ru.yandex.webmaster.common.urltree.YandexSearchShard;
import ru.yandex.webmaster.common.host.dao.HostListDao;
import ru.yandex.wmconsole.data.VerificationStateEnum;
import ru.yandex.wmconsole.data.info.HostDbHostInfo;
import ru.yandex.wmconsole.data.partition.WMCPartition;
import ru.yandex.wmconsole.service.HostDbHostInfoService;
import ru.yandex.wmconsole.service.LinkType;
import ru.yandex.wmconsole.service.LinksCacheService;
import ru.yandex.wmconsole.service.dao.TblUrlTreesDao;
import ru.yandex.wmconsole.verification.VerificationTypeEnum;
import ru.yandex.wmtools.common.error.InternalException;
import ru.yandex.wmtools.common.error.InternalProblem;
import ru.yandex.wmtools.common.service.AbstractDbService;

/**
 * User: azakharov
 * Date: 15.01.14
 * Time: 14:40
 */
public class HostListService extends AbstractDbService {
    private static final Logger log = LoggerFactory.getLogger(HostListService.class);
    private static final int MAX_THREAD_NUM = 24;

    private HostListDao hostListDao;
    private LinksCacheService linksCacheService;
    private TblUrlTreesDao tblUrlTreesDao;
    private HostDbHostInfoService hostDbHostInfoService;

    private ExecutorService executorService;

    public void init() {
        CommonThreadFactory threadFactory = new CommonThreadFactory(true, HostListService.class.getSimpleName() + "-");
        executorService = Executors.newFixedThreadPool(MAX_THREAD_NUM, threadFactory);
    }

    public List<HostNodeInfo> getHostList(final long userId, final Pager pager) throws InternalException {
        List<HostNodeInfo> nodes = hostListDao.getHostNodes(userId);
        List<HostNodeInfo> results = new ArrayList<>(nodes.size());
        List<HostNodeInfo> unprocessed = new ArrayList<>(nodes.size());
        Map<Long, HostNodeInfo> mirrorIdToNode = new HashMap<>(nodes.size());
        for (HostNodeInfo n : nodes) {
            if (n.getMainMirrorHostId() == null) {
                // хост является главным зеркалом
                HostNodeInfo nodeInfo = mirrorIdToNode.get(n.getHostId());
                if (nodeInfo == null) {
                    // хост ещё не добавлялся в список сайтов
                    mirrorIdToNode.put(n.getHostId(), n);
                    results.add(n);
                }
            } else {
                // хост является неглавным зеркалом
                final HostNodeInfo nodeInfo = mirrorIdToNode.get(n.getMainMirrorHostId());
                if (nodeInfo == null) {
                    //мы не имеем главного зеркала, поэтому откладываем хост на потом
                    unprocessed.add(n);
                } else {
                    nodeInfo.addNode(n);
                }
            }
        }

        // Добавляем необработанные хосты
        for (HostNodeInfo info : unprocessed) {
            HostNodeInfo mainMirrorInfo = mirrorIdToNode.get(info.getMainMirrorHostId());
            if (mainMirrorInfo == null) {
                mainMirrorInfo = new HostNodeInfo(
                        info.getMainMirrorHostId(), info.getMainMirrorHostName(),
                        null, null,
                        VerificationStateEnum.NEVER_VERIFIED, VerificationTypeEnum.META_TAG);
                mainMirrorInfo.addNode(info);
                mirrorIdToNode.put(info.getMainMirrorHostId(), mainMirrorInfo);
                results.add(mainMirrorInfo);
            } else {
                mainMirrorInfo.addNode(info);
            }
        }

        Collections.sort(results);
        if (pager != null) {
            pager.setCount(results.size());
            results = results.subList(pager.getFrom() - 1, Math.min(pager.getTo(), results.size()));
        }

        loadHostDbInfo(results);

        return results;
    }

    private void loadHostDbInfo(List<HostNodeInfo> nodes) throws InternalException {
        final int baseCount = getDatabaseCount();
        final List<List<HostNodeInfo>> queues = new ArrayList<>(WMCPartition.getHostDbCount(baseCount));
        final List<List<HostNodeInfo>> mirrorQueues = new ArrayList<>(WMCPartition.getHostDbCount(baseCount));
        for (int i = 0; i < WMCPartition.getHostDbCount(baseCount); i++ ) {
            queues.add(i, new ArrayList<HostNodeInfo>(4));
            mirrorQueues.add(i, new ArrayList<HostNodeInfo>(4));
        }

        for (HostNodeInfo n : nodes) {
            int dbIndex = WMCPartition.getDatabaseIndex(baseCount, n.getHostName().toLowerCase());
            queues.get(dbIndex).add(n);
            for (HostNodeInfo c: n.getChildren()) {
                int dataBaseIndex = WMCPartition.getDatabaseIndex(baseCount, c.getHostName().toLowerCase());
                mirrorQueues.get(dataBaseIndex).add(c);
            }
        }

        final List<HostDbRunnableTask> tasks = new ArrayList<>(WMCPartition.getHostDbCount(baseCount));
        final List<Future<Boolean>> futures = new ArrayList<>(WMCPartition.getHostDbCount(baseCount));
        for (int i = 0; i < WMCPartition.getHostDbCount(baseCount); i++ ) {
            List<HostNodeInfo> queue = queues.get(i);
            List<HostNodeInfo> mirrorsQueue = mirrorQueues.get(i);
            if (!queue.isEmpty() || !mirrorQueues.isEmpty()) {
                HostDbRunnableTask task = new HostDbRunnableTask(i, queue, mirrorsQueue);
                Future<Boolean> f = executorService.submit(task, true);
                futures.add(f);
                tasks.add(task);
            }
        }

        await(futures);
        for (HostDbRunnableTask task : tasks) {
            if (task.error != null) {
                throw task.error;
            }
        }
    }

    private static void await(Collection<Future<Boolean>> futures) throws InternalException {
        for (Future<Boolean> f : futures) {
            try {
                if (f.get() == null || f.get() != true) {
                    break;
                }
            } catch (InterruptedException e) {
                throw new InternalException(InternalProblem.INTERNAL_PROBLEM, "", e);
            } catch (ExecutionException e) {
                throw new InternalException(InternalProblem.INTERNAL_PROBLEM, "", e);
            }
        }
    }

    public class HostDbRunnableTask implements Runnable {

        private final int dbIndex;
        private final List<HostNodeInfo> hosts;
        private final List<HostNodeInfo> mirrors;
        private volatile InternalException error = null;

        public HostDbRunnableTask(int dbIndex, List<HostNodeInfo> hosts, List<HostNodeInfo> mirrors) {
            this.dbIndex = dbIndex;
            this.hosts = hosts;
            this.mirrors = mirrors;
        }

        @Override
        public void run() {
            try {
                if (!hosts.isEmpty()) {
                    hostListDao.updateHostNodesWithHostDbInfo(hosts, dbIndex);

                    // Updating index count for search shard if it is available
                    for (HostNodeInfo n : hosts) {
                        final HostDbHostInfo hostDbHostInfo = hostDbHostInfoService.getHostDbHostInfo(n.getHostName(), true);
                        if (hostDbHostInfo == null) {
                            log.error("host db info not found for {}", n.getHostName());
                            continue;
                        }
                        final Pair<YandexSearchShard, Long> pair = tblUrlTreesDao.getOptimumShardIdAndIndexCount(hostDbHostInfo);
                        final YandexSearchShard shard = pair.getFirst();
                        if (pair.getSecond() != null) {
                            n.setIndexCount(pair.getSecond());
                        }
                        updateCacheValues(hostDbHostInfo, n, shard);
                    }
                }

                if (!mirrors.isEmpty()) {
                    hostListDao.updateMirrorNodesWithVirusInfo(mirrors, dbIndex);
                }
            } catch (InternalException e) {
                error = e;
            }
        }

        private void updateCacheValues(HostDbHostInfo hostDbHostInfo, HostNodeInfo info, YandexSearchShard searchShard) {
            String hostName = info.getHostName().toLowerCase();
            if (info.getTcy() == null) {
                try {
                    log.debug("tcy is null for " + hostName);
                    Integer tcy = linksCacheService.getAndCacheTcyByHostname(hostName);
                    info.setTcy(tcy);
                } catch (Throwable e) {
                    log.error("Ignoring the following exception", e);
                }
            }

            if (info.getIndexCount() == null || info.getIndexCountCacheUpdateTime() != null) {
                // Делаем запрос к xml-поиску
                try {
                    log.debug("checking index_count cache for " + hostName);
                    Long indexCount = linksCacheService.checkCacheTtl(
                            hostDbHostInfo, info.getIndexCount(),
                            info.getIndexCountCacheUpdateTime(), LinkType.INDEX_URLS, true, searchShard);
                    info.setIndexCount(indexCount);
                } catch (Throwable e) {
                    log.error("Ignoring the following exception", e);
                }
            }
        }
    }

    @Required
    public void setHostListDao(HostListDao hostListDao) {
        this.hostListDao = hostListDao;
    }

    @Required
    public void setLinksCacheService(LinksCacheService linksCacheService) {
        this.linksCacheService = linksCacheService;
    }

    @Required
    public void setTblUrlTreesDao(TblUrlTreesDao tblUrlTreesDao) {
        this.tblUrlTreesDao = tblUrlTreesDao;
    }

    @Required
    public void setHostDbHostInfoService(HostDbHostInfoService hostDbHostInfoService) {
        this.hostDbHostInfoService = hostDbHostInfoService;
    }
}
