package com.sonic.common.stat;

import java.util.Collections;
import java.util.Comparator;
import java.util.List;
import java.util.Map;
import java.util.Optional;
import java.util.concurrent.ArrayBlockingQueue;
import java.util.concurrent.TimeUnit;
import java.util.function.BiConsumer;
import java.util.function.Function;
import java.util.stream.Collectors;

import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;

import com.sonic.common.auth.OriginalCaller;
import org.apache.commons.lang3.StringUtils;
import org.springframework.core.annotation.Order;
import org.springframework.util.AntPathMatcher;
import org.springframework.web.method.HandlerMethod;
import org.springframework.web.servlet.HandlerInterceptor;

import com.sonic.common.auth.AuthInterceptor;
import com.google.common.base.Splitter;
import com.google.common.cache.CacheBuilder;
import com.google.common.cache.CacheLoader;
import com.google.common.cache.LoadingCache;
import com.google.common.collect.Lists;

import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Data;
import lombok.Getter;
import lombok.ToString;
import lombok.extern.slf4j.Slf4j;

/**
 * 访问告警, 如果某个接口访问次数超过阈值, 则告警
 * <p>
 * 配置规则:
 * /{path} = {seconds}/{permits}. 表示该路径允许多少秒内访问多少次
 * eg:
 * /* = 10/100. 表示所有的请求允许10秒内访问100次
 * /index = 5/10 表示/home请求允许5秒内访问10次
 * note:
 * 1. 支持通配符与PathVariable
 * 2. seconds, permits不能为负数, permits不能超过1000. 否则会被拦截器修正为 seconds = 1, permits = 1000
 * </p>
 * @author code
 */
@Slf4j
@Order(200)
public class FrequencyAlertInterceptor implements HandlerInterceptor {
    private Map<String, Rule> rules;
    private BiConsumer<HttpServletRequest, FrequencyAlert> alertConsumer;
    private final static int MAX_TRACER_SIZE = 1000;

    private AntPathMatcher matcher = new AntPathMatcher();
    /** 每个Ip+Rule访问对应的访问时间队列, key = ip+rule, value = touch queue */
    private static final LoadingCache<String, ArrayBlockingQueue<Tracer>> touchQueueCache = CacheBuilder.newBuilder()
            .maximumSize(1000)
            .expireAfterAccess(1, TimeUnit.DAYS)
            .build(new CacheLoader<String, ArrayBlockingQueue<Tracer>>() {
                @Override
                public ArrayBlockingQueue<Tracer> load(String key) throws Exception {
                    //队列在最大trace size上做200的冗余, 避免访问记录放到队列时失败
                    return new ArrayBlockingQueue<>(MAX_TRACER_SIZE + 200);
                }
            });

    public FrequencyAlertInterceptor(Map<String, String> rules, BiConsumer<HttpServletRequest, FrequencyAlert> alertConsumer) {
        this.alertConsumer = alertConsumer;
        this.rules = Rule.fromExpression(rules);
    }

    @Override
    public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
        if (handler == null
                || !handler.getClass().isAssignableFrom(HandlerMethod.class)
                || alertConsumer == null
                || rules.isEmpty()) {
            return true;
        }

        Optional<Rule> matchRule = getMatchRule(request.getRequestURI());
        if (!matchRule.isPresent()) {
            return true;
        }

        //key = ip+handlerMethodMessage
        String key = getRemoteIp(request) + ((HandlerMethod) handler).getShortLogMessage();
        long now = System.currentTimeMillis();

        //拿到当前的queue, 并将当前访问时间加到队列中
        ArrayBlockingQueue<Tracer> touchQueue = touchQueueCache.getUnchecked(key);
        Object caller = request.getAttribute(AuthInterceptor.CALLER_PAYLOAD_KEY);
        touchQueue.offer(Tracer.builder()
                .time(now)
                .url(request.getRequestURI())
                .caller((OriginalCaller) caller)
                .build());

