package ru.yandex.webmaster3.worker.spamban;

import java.util.ArrayDeque;
import java.util.Iterator;
import java.util.Map;
import java.util.NavigableMap;
import java.util.Optional;
import java.util.UUID;

import com.datastax.driver.core.utils.UUIDs;
import lombok.Getter;
import lombok.RequiredArgsConstructor;
import org.joda.time.Duration;
import org.joda.time.Instant;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;

import ru.yandex.webmaster3.core.worker.task.PeriodicTaskState;
import ru.yandex.webmaster3.core.worker.task.PeriodicTaskType;
import ru.yandex.webmaster3.core.worker.task.TaskResult;
import ru.yandex.webmaster3.storage.WebmasterRedisException;
import ru.yandex.webmaster3.storage.host.CommonDataType;
import ru.yandex.webmaster3.storage.settings.SettingsService;
import ru.yandex.webmaster3.storage.user.SpamBanRule;
import ru.yandex.webmaster3.storage.user.dao.SpamBanCountersRedisService;
import ru.yandex.webmaster3.storage.user.dao.SpamBanRulesYDao;
import ru.yandex.webmaster3.worker.PeriodicTask;
import ru.yandex.webmaster3.worker.TaskSchedule;
import ru.yandex.wmtools.common.data.info.IPInfo;

/**
 * Created by ifilippov5 on 23.01.17.
 */
@Component
public class AddSpamBanRulesTask extends PeriodicTask<AddSpamBanRulesTask.TaskState> {
    @Autowired
    private SettingsService settingsService;
    @Autowired
    private SpamBanCountersRedisService spamBanCountersRedisService;
    @Autowired
    private SpamBanRulesYDao spamBanRulesYDao;

    @Override
    public Result run(UUID runId) {
        setState(new TaskState());
        Map<String, NavigableMap<Instant, Long>> counter = spamBanCountersRedisService.getHitsCountersLast2Hours();

        updateTaskStatistics(counter);

        for (Map.Entry<String, NavigableMap<Instant, Long>> entry : counter.entrySet()) {
            Optional<QuotaExceeded> quotaExceededO = isExceedQuota(entry.getValue());
            if (quotaExceededO.isPresent()) {
                QuotaExceeded quotaExceeded = quotaExceededO.get();
                String hostIp = entry.getKey();
                try {
                    IPInfo ipInfo = IPInfo.createFromString(hostIp);
                    spamBanRulesYDao.insertRule(
                            new SpamBanRule(UUIDs.timeBased(),
                                    ipInfo.getAddressAsString(),
                                    ipInfo.getMaskLength(),
                                    0L,
                                    false,
                                    String.format("Autoban ip: 1m=%d 10m=%d",
                                            quotaExceeded.minuteCount,
                                            quotaExceeded.intervalCount),
                                    Instant.now()));
                } catch (Exception e) {
                    throw new WebmasterRedisException("Redis stores incorrect ip address: " + hostIp, e);
                }
            }
        }

        return new PeriodicTask.Result(TaskResult.SUCCESS);
    }

    private void updateTaskStatistics(Map<String, NavigableMap<Instant, Long>> counter) {
        getState().ipCount = counter.size();
        for (Map.Entry<String, NavigableMap<Instant, Long>> topEntry : counter.entrySet()) {
            NavigableMap<Instant, Long> map = topEntry.getValue();
            String ip = topEntry.getKey();
            for (Map.Entry<Instant, Long> dateEntry : map.entrySet()) {
                Instant date = dateEntry.getKey();
                Long reqPerMinute = dateEntry.getValue();

                if (reqPerMinute > getState().getMaxRequestSeenPerMinute()) {
                    getState().maxRequestSeenPerMinute = reqPerMinute;
                    getState().maxRequestSeenPerMinuteIp = ip;
                    getState().maxRequestSeenPerMinuteDate = date;
                }

                Instant currentMinDate = getState().minDate;
                if (currentMinDate == null) {
                    getState().minDate = date;
                } else {
                    getState().minDate = date.isBefore(currentMinDate) ? date : currentMinDate;
                }

                Instant currentMaxDate = getState().maxDate;
                if (currentMaxDate == null) {
                    getState().maxDate = date;
                } else {
                    getState().maxDate = date.isAfter(currentMaxDate) ? date : currentMaxDate;
                }
            }
        }
    }

    Optional<QuotaExceeded> isExceedQuota(NavigableMap<Instant, Long> ipCounter) {
        final long quota = getQuota();
        final long intervalQuota = getIntervalQuota();

        ArrayDeque<Map.Entry<Instant, Long>> window = new ArrayDeque<>();
        for (Map.Entry<Instant, Long> entry : ipCounter.entrySet()) {
            if (entry.getValue() > quota) {
                return Optional.of(new QuotaExceeded(entry.getValue(), 0L));
            }

            if (window.size() == 10) {
                window.removeFirst();
            }
            window.addLast(entry);
            long intervalSum = 0L;
            assert window.peekLast() != null;
            Instant windowStart = window.peekLast().getKey().minus(Duration.standardMinutes(10L));
            Iterator<Map.Entry<Instant, Long>> it = window.descendingIterator();
            while (it.hasNext()) {
                Map.Entry<Instant, Long> next = it.next();
                if (!next.getKey().isBefore(windowStart)) {
                    intervalSum += next.getValue();
                }
            }
            if (intervalSum > intervalQuota) {
                return Optional.of(new QuotaExceeded(entry.getValue(), intervalSum));
            }
        }
        return Optional.empty();
    }

    @RequiredArgsConstructor(onConstructor_ = @Autowired)
    private static class QuotaExceeded {
        private final long minuteCount;
        private final long intervalCount;
    }

    long getQuota() {
        return settingsService.getSettingCached(CommonDataType.COUNT_OF_HITS_QUOTA_PER_MINUTE, Long::parseLong);
    }

    long getIntervalQuota() {
        return settingsService.getSettingCached(CommonDataType.COUNT_OF_HITS_QUOTA_PER_TEN_MINUTES, Long::parseLong);
    }

    @Getter
    public static class TaskState implements PeriodicTaskState {
        private long ipCount;

        private String maxRequestSeenPerMinuteIp;
        private long maxRequestSeenPerMinute;
        private Instant maxRequestSeenPerMinuteDate;

        private Instant minDate;
        private Instant maxDate;
    }

    @Override
    public PeriodicTaskType getType() {
        return PeriodicTaskType.ADD_SPAM_BAN_RULES;
    }

    @Override
    public TaskSchedule getSchedule() {
        return TaskSchedule.startByCron("0 * * * * *");
    }
}
