package ru.yandex.webmaster.viewer.service;

import java.net.URL;
import java.util.ArrayList;
import java.util.Collections;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.ConcurrentMap;
import java.util.concurrent.Executors;
import java.util.concurrent.ScheduledExecutorService;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicInteger;

import org.joda.time.DateTime;
import org.joda.time.Days;
import org.joda.time.ReadablePeriod;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Required;

import ru.yandex.common.util.concurrent.CommonThreadFactory;
import ru.yandex.webmaster.viewer.dao.TblCaptchaUsersDao;
import ru.yandex.webmaster.viewer.dao.TblSpammerDomainDao;
import ru.yandex.wmtools.common.error.InternalException;
import ru.yandex.wmtools.common.service.AbstractDbService;
import ru.yandex.wmtools.common.util.URLUtil;

/**
 * Сервис для получения из базы данных и кэширования списка доменов, для которых показывается каптча
 * при добавлении сайта
 *
 * User: azakharov
 * Date: 12.10.12
 * Time: 19:22
 */
public class SpammerDomainsCacheService extends AbstractDbService {
    private static final Logger log = LoggerFactory.getLogger(SpammerDomainsCacheService.class);

    private volatile Set<String> spamerDomains = Collections.emptySet();

    private volatile Map<Long, GlobalCaptchaShows> globalCache = Collections.emptyMap();
    private ConcurrentMap<Long, LocalCaptchaShows> localCache = new ConcurrentHashMap<Long, LocalCaptchaShows>();

    private ScheduledExecutorService initExecutor;

    private TblSpammerDomainDao tblSpammerDomainDao;
    private TblCaptchaUsersDao tblCaptchaUsersDao;
    private ReadablePeriod captchaShowPeriod = Days.days(3);
    private int cacheSyncPeriodMinutes = 10;
    private int showCaptchaThreshold = 3;

    /**
     * Вычисляет, нужно ли показывать каптчу для пользователя (и хоста).
     * Сохраняет показы каптчи в cache, затем они должны попадать в базу.
     *
     * @param url       добавляемый хост
     * @param userId    пользователь, добавляющий хост
     * @return          признак, что требуется каптча
     */
    public boolean needCaptcha(final URL url, long userId) {
        if (isSpamerDomain(url)) {
            incrementShows(userId);
            return true;
        } else {
            return getShows(userId) > showCaptchaThreshold;
        }
    }

    int getShows(long userId) {
        LocalCaptchaShows localCaptchaShows = localCache.get(userId);
        if (localCaptchaShows != null) {
            return localCaptchaShows.getCount();
        }
        GlobalCaptchaShows globalCaptchaShows = globalCache.get(userId);
        if (globalCaptchaShows != null) {
            return globalCaptchaShows.getCount();
        }
        return 0;
    }

    int incrementShows(long userId) {
        LocalCaptchaShows localCaptchaShows = localCache.get(userId);
        if (localCaptchaShows == null) {
            GlobalCaptchaShows globalCaptchaShows = globalCache.get(userId);
            if (globalCaptchaShows != null) {
                localCaptchaShows = new LocalCaptchaShows(globalCaptchaShows);
            } else {
                localCaptchaShows = new LocalCaptchaShows(userId, DateTime.now());
            }
            LocalCaptchaShows existingShows = localCache.putIfAbsent(userId, localCaptchaShows);
            if (existingShows != null) {
                localCaptchaShows = existingShows;
            }
        }

        return localCaptchaShows.increment();
    }

    /**
     * Определяет, является ли домен спамерским
     * @param url   домен
     * @return      признак, что домен спамерский
     */
    public boolean isSpamerDomain(final URL url) {
        String host = url.getHost();
        while (true) {
            String upperLevel = URLUtil.getUpperLevelDomain(host);
            if (upperLevel == null) {
                // дошли до домена первого уровня
                return false;
            }

            if (spamerDomains.contains(upperLevel)) {
                return true;
            }

            host = upperLevel;
        }
    }

    public final void init() {
        CommonThreadFactory threadFactory = new CommonThreadFactory(true, SpammerDomainsCacheService.class.getSimpleName() + "-");
        initExecutor = Executors.newSingleThreadScheduledExecutor(threadFactory);

        // подчитываем список спамерских доменов из базы каждые 30 минут
        initExecutor.scheduleAtFixedRate(new CacheUpdater(), 10, cacheSyncPeriodMinutes, TimeUnit.MINUTES);
    }

    private class CacheUpdater implements Runnable {
        @Override
        public void run() {
            try {
                updateSpammerDomains();
                updateUsers();
            } catch (InternalException e) {
                log.error("Unable to update cache", e);
            }
        }

