package ru.yandex.wmconsole.service;

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Required;
import org.springframework.jdbc.core.simple.ParameterizedRowMapper;
import org.springframework.transaction.TransactionException;
import org.springframework.transaction.TransactionStatus;
import ru.yandex.common.util.collections.Cu;
import ru.yandex.common.util.collections.Pair;
import ru.yandex.common.util.concurrent.Executors;
import ru.yandex.common.util.functional.Function;
import ru.yandex.webmaster.common.host.dao.TblHostsMainDao;
import ru.yandex.webmaster.common.host.dao.TblUsersHostsDao;
import ru.yandex.webmaster.common.sitemap.ExtendedSitemapInfo;
import ru.yandex.webmaster.common.sitemap.SitemapService;
import ru.yandex.wmconsole.data.DisplayNameModerationStateEnum;
import ru.yandex.wmconsole.data.NotificationTypeEnum;
import ru.yandex.wmconsole.data.info.BriefHostInfo;
import ru.yandex.wmconsole.data.info.HostDbHostInfo;
import ru.yandex.wmconsole.data.info.HostMirrorInfo;
import ru.yandex.wmconsole.data.info.ShortUsersHostsInfo;
import ru.yandex.wmconsole.data.partition.WMCPartition;
import ru.yandex.wmconsole.service.dao.*;
import ru.yandex.wmconsole.util.WwwUtil;
import ru.yandex.wmtools.common.data.info.CustomQueryInfo;
import ru.yandex.wmtools.common.error.InternalException;
import ru.yandex.wmtools.common.error.UserException;
import ru.yandex.wmtools.common.servantlet.AbstractServantlet;
import ru.yandex.wmtools.common.service.AbstractLockableDbService;
import ru.yandex.wmtools.common.util.ServiceTransactionCallback;
import ru.yandex.wmtools.common.util.ServiceTransactionCallbackWithoutResult;

import java.sql.ResultSet;
import java.sql.SQLException;
import java.util.*;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Semaphore;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicInteger;

/**
 * User: azakharov
 * Date: 05.03.14
 * Time: 17:37
 */
public class MainMirrorProcessingService extends AbstractLockableDbService {

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

    private static final String FIELD_NOTIFICATION_ID = "notification_id";
    private static final String FIELD_ID = "mirror_groups_id";
    private static final String FIELD_RECEIVE_TIME = "receive_time";

    private static final int NOTIFICATIONS_COUNT_LIMIT = 1000;

    private static final String MAIN_MIRROR_LOCK_NAME = "main_mirror_lock";

    private HostInfoService hostInfoService;
    private HostDbHostInfoService hostDbHostInfoService;
    private WMCTopInfoService topInfoService;
    private WMCCustomRegionService customRegionService;
    private SitemapService sitemapService;
    private IndexHistoryService indexHistoryService;
    private DispatcherHttpService dispatcherHttpService;
    private HostMonHttpService hostMonHttpService;
    private MirrorGroupsChangeService mirrorGroupsChangeService;
    private NotificationService notificationService;

    // host db tables
    private TblCustomRegionsDao tblCustomRegionsDao;

    // user db tables
    private TblHostsMainDao tblHostsMainDao;
    private TblDisplayNameModerationDao tblDisplayNameModerationDao;
    private TblUsersHostsRegionsDao tblUsersHostsRegionsDao;
    private TblRegionModerationDao tblRegionModerationDao;
    private TblVerifiedHostsRegionsDao tblVerifiedHostsRegionsDao;
    private TblUsersHostsDao tblUsersHostsDao;
    private TblNotificationsDao tblNotificationsDao;
    private TblMainMirrorNotificationPairDao tblMainMirrorNotificationPairDao;
    private TblNotificationMirrorChangedDao tblNotificationMirrorChangedDao;

    private List<ExecutorService> hostDbExecutors;
    private ExecutorService userDbExecutor;

    private int mirrorChangeErrorsMax = 20;

    private final AtomicInteger undoneTasks = new AtomicInteger();

