package org.apereo.cas.web.support;

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import javax.servlet.http.HttpServletRequest;
import java.time.ZoneOffset;
import java.time.ZonedDateTime;
import java.util.Iterator;
import java.util.Map.Entry;
import java.util.Set;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.ConcurrentMap;

/**
 * Implementation of a HandlerInterceptorAdapter that keeps track of a mapping
 * of IP Addresses to number of failures to authenticate.
 *
 * @author Scott Battaglia
 * @since 3.0.0
 */
public abstract class AbstractInMemoryThrottledSubmissionHandlerInterceptorAdapter extends AbstractThrottledSubmissionHandlerInterceptorAdapter
        implements InMemoryThrottledSubmissionHandlerInterceptor {

    private static final Logger LOGGER = LoggerFactory.getLogger(AbstractInMemoryThrottledSubmissionHandlerInterceptorAdapter.class);
    
    private static final double SUBMISSION_RATE_DIVIDEND = 1000.0;

    private ConcurrentMap<String, ZonedDateTime> ipMap = new ConcurrentHashMap<>();

    public AbstractInMemoryThrottledSubmissionHandlerInterceptorAdapter(final int failureThreshold, final int failureRangeInSeconds,
                                                                        final String usernameParameter) {
        super(failureThreshold, failureRangeInSeconds, usernameParameter);
    }

    @Override
    public boolean exceedsThreshold(final HttpServletRequest request) {
        final ZonedDateTime last = this.ipMap.get(constructKey(request));
        return last != null && submissionRate(ZonedDateTime.now(ZoneOffset.UTC), last) > getThresholdRate();
    }

    @Override
    public void recordSubmissionFailure(final HttpServletRequest request) {
        this.ipMap.put(constructKey(request), ZonedDateTime.now(ZoneOffset.UTC));
    }
    
    /**
     * This class relies on an external configuration to clean it up.
     * It ignores the threshold data in the parent class.
     */
    @Override
    public void decrement() {
        LOGGER.info("Beginning audit cleanup...");

        final Set<Entry<String, ZonedDateTime>> keys = this.ipMap.entrySet();
        LOGGER.debug("Decrementing counts for throttler.  Starting key count: [{}]", keys.size());

        final ZonedDateTime now = ZonedDateTime.now(ZoneOffset.UTC);
        for (final Iterator<Entry<String, ZonedDateTime>> iter = keys.iterator(); iter.hasNext();) {
            final Entry<String, ZonedDateTime> entry = iter.next();
            if (submissionRate(now, entry.getValue()) < getThresholdRate()) {
                LOGGER.trace("Removing entry for key [{}]", entry.getKey());
                iter.remove();
            }
        }
        LOGGER.debug("Done decrementing count for throttler.");
    }

    /**
     * Computes the instantaneous rate in between two given dates corresponding to two submissions.
     *
     * @param a First date.
     * @param b Second date.
     * @return Instantaneous submission rate in submissions/sec, e.g. {@code a - b}.
     */
    private static double submissionRate(final ZonedDateTime a, final ZonedDateTime b) {
        return SUBMISSION_RATE_DIVIDEND / (a.toInstant().toEpochMilli() - b.toInstant().toEpochMilli());
    }
}