        private void updateSpammerDomains() throws InternalException {
            spamerDomains = Collections.unmodifiableSet(new HashSet<String>(tblSpammerDomainDao.listDomains()));
        }

        private void updateUsers() throws InternalException {
            // We must update immutable collection of local values
            List<LocalCaptchaShows> localCountsCopy = new ArrayList<LocalCaptchaShows>(localCache.values());

            // Update DB shows and add new users
            for (LocalCaptchaShows localCount : localCountsCopy) {
                int localDelta = localCount.count.get() - localCount.globalShowsCount;
                tblCaptchaUsersDao.updateCaptchaShows(localDelta, localCount);
                localCount.globalShowsCount += localDelta;
            }
            // Remove expired values
            tblCaptchaUsersDao.removeExpiredUsers(captchaShowPeriod);

            // Load cleaned up shows from DB
            List<GlobalCaptchaShows> dbCounts = tblCaptchaUsersDao.listCaptchaUsers();
            Map<Long, GlobalCaptchaShows> newDbCountMap = new HashMap<Long, GlobalCaptchaShows>(dbCounts.size());
            for (GlobalCaptchaShows dbCount : dbCounts) {
                newDbCountMap.put(dbCount.getUserId(), dbCount);
            }

            // Update local shows
            for (LocalCaptchaShows localCount : localCountsCopy) {
                GlobalCaptchaShows newGlobalCount = newDbCountMap.get(localCount.getUserId());
                // We already saved local data to DB
                if (newGlobalCount == null) {
                    // But it was deleted. It means local data expired.
                    localCache.remove(localCount.getUserId());
                } else {
                    updateGlobalCount(newGlobalCount.getCount(), localCount);
                }
            }
            globalCache = newDbCountMap;
        }

        void updateGlobalCount(int newGlobalCount, LocalCaptchaShows localCaptchaShows) {
            // ALWAYS: count >= globalShowsCount
            int globalDelta = newGlobalCount - localCaptchaShows.globalShowsCount;
            localCaptchaShows.globalShowsCount = newGlobalCount;
            localCaptchaShows.count.addAndGet(globalDelta);
        }
    }

    public static class GlobalCaptchaShows {
        private final long userId;
        private final DateTime firstCaptchaDate;
        private final int count;

        public GlobalCaptchaShows(long userId, DateTime firstCaptchaDate, int count) {
            this.userId = userId;
            this.firstCaptchaDate = firstCaptchaDate;
            this.count = count;
        }

        public long getUserId() {
            return userId;
        }

        public DateTime getFirstCaptchaDate() {
            return firstCaptchaDate;
        }

        public int getCount() {
            return count;
        }
    }

    public static class LocalCaptchaShows {
        private final long userId;
        private final DateTime firstCaptchaDate;
        private final AtomicInteger count;
        private volatile int globalShowsCount;

        public LocalCaptchaShows(long userId, DateTime firstCaptchaDate) {
            this.userId = userId;
            this.firstCaptchaDate = firstCaptchaDate;
            this.count = new AtomicInteger(0);
            this.globalShowsCount = 0;
        }

        public LocalCaptchaShows(GlobalCaptchaShows globalCaptchaShows) {
            this.userId = globalCaptchaShows.getUserId();
            this.firstCaptchaDate = globalCaptchaShows.getFirstCaptchaDate();
            this.count = new AtomicInteger(globalCaptchaShows.getCount());
            this.globalShowsCount = globalCaptchaShows.getCount();
        }

        public long getUserId() {
            return userId;
        }

        public int getCount() {
            return count.get();
        }

        int increment() {
            return count.incrementAndGet();
        }

        public DateTime getFirstCaptchaDate() {
            return firstCaptchaDate;
        }
    }

    @Required
    public void setCaptchaShowDays(int days) {
        this.captchaShowPeriod = Days.days(days);
    }

    @Required
    public void setTblSpammerDomainDao(TblSpammerDomainDao tblSpammerDomainDao) {
        this.tblSpammerDomainDao = tblSpammerDomainDao;
    }

    @Required
    public void setTblCaptchaUsersDao(TblCaptchaUsersDao tblCaptchaUsersDao) {
        this.tblCaptchaUsersDao = tblCaptchaUsersDao;
    }

    @Required
    public void setCacheSyncPeriodMinutes(int cacheSyncPeriodMinutes) {
        this.cacheSyncPeriodMinutes = cacheSyncPeriodMinutes;
    }

    @Required
    public void setShowCaptchaThreshold(int showCaptchaThreshold) {
        this.showCaptchaThreshold = showCaptchaThreshold;
    }
}