    public synchronized void changeMainMirrors() throws InternalException {
        try {
            if (!getLock(MAIN_MIRROR_LOCK_NAME)) {
                log.warn("Failed to get lock for main mirror change task");
                return;
            }

            logUserDbConnections();
            userDbExecutor = Executors.newFixedThreadPool(1);
            hostDbExecutors = new ArrayList<ExecutorService>();
            for (int i = 0; i < getDatabaseCount(); i++) {
                hostDbExecutors.add(Executors.newFixedThreadPool(1));
            }

            List<MirrorGroupNotificationInfo> notificationInfos = getUnhandledNotifications();
            undoneTasks.set(0);

            int errorCounter = 0;
            for (MirrorGroupNotificationInfo info : notificationInfos) {
                if (errorCounter >= mirrorChangeErrorsMax) {
                    log.error("Stopping changeMainMirror due to errors");
                    // завершаем цикл, чтобы дождаться завершения всех потоков и освободить блокировку
                    break;
                }

                Map<String, HostMirrorInfo> notMainBecomesMain = new HashMap<>();
                Map<String, HostMirrorInfo> mainBecomesNotMain = new HashMap<>();
                Map<String, HostMirrorInfo> notMainChangesMain = new HashMap<>();

                prepareMirrorGroups(info, notMainBecomesMain, mainBecomesNotMain, notMainChangesMain);

                final Semaphore semaphore = new Semaphore(0);

                for (Map.Entry<String, HostMirrorInfo> entry : mainBecomesNotMain.entrySet()) {
                    HostMirrorInfo mirrorInfo = entry.getValue();

                    // mirror.getHostInfo() - сам хост
                    // mirror.getNewMainMirrorInfo - новое главное зеркало хоста
                    // mirror.getOldMainMirrorInfo - NULL

                    final BriefHostInfo newHostInfo = mirrorInfo.getNewMainMirrorInfo();
                    final BriefHostInfo oldHostInfo = mirrorInfo.getHostInfo();
                    final HostDbHostInfo oldHostDbHostInfo = hostDbHostInfoService.getHostDbHostInfo(oldHostInfo.getName(), true);
                    final HostDbHostInfo newHostDbHostInfo = hostDbHostInfoService.getHostDbHostInfo(newHostInfo.getName(), true);

                    if (oldHostDbHostInfo == null || newHostDbHostInfo == null) {
                        log.error("{} or {} host not found in host db", oldHostInfo.getName(), newHostInfo.getName());

                        continue;
                    }

                    int dbIndex = WMCPartition.getDatabaseIndex(getDatabaseCount(), newHostInfo.getName());
                    logConnections(dbIndex);

                    hostDbExecutors.get(dbIndex).execute(
                            new HostDbTask(oldHostDbHostInfo, newHostDbHostInfo, oldHostInfo, newHostInfo, info, semaphore));
                    errorCounter = 0;
                }

                // Enque task to update user db
                undoneTasks.incrementAndGet();
                userDbExecutor.execute(new UserDbTask(info, notMainBecomesMain, mainBecomesNotMain, notMainChangesMain,
                        semaphore, mainBecomesNotMain.size()));
            }
            for (ExecutorService service : hostDbExecutors) {
                service.shutdown();
            }
            for (ExecutorService service : hostDbExecutors) {
                waitForShutdown(service);
            }
            log.info("Done with hostDb executors");
            log.info("Terminated");
        } finally {
            releaseLock(MAIN_MIRROR_LOCK_NAME);
        }
    }

    /**
     * Обрабатывает связанные группы зеркал из объекта <code>info</code>, выделяя три множества:
     *  - notMainBecomesMain - неглавные зеркала, которые станут главными
     *  - mainBecomesNotMain - главные зеркала, которые станут неглавными
     *  - notMainChangesMain - неглавные зеркала, которые сменят владельца
     *
     * @param info               данные из нотификации
     * @param notMainBecomesMain  неглавные зеркала, которые станут главными
     * @param mainBecomesNotMain  главные зеркала, которые станут неглавными
     * @param notMainChangeMain  неглавные зеркала, которые сменят владельца
     */
    private void prepareMirrorGroups(MirrorGroupNotificationInfo info,
                                     Map<String, HostMirrorInfo> notMainBecomesMain,
                                     Map<String, HostMirrorInfo> mainBecomesNotMain,
                                     Map<String, HostMirrorInfo> notMainChangeMain) throws InternalException {
        for (Map.Entry<String, String> entry : info.getHostMirrors().entrySet()) {

            final String hostName = entry.getKey();
            final String newMainMirrorName = entry.getValue();

            final BriefHostInfo hostInfo = tblHostsMainDao.getHostIdByHostname(hostName);
            if (hostInfo == null) {
                // хоста ещё нет в базе, значит он ещё не может быть ни у кого добавлен
                continue;
            }

            if (hostName.equalsIgnoreCase(newMainMirrorName)) {
                if (hostInfo.getMainMirrorId() != null) {
                    // Хост был неглавным, стал главным и нет других хостов, для которых он стал главным
                    final BriefHostInfo oldMainMirrorInfo = tblHostsMainDao.getBriefHostInfoByHostId(hostInfo.getMainMirrorId());

                    notMainBecomesMain.put(
                            hostName,
                            new HostMirrorInfo(
                                    hostInfo,
                                    oldMainMirrorInfo.getName(),
                                    newMainMirrorName,
                                    oldMainMirrorInfo,
                                    null,
                                    Collections.<BriefHostInfo>emptyList()));
                }

                continue;
            }

            BriefHostInfo newMainMirrorInfo = tblHostsMainDao.getHostIdByHostname(newMainMirrorName);
            if (newMainMirrorInfo == null) {
                newMainMirrorInfo = tblHostsMainDao.addHostInfo(newMainMirrorName, null);
            }

            if (newMainMirrorInfo.getMainMirrorId() != null) {
                // хост был неглавным, а стал главным
                if (notMainBecomesMain.get(newMainMirrorInfo.getName()) == null) {
                    final BriefHostInfo oldMainMirrorInfo = tblHostsMainDao.getBriefHostInfoByHostId(newMainMirrorInfo.getMainMirrorId());
                    notMainBecomesMain.put(
                            newMainMirrorInfo.getName(),
                            new HostMirrorInfo(
                                    newMainMirrorInfo,
                                    oldMainMirrorInfo.getName(),
                                    newMainMirrorInfo.getName(),
                                    oldMainMirrorInfo,
                                    null,
                                    Collections.<BriefHostInfo>emptyList()));
                }
            }

            if (hostInfo.getMainMirrorId() != null) {
                final BriefHostInfo oldMainMirrorInfo = tblHostsMainDao.getBriefHostInfoByHostId(hostInfo.getMainMirrorId());

                notMainChangeMain.put(
                        hostName,
                        new HostMirrorInfo(
                                hostInfo,
                                oldMainMirrorInfo.getName(),
                                newMainMirrorName,
                                oldMainMirrorInfo,
                                newMainMirrorInfo,
                                Collections.<BriefHostInfo>emptyList()));
            } else {
                List<BriefHostInfo> oldHostMirrors = tblHostsMainDao.getHostMirrors(hostInfo.getId());

                mainBecomesNotMain.put(
                        hostName,
                        new HostMirrorInfo(
                                hostInfo,
                                hostInfo.getName(),
                                newMainMirrorName,
                                null,
                                newMainMirrorInfo,
                                oldHostMirrors));
            }
        }
    }