        //如果访问次数超过配置, 则将头部第一条拿出检查. 时间是否在规定时间内(当前时间-规则限制时间)
        if (touchQueue.size() > matchRule.get().getPermits()) {
            Tracer head = touchQueue.poll();
            if (head != null && head.getTime() > now - matchRule.get().getMillis()) {
                List<Tracer> tracers = Lists.newArrayListWithCapacity(touchQueue.size());
                touchQueue.drainTo(tracers);

                List<Tracer> sortedTracers = tracers.stream()
                        .sorted(Comparator.comparingLong(Tracer::getTime))
                        .collect(Collectors.toList());
                //XXX 此处为了便于观察, 需要计算每个tracer与前一个tracer的时间差值
                for (int i = sortedTracers.size() - 1; i > 0; i--) {
                    Tracer tracer = sortedTracers.get(i);
                    tracer.setMillsToPrevious(tracer.getTime() - sortedTracers.get(i - 1).getTime());
                }

                alertConsumer.accept(request, FrequencyAlert.builder()
                        .rule(matchRule.get())
                        .startMillis(head.getTime())
                        .endMillis(now)
                        .tracers(sortedTracers)
                        .build());
            }
        }
        return true;
    }

    protected Optional<Rule> getMatchRule(String requestUrl) {
        Comparator<String> comparator = matcher.getPatternComparator(requestUrl);
        return rules.keySet().stream()
                .filter(pattern -> matcher.match(pattern, requestUrl))
                .min(comparator)
                .map(e -> rules.get(e));
    }

    private String getRemoteIp(HttpServletRequest request) {
        //查找顺序 代理IP>重定向IP>真是IP头, 如果多次转发只保留第一个
        return Splitter.on(",")
                .omitEmptyStrings()
                .trimResults()
                .splitToList(StringUtils.firstNonBlank(request.getHeader("Cdn-Src-Ip"),
                        request.getHeader("X-Forwarded-For"),
                        request.getHeader("X-Real-IP"),
                        request.getRemoteAddr()))
                .stream()
                .findFirst()
                .orElse(StringUtils.EMPTY);
    }

    @Data
    @Builder
    @AllArgsConstructor
    public static class Tracer {
        private long time;
        private String url;
        private OriginalCaller caller;
        private Long millsToPrevious;
    }

    @Data
    @Builder
    @AllArgsConstructor
    public static class FrequencyAlert {
        private Rule rule;
        private long startMillis;
        private long endMillis;
        private List<Tracer> tracers;
    }


    @Getter
    @Builder
    @AllArgsConstructor
    @ToString
    public static class Rule {
        long millis;
        int permits;
        String path;

        /**
         * 根据约定规则创建Rules
         * eg:
         * /index 10/1  代表index路径规则, 10秒1次
         * /home  5/2   代表home路径规则, 5秒2次
         * /*     10/2
         * note: path不可重复, 优先使用最匹配的路径规则. millis不能为负数, permits不能超过1000
         */
        public static Map<String, Rule> fromExpression(Map<String, String> ruleMap) {
            if (ruleMap == null || ruleMap.isEmpty()) {
                log.warn("frequency rule not found, the interceptor will be silence");
                return Collections.emptyMap();
            }
            return ruleMap.entrySet().stream()
                    .map(e -> {
                        List<String> split = Splitter.on("/")
                                .omitEmptyStrings()
                                .trimResults()
                                .splitToList(e.getValue());

                        Rule build = Rule.builder()
                                .path(e.getKey())
                                .millis(Math.max(TimeUnit.SECONDS.toMillis(Long.parseLong(split.get(0))), 1))
                                .permits(Math.min(Integer.parseInt(split.get(1)), MAX_TRACER_SIZE))
                                .build();
                        return build;

                    }).collect(Collectors.toMap(Rule::getPath, Function.identity(), (ov, nv) -> nv));
        }
    }
}
