package ru.yandex.webmaster.common.addurl;

import java.util.Collections;
import java.util.HashSet;
import java.util.Iterator;
import java.util.LinkedList;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;

import org.jetbrains.annotations.Nullable;
import org.joda.time.DateTime;

import ru.yandex.webmaster.common.util.TopKCounter;

/**
 * @author aherman
 */
public class AddUrlRateLimiter {
    private final Lock hostsModificationLock = new ReentrantLock();
    private TopKCounter<String> hostnameCounter = new TopKCounter<>(20000);
    private volatile Map<String, Integer> sharedHostnameCounter = Collections.emptyMap();

    private final Lock ipModificationLock = new ReentrantLock();
    private TopKCounter<Integer> ipCounter = new TopKCounter<>(1000);
    private volatile Map<Integer, Integer> sharedIpCounter = Collections.emptyMap();

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

    public boolean checkUrlAllowed(String hostname, int maxRequests, float error) {
        hostname = toOwnerName(hostname, hostingNames);
        TopKCounter.Measure measure;
        hostsModificationLock.lock();
        try {
            measure = hostnameCounter.getMeasure(hostname);
        } finally {
            hostsModificationLock.unlock();
        }
        Integer sharedCount = sharedHostnameCounter.get(hostname);
        return checkLimit(measure, sharedCount, maxRequests, error);
    }

    public boolean markAndCheckUrlAllowed(String hostname, int maxRequests, float error) {
        hostname = toOwnerName(hostname, hostingNames);
        TopKCounter.Measure measure;
        hostsModificationLock.lock();
        try {
            measure = hostnameCounter.newEntry(hostname);
        } finally {
            hostsModificationLock.unlock();
        }
        Integer sharedCount = sharedHostnameCounter.get(hostname);
        return checkLimit(measure, sharedCount, maxRequests, error);
    }

    public boolean markAndCheckIpAllowed(int ip, int maxRequest, float error) {
        TopKCounter.Measure measure;
        ipModificationLock.lock();
        try {
            measure = ipCounter.newEntry(ip);
        } finally {
            ipModificationLock.unlock();
        }
        Integer sharedCounter = sharedIpCounter.get(ip);
        return checkLimit(measure, sharedCounter, maxRequest, error);
    }

    static boolean checkLimit(TopKCounter.Measure measure, @Nullable Integer sharedCounter, int maxRequests, float error) {
        int sc = sharedCounter != null ? sharedCounter : 0;

        if (sc > maxRequests) {
            return false;
        }

        if (sc + measure.getCount() < maxRequests) {
            return true;
        }
        if (1.0f * measure.getError() > error * measure.getCount()) {
            return true;
        }
        return false;
    }

    public List<PopularHostInfo> getPopularHosts(String backendHost, int minRequests) {
        hostsModificationLock.lock();
        try {
            Iterator<TopKCounter.Measure<String>> measureIterator = hostnameCounter.topItemIterator();
            List<PopularHostInfo> result = new LinkedList<>();
            DateTime now = DateTime.now();
            while (measureIterator.hasNext()) {
                TopKCounter.Measure<String> next = measureIterator.next();
                if (next.getCount() - next.getError() < minRequests) {
                    continue;
                }
                result.add(new PopularHostInfo(now, backendHost, next.getEntry(), next.getCount()));
            }
            return result;
        } finally {
            hostsModificationLock.unlock();
        }
    }

    public List<PopularIpInfo> getPopularIp(String backendHost, int minRequests) {
        ipModificationLock.lock();
        try {
            Iterator<TopKCounter.Measure<Integer>> measureIterator = ipCounter.topItemIterator();
            List<PopularIpInfo> result = new LinkedList<>();
            DateTime now = DateTime.now();
            while (measureIterator.hasNext()) {
                TopKCounter.Measure<Integer> next = measureIterator.next();
                if (next.getCount() - next.getError() < minRequests) {
                    continue;
                }
                result.add(new PopularIpInfo(now, backendHost, next.getEntry(), next.getCount()));
            }
            return result;
        } finally {
            ipModificationLock.unlock();
        }
    }

    public void setHostings(List<String> newHostings) {
        Set<String> newSet = new HashSet<>(newHostings.size());
        for (String hosting : newHostings) {
            newSet.add(hosting);
        }

        hostingNames = newSet;
    }

    static String toOwnerName(String hostname, Set<String> hostingNames) {
        int lastDot = hostname.lastIndexOf('.');
        int hostNameStart = hostname.lastIndexOf('.', lastDot - 1);
        if (hostNameStart < 0) {
            return hostname;
        }
        String result = hostname.substring(hostNameStart + 1, hostname.length());
        if (!hostingNames.contains(result)) {
            return result;
        }

        hostNameStart = hostname.lastIndexOf('.', hostNameStart - 1);
        if (hostNameStart < 0) {
            return hostname;
        }

        return hostname.substring(hostNameStart + 1, hostname.length());
    }

    public void setSharedHostnameCounter(Map<String, Integer> sharedHostnameCounter) {
        this.sharedHostnameCounter = sharedHostnameCounter;
    }

    public void setSharedIpCounter(Map<Integer, Integer> sharedIpCounter) {
        this.sharedIpCounter = sharedIpCounter;
    }
}