    class HostDbTask implements Runnable {
        private final HostDbHostInfo oldHostDbHostInfo;
        private final HostDbHostInfo newHostDbHostInfo;
        private final BriefHostInfo oldHostInfo;
        private final BriefHostInfo newHostInfo;
        private final MirrorGroupNotificationInfo info;
        private final Semaphore semaphore;

        public HostDbTask(HostDbHostInfo oldHostDbHostInfo, HostDbHostInfo newHostDbHostInfo, BriefHostInfo oldHostInfo, BriefHostInfo newHostInfo, MirrorGroupNotificationInfo info, Semaphore semaphore) {
            this.oldHostDbHostInfo = oldHostDbHostInfo;
            this.newHostDbHostInfo = newHostDbHostInfo;
            this.oldHostInfo = oldHostInfo;
            this.newHostInfo = newHostInfo;
            this.info = info;
            this.semaphore = semaphore;
        }

        @Override
        public void run() {
            log.info("Copying host data from " + oldHostDbHostInfo.getName() + " to " + newHostDbHostInfo.getName());

            // Copy custom queries
            try {
                if (topInfoService.getCustomQueriesCountForHost(newHostDbHostInfo) == 0) {
                    List<CustomQueryInfo> customQueries =
                            topInfoService.getCustomQueriesForHost(oldHostDbHostInfo);
                    if (!customQueries.isEmpty()) {
                        List<String> queries = new ArrayList<String>();
                        for (CustomQueryInfo queryInfo : customQueries) {
                            queries.add(queryInfo.getQuery());
                        }
                        topInfoService.addCustomQueriesForHost(newHostDbHostInfo, queries);
                    }
                }
            } catch (UserException e) {
                log.error("Unable to copy custom queries", e);
            } catch (InternalException e) {
                log.error("Unable to copy custom queries", e);
            }

            // Copy custom regions
            try {
                int newCustomRegionsCount = tblCustomRegionsDao.getCustomRegionsCount(newHostDbHostInfo);

                if (newCustomRegionsCount == 0) {
                    List<Integer> regionIds = tblCustomRegionsDao.getCustomRegions(oldHostDbHostInfo);
                    if (!regionIds.isEmpty()) {
                        customRegionService.internalAddKeyRegionsForHostQuery(newHostDbHostInfo, regionIds);
                    }
                }
            } catch (InternalException e) {
                log.error("Unable to copy custom regions", e);
            }

            boolean isWwwChange = WwwUtil.equalsIgnoreWww(oldHostDbHostInfo.getName(), newHostDbHostInfo.getName());
            if (isWwwChange) {
                try {
                    topInfoService.tryToCopyTopData(oldHostDbHostInfo, newHostDbHostInfo);
                } catch (InternalException e) {
                    log.error("Unable to copy queries info", e);
                } catch (UserException e) {
                    log.error("Unable to copy queries info", e);
                }
                //Copy sitemap info

                try {
                    List<ExtendedSitemapInfo> sitemaps = sitemapService.getUserAddedSitemapUrls(oldHostDbHostInfo);
                    if (sitemaps != null && !sitemaps.isEmpty()) {
                        List<Long> sitemapIds = new LinkedList<Long>();
                        // add sitemaps to new host
                        for (ExtendedSitemapInfo extInfo: sitemaps) {
                            final String sitemapUrl = extInfo.getSitemapHostName() + extInfo.getSitemapUrlName();
                            final Date submittedOn = extInfo.getSubmittedOn();
                            try {
                                sitemapService.addSitemap(0L, 0L, newHostDbHostInfo, AbstractServantlet.prepareUrl(sitemapUrl, false), submittedOn);
                                sitemapIds.add(extInfo.getId());
                            } catch (UserException e) {
                                log.warn("Can't copy sitemap " + sitemapUrl, e);
                            }
                        }
                        // remove copied sitemaps for old host
                        try {
                            sitemapService.removeSitemaps(oldHostDbHostInfo, 0L, 0L, sitemapIds);
                        } catch (UserException e) {
                            log.warn("Can't remove sitemaps " + sitemapIds, e);
                        }
                    }
                } catch (InternalException e) {
                    log.error("Unable to copy sitemap infos", e);
                } catch (UserException e) {
                    log.error("Unable to copy sitemap infos", e);
                }
                // Копируем таблицы истории
                try {
                    indexHistoryService.tryToCopyHistoryData(oldHostDbHostInfo, newHostDbHostInfo);
                    dispatcherHttpService.updateTrendInfo(newHostDbHostInfo.getName());
                } catch (InternalException e) {
                    log.error("Unable to copy history data or update trends for host " +
                            oldHostDbHostInfo.getName() + " to " + newHostDbHostInfo.getName(), e);
                } catch (UserException e) {
                    log.error("Unable to copy history data for host " +
                            oldHostDbHostInfo.getName() + " to " + newHostDbHostInfo.getName(), e);
                }
            } // end of if isWwwChange

            // Загружаем данные hostmon
            try {
                hostMonHttpService.hostMonUploadHostData(newHostDbHostInfo.getName());
            } catch (InternalException ie) {
                log.error("Unable to upload host data for host " + newHostDbHostInfo.getName() + " from hostmon");
            }

            log.info("Finished copying host data");

            semaphore.release();

            int dbIndex = WMCPartition.getDatabaseIndex(getDatabaseCount(), newHostInfo.getName());
            logConnections(dbIndex);
        }
    }

    public class UserDbTask implements Runnable {
        private final MirrorGroupNotificationInfo info;
        private final Map<String, HostMirrorInfo> notMainBecomesMain;
        private final Map<String, HostMirrorInfo> mainBecomesNotMain;
        private final Map<String, HostMirrorInfo> notMainChangesMain;
        private final Semaphore semaphore;
        private final int desiredSemaphoreValue;

        public UserDbTask(MirrorGroupNotificationInfo info,
                          Map<String, HostMirrorInfo> notMainBecomesMain,
                          Map<String, HostMirrorInfo> mainBecomesNotMain,
                          Map<String, HostMirrorInfo> notMainChangeMain,
                          final Semaphore semaphore, int desiredSemaphoreValue) {
            this.info = info;
            this.semaphore = semaphore;
            this.desiredSemaphoreValue = desiredSemaphoreValue;
            this.notMainBecomesMain = notMainBecomesMain;
            this.mainBecomesNotMain = mainBecomesNotMain;
            this.notMainChangesMain = notMainChangeMain;
        }

        @Override
        public void run() {
            try {
                Thread.sleep(100);
            } catch (InterruptedException e) {
                //ignore
            }

            try {
                boolean success = semaphore.tryAcquire(desiredSemaphoreValue, 10, TimeUnit.MINUTES);
                if (!success) {
                    log.error("UserDb task is unable to acquire lock for notification #{} (cur = {}, desired = {})",
                            info.getNotificationId(),
                            semaphore.availablePermits(),
                            desiredSemaphoreValue);
                    return;
                }
            } catch (InterruptedException e) {
                log.error("UserDb task interrupred for notification #{}", info.getNotificationId());
                return;
            }

            try {
                try {
                    if (!getLock(MAIN_MIRROR_LOCK_NAME)) {
                        log.warn("Can't get lock to change main mirror. Shutting down.");
                        shutdownAll();
                        return;
                    }
                } catch (InternalException e) {
                    log.error("Error while getting lock to change main mirror. Shutting down.", e);
                    shutdownAll();
                    return;
                }

                for (Map.Entry<String, HostMirrorInfo> entry : mainBecomesNotMain.entrySet()) {
                    final HostMirrorInfo mirrorInfo = entry.getValue();

                    try {
                        final BriefHostInfo newHostInfo = mirrorInfo.getNewMainMirrorInfo();
                        final BriefHostInfo oldHostInfo = mirrorInfo.getHostInfo();
                        copyDataInUserDb(oldHostInfo, newHostInfo);
                    } catch (InternalException e) {
                        log.error("Unable to get data from db", e);
                        return;
                    }
                }

                final Map<Pair<String, String>, Set<Long>> notifications = new HashMap<>();

                try {
                    getServiceTransactionTemplate(WMCPartition.nullPartition()).executeInService(
                            new ServiceTransactionCallbackWithoutResult() {

                                @Override
                                public void doInTransactionWithoutResult(TransactionStatus transactionStatus) throws InternalException {


                                    // шаг 1. Отклеиваем неглавные, которые должны стать главными
                                    for (Map.Entry<String, HostMirrorInfo> entry : notMainBecomesMain.entrySet()) {

                                        HostMirrorInfo mirrorInfo = entry.getValue();

                                        BriefHostInfo hostInfo = mirrorInfo.getHostInfo();
                                        if (hostInfo == null) {
                                            // хост отсутствует в базе данных (сам хост и его неглавные зеркала не добавлены пользователями)
                                            log.warn("Host {} not found in database", entry.getKey());
                                            continue;
                                        }

                                        tblHostsMainDao.updateMirror(hostInfo, null);
                                    }

                                    // шаг 2. выставляем главные зеркала для главных, которые должны стать неглавными
                                    // и их старых неглавных зеркал
                                    for (Map.Entry<String, HostMirrorInfo> entry : mainBecomesNotMain.entrySet()) {

                                        HostMirrorInfo mirrorInfo = entry.getValue();

                                        BriefHostInfo hostInfo = mirrorInfo.getHostInfo();

                                        if (hostInfo == null) {
                                            // хост отсутствует в базе данных (сам хост и его неглавные зеркала не добавлены пользователями)
                                            log.warn("Host {} not found in database", entry.getKey());
                                            continue;
                                        }

                                        for (BriefHostInfo oldHostMirrorInfo : mirrorInfo.getOldHostMirrors()) {
                                            // хост мог сам стать главным
                                            HostMirrorInfo mi = notMainBecomesMain.get(oldHostMirrorInfo.getName());
                                            if (mi != null) {
                                                // эту ситуацию мы обработали раньше (шаг 1)
                                                continue;
                                            }
                                            mi = notMainChangesMain.get(oldHostMirrorInfo.getName());
                                            if (mi == null) {
                                                log.error("Host {} was a mirror of {}, which becomes not main mirror, " +
                                                        "but {} does not become main nor change main mirror",
                                                        oldHostMirrorInfo.getName(), entry.getKey(), oldHostMirrorInfo.getName());
                                                continue;
                                            }
                                            tblHostsMainDao.updateMirror(mi.getHostInfo(), mi.getNewMainMirrorInfo());
                                        }

                                        tblHostsMainDao.updateMirror(hostInfo, mirrorInfo.getNewMainMirrorInfo());

                                        updateUsersHosts(hostInfo, mirrorInfo.getNewMainMirrorInfo(), notifications);
                                    }

                                    // шаг 3. меняем главные зеркала у хостов, которые были неглавными и остались неглавными
                                    for (Map.Entry<String, HostMirrorInfo> entry : notMainChangesMain.entrySet()) {

                                        HostMirrorInfo mirrorInfo = entry.getValue();

                                        BriefHostInfo hostInfo = mirrorInfo.getHostInfo();

                                        if (hostInfo == null) {
                                            // хост отсутствует в базе данных (сам хост и его неглавные зеркала не добавлены пользователями)
                                            log.warn("Host {} not found in database", entry.getKey());
                                            continue;
                                        }

                                        tblHostsMainDao.updateMirror(hostInfo, mirrorInfo.getNewMainMirrorInfo());

                                        updateUsersHosts(hostInfo, mirrorInfo.getNewMainMirrorInfo(), notifications);
                                    }
                                }
                            });
                } catch (InternalException e) {
                    log.error("unable to update tbl_hosts.mirror_id for notification #{}", info.getNotificationId());
                    return;
                } catch (UserException e) {
                    log.error("unable to update tbl_hosts.mirror_id for notification #{}", info.getNotificationId());
                    return;
                }

                try {
                    tblNotificationsDao.deleteNotification(info.getNotificationId());

                    for (Map.Entry<Pair<String, String>, Set<Long>> notification : notifications.entrySet()) {
                        try {
                            long issueId = tblNotificationMirrorChangedDao.addMainMirrorChangeNotification(
                                    notification.getKey().getFirst(),
                                    notification.getKey().getSecond(),
                                    info.getReceiveTime());
                            for (Long userId : notification.getValue()) {
                                notificationService.insertNotificationForUser(NotificationTypeEnum.MIRROR_CHANGED,
                                        issueId, userId, info.getReceiveTime());
                            }
                        } catch (InternalException e) {
                            log.error("Unable to send main mirror notification for pair {}, {}",
                                    notification.getKey().getFirst(), notification.getKey().getSecond());
                        }
                    }

                    // check request status for host mirrorInfo.getHostinfo
                    for (Map.Entry<String, HostMirrorInfo> entry : mainBecomesNotMain.entrySet()) {
                        final HostMirrorInfo mirrorInfo = entry.getValue();
                        mirrorGroupsChangeService.applyLogicOnMainMirrorChange(
                                mirrorInfo.getHostInfo(), mirrorInfo.getNewMainMirrorInfo());
                    }
                    // check request status for host mirrorInfo.getHostinfo
                    for (Map.Entry<String, HostMirrorInfo> entry : notMainBecomesMain.entrySet()) {
                        final HostMirrorInfo mirrorInfo = entry.getValue();
                        mirrorGroupsChangeService.applyLogicOnMainMirrorChange(
                                mirrorInfo.getHostInfo(), null);
                    }
                    // check request status for host mirrorInfo.getHostinfo
                    for (Map.Entry<String, HostMirrorInfo> entry : notMainChangesMain.entrySet()) {
                        final HostMirrorInfo mirrorInfo = entry.getValue();
                        mirrorGroupsChangeService.applyLogicOnMainMirrorChange(
                                mirrorInfo.getHostInfo(), mirrorInfo.getNewMainMirrorInfo());
                    }


                } catch (InternalException e) {
                    log.error("Unable to change main mirror for notification #{}", info.getNotificationId());
                }
                log.info("Finished changing main mirrors for for notification #{}", info.getNotificationId());
            } finally {
                undoneTasks.decrementAndGet();
            }
        }

        private void copyDataInUserDb(final BriefHostInfo oldHostInfo, final BriefHostInfo newHostInfo) throws InternalException {
            log.info("Copying info in user db");
            boolean isWwwChange = WwwUtil.equalsIgnoreWww(oldHostInfo.getName(), newHostInfo.getName());
            if (isWwwChange) {
                // Copy display name
                try {
                    DisplayNameModerationStateEnum state;
                    if (hostInfoService.getDisplayNameModerationStateByHostId(newHostInfo.getId()) == null &&
                            (state = hostInfoService.getDisplayNameModerationStateByHostId(oldHostInfo.getId())) != null) {
                        String oldDisplayName = hostInfoService.getDisplayHostNameByHostName(oldHostInfo.getName());
                        if (oldDisplayName != null) {
                            // Update display name change request to new name
                            String newDisplayName = WwwUtil.switchWWW(oldDisplayName);
                            log.debug("Update display name from " + oldDisplayName + " to " + newDisplayName + " status " + state);
                            tblDisplayNameModerationDao.copyDisplayNameModeration(newHostInfo.getId(), newDisplayName, oldHostInfo.getId());
                        }
                    }
                } catch (InternalException e) {
                    log.error("Unable to copy display name");
                }

                // Copy user regions
                try {
                    getServiceTransactionTemplate(WMCPartition.nullPartition()).executeInService(
                            new ServiceTransactionCallback() {
                                @Override
                                public Object doInTransaction(TransactionStatus transactionStatus) throws InternalException {
                                    log.debug("Copy host regions from " + oldHostInfo.getId() + " to " + newHostInfo.getId());
                                    tblUsersHostsRegionsDao.copyUsersHostsRegions(oldHostInfo.getId(), newHostInfo.getId());
                                    tblRegionModerationDao.updateRegionModerationHostId(oldHostInfo.getId(), newHostInfo.getId());
                                    tblVerifiedHostsRegionsDao.copyVerifiedHostsRegions(oldHostInfo.getId(), newHostInfo.getId());
                                    return null;
                                }
                            });
                } catch (TransactionException e) {
                    log.error("Unable to copy user regions", e);
                } catch (UserException e) {
                    log.error("Unable to copy user regions", e);
                } catch (InternalException e) {
                    log.error("Unable to copy user regions", e);
                }
            } // end of if isWwwChange

            mirrorGroupsChangeService.copyRequestsOnMainMirrorChange(oldHostInfo, newHostInfo);
        }
    }

    private void updateUsersHosts(final BriefHostInfo oldHostInfo, final BriefHostInfo newHostInfo, final Map<Pair<String, String>, Set<Long>> notifications) throws InternalException {
        List<ShortUsersHostsInfo> verificationInfos = tblUsersHostsDao.listUsersForHostShortInfo(oldHostInfo.getId());
        List<ShortUsersHostsInfo> newHostOwners = tblUsersHostsDao.listUsersForHostShortInfo(newHostInfo.getId());

        Function<ShortUsersHostsInfo, Long> getUserId = new Function<ShortUsersHostsInfo, Long>() {
            @Override
            public Long apply(ShortUsersHostsInfo info) {
                return info.getUserId();
            }
        };
        final List<Long> oldHostUsers = Cu.map(getUserId, verificationInfos);
        final List<Long> newHostUsers = Cu.map(getUserId, newHostOwners);

        final boolean isWwwChange = WwwUtil.equalsIgnoreWww(oldHostInfo.getName(), newHostInfo.getName());

        try {
            log.debug("Changing main mirror from {} to {}", oldHostInfo.getId(), newHostInfo.getId());

            List<Long> onlyOldHostOwners = new ArrayList<>(oldHostUsers);
            onlyOldHostOwners.removeAll(newHostUsers);
            if (isWwwChange) {
                // if user has both sites, hold both sites in site list
                // if user has only one site, replace old main mirror with new one

                if (!onlyOldHostOwners.isEmpty()) {
                    tblUsersHostsDao.changeHostIdAndResetVerification(newHostInfo.getId(), oldHostInfo.getId(), onlyOldHostOwners);
                }
            } else {
                // if new main mirror is not is user sites list, add new main mirror to the list
                for (Long userId : onlyOldHostOwners) {
                    tblUsersHostsDao.addHostToUser(userId, newHostInfo);
                }
            }

            if (newHostInfo.getMainMirrorId() != null) {
                // newHostInfo.mirrorId = null
                tblHostsMainDao.updateMirror(newHostInfo, null);
            }

            if (oldHostInfo.getMainMirrorId() == null ||
                    oldHostInfo.getMainMirrorId() != newHostInfo.getId()) {
                // oldHostInfo.mirrorId = newHostInfo.getId
                tblHostsMainDao.updateMirror(oldHostInfo, newHostInfo);
            }

            Set<Long> users = notifications.get(new Pair<String, String>(oldHostInfo.getName(), newHostInfo.getName()));
            if (users == null) {
                users = new HashSet<>();
                notifications.put(new Pair<String, String>(oldHostInfo.getName(), newHostInfo.getName()), users);
            }
            users.addAll(oldHostUsers);
        } catch (InternalException e) {
            log.error("Unable to change main mirror", e);
        }
    }

    private List<MirrorGroupNotificationInfo> getUnhandledNotifications() throws InternalException {
        final String query =
                "SELECT " +
                        "   n.notification_id AS " + FIELD_NOTIFICATION_ID + ", " +
                        "   nmc.mirror_groups_id AS " + FIELD_ID + ", " +
                        "   nmc.receive_time AS " + FIELD_RECEIVE_TIME + " " +
                        "FROM " +
                        "   tbl_notifications n " +
                        "JOIN " +
                        "   tbl_notification_mirrorgroups nmc " +
                        "ON " +
                        "   n.issue_id = nmc.mirror_groups_id " +
                        "WHERE " +
                        "   n.notification_type = " + NotificationTypeEnum.MIRROR_GROUPS.getValue() + " " +
                        "LIMIT " + NOTIFICATIONS_COUNT_LIMIT;

        List<MirrorGroupNotificationInfo> res = getJdbcTemplate(WMCPartition.nullPartition()).query(query, MAIN_MIRROR_NOTIFICATION_MAPPER);

        for (MirrorGroupNotificationInfo info : res) {
            for (Pair<String, String> p : tblMainMirrorNotificationPairDao.getMirrorGroups(info.getIssueId())) {
                info.addPair(p.getFirst(), p.getSecond());
            }
        }

        return res;
    }

    private boolean waitForShutdown(ExecutorService service) {
        while (!Thread.interrupted()) {
            try {
                if (service.awaitTermination(1, TimeUnit.SECONDS)) {
                    return true;
                }
            } catch (InterruptedException e) {
                return false;
            }
        }
        return false;
    }

    private void shutdownAll() {
        userDbExecutor.shutdownNow();
        for (ExecutorService service : hostDbExecutors) {
            service.shutdownNow();
        }
    }

    private static final ParameterizedRowMapper<MirrorGroupNotificationInfo> MAIN_MIRROR_NOTIFICATION_MAPPER =
                new ParameterizedRowMapper<MirrorGroupNotificationInfo>() {
                    @Override
                    public MirrorGroupNotificationInfo mapRow(ResultSet resultSet, int rowNum) throws SQLException {
                        Long notificationId = resultSet.getLong(FIELD_NOTIFICATION_ID);
                        Long id = resultSet.getLong(FIELD_ID);
                        Date receiveTime = resultSet.getDate(FIELD_RECEIVE_TIME);
                        return new MirrorGroupNotificationInfo(notificationId, id, receiveTime);
                    }
                };

    public static final class MirrorGroupNotificationInfo {
        private final long notificationId;
        private final long issueId;
        private final Date receiveTime;
        private final Map<String, String> hostMirrors;

        public MirrorGroupNotificationInfo(long notificationId, long issueId, Date receiveTime) {
            this.notificationId = notificationId;
            this.issueId = issueId;
            this.receiveTime = receiveTime;
            this.hostMirrors = new HashMap<>();
        }

        public long getNotificationId() {
            return notificationId;
        }

        public long getIssueId() {
            return issueId;
        }

        public Date getReceiveTime() {
            return receiveTime;
        }

        public Map<String, String> getHostMirrors() {
            return hostMirrors;
        }

        public void addPair(String host, String newMainMirror) {
            hostMirrors.put(host, newMainMirror);
        }
    }

    @Required
    public void setHostInfoService(HostInfoService hostInfoService) {
        this.hostInfoService = hostInfoService;
    }

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

    @Required
    public void setTopInfoService(WMCTopInfoService topInfoService) {
        this.topInfoService = topInfoService;
    }

    @Required
    public void setCustomRegionService(WMCCustomRegionService customRegionService) {
        this.customRegionService = customRegionService;
    }

    @Required
    public void setTblCustomRegionsDao(TblCustomRegionsDao tblCustomRegionsDao) {
        this.tblCustomRegionsDao = tblCustomRegionsDao;
    }

    @Required
    public void setSitemapService(SitemapService sitemapService) {
        this.sitemapService = sitemapService;
    }

    @Required
    public void setIndexHistoryService(IndexHistoryService indexHistoryService) {
        this.indexHistoryService = indexHistoryService;
    }

    @Required
    public void setDispatcherHttpService(DispatcherHttpService dispatcherHttpService) {
        this.dispatcherHttpService = dispatcherHttpService;
    }

    @Required
    public void setHostMonHttpService(HostMonHttpService hostMonHttpService) {
        this.hostMonHttpService = hostMonHttpService;
    }

    @Required
    public void setTblDisplayNameModerationDao(TblDisplayNameModerationDao tblDisplayNameModerationDao) {
        this.tblDisplayNameModerationDao = tblDisplayNameModerationDao;
    }

    @Required
    public void setTblHostsMainDao(TblHostsMainDao tblHostsMainDao) {
        this.tblHostsMainDao = tblHostsMainDao;
    }

    @Required
    public void setTblUsersHostsRegionsDao(TblUsersHostsRegionsDao tblUsersHostsRegionsDao) {
        this.tblUsersHostsRegionsDao = tblUsersHostsRegionsDao;
    }

    @Required
    public void setTblRegionModerationDao(TblRegionModerationDao tblRegionModerationDao) {
        this.tblRegionModerationDao = tblRegionModerationDao;
    }

    @Required
    public void setTblVerifiedHostsRegionsDao(TblVerifiedHostsRegionsDao tblVerifiedHostsRegionsDao) {
        this.tblVerifiedHostsRegionsDao = tblVerifiedHostsRegionsDao;
    }

    @Required
    public void setMirrorGroupsChangeService(MirrorGroupsChangeService mirrorGroupsChangeService) {
        this.mirrorGroupsChangeService = mirrorGroupsChangeService;
    }

    @Required
    public void setTblUsersHostsDao(TblUsersHostsDao tblUsersHostsDao) {
        this.tblUsersHostsDao = tblUsersHostsDao;
    }

    @Required
    public void setNotificationService(NotificationService notificationService) {
        this.notificationService = notificationService;
    }

    @Required
    public void setTblNotificationsDao(TblNotificationsDao tblNotificationsDao) {
        this.tblNotificationsDao = tblNotificationsDao;
    }

    @Required
    public void setTblMainMirrorNotificationPairDao(TblMainMirrorNotificationPairDao tblMainMirrorNotificationPairDao) {
        this.tblMainMirrorNotificationPairDao = tblMainMirrorNotificationPairDao;
    }

    @Required
    public void setTblNotificationMirrorChangedDao(TblNotificationMirrorChangedDao tblNotificationMirrorChangedDao) {
        this.tblNotificationMirrorChangedDao = tblNotificationMirrorChangedDao;
    }

    @Required
    public void setMirrorChangeErrorsMax(int mirrorChangeErrorsMax) {
        this.mirrorChangeErrorsMax = mirrorChangeErrorsMax;
    }
}
