package ru.yandex.qe.dispenser.ws.intercept;

import java.io.IOException;
import java.util.Arrays;
import java.util.List;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicInteger;

import javax.servlet.Filter;
import javax.servlet.FilterChain;
import javax.servlet.FilterConfig;
import javax.servlet.ServletException;
import javax.servlet.ServletRequest;
import javax.servlet.ServletResponse;
import javax.servlet.http.HttpServletRequest;
import javax.ws.rs.HttpMethod;

import com.google.common.base.Stopwatch;
import org.jetbrains.annotations.NotNull;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.transaction.TransactionException;

import ru.yandex.qe.dispenser.domain.request.ConcurrentOperationException;

//000 - used for filters priority
//@WebFilter(filterName = "000_transactionWrapper", urlPatterns = "/*", asyncSupported = true)
public final class TransactionFilter implements Filter {
    private static final Logger LOG = LoggerFactory.getLogger(TransactionFilter.class);
    private static final String[] TRANSACTION_PATHS = {
            "/reinitialize", "/entity-ownerships", "/entities"
    };
    private static final List<String> NO_TRANSACTION_PATHS = List.of("/provider-allocation/", "/request-allocation",
            "/provide-to-account/");
    private static final long MAX_LOCK_TIMEOUT = 500000L;
    private static final AvgCounter counter = new AvgCounter(100);

    @Override
    public void init(@NotNull final FilterConfig config) {
    }

    @Override
    public void doFilter(@NotNull final ServletRequest req,
                         @NotNull final ServletResponse resp,
                         @NotNull final FilterChain chain) throws IOException, ServletException {
        if (needTransaction((HttpServletRequest) req)) {
            doFilterInTransaction(req, resp, chain);
        } else {
            chain.doFilter(req, resp);
        }
    }

    public boolean needTransaction(@NotNull final HttpServletRequest req) {
        boolean transaction = Arrays.stream(TRANSACTION_PATHS).anyMatch(req.getRequestURL().toString()::contains);
        transaction |= !HttpMethod.GET.equals(req.getMethod());
        transaction &= NO_TRANSACTION_PATHS.stream().noneMatch(req.getRequestURL().toString()::contains);
        return transaction;
    }

    public void doFilterInTransaction(@NotNull final ServletRequest req,
                                      @NotNull final ServletResponse resp,
                                      @NotNull final FilterChain chain) throws IOException, ServletException {
        final String path = ((HttpServletRequest) req).getPathInfo();
        final Stopwatch filterStopwatch = Stopwatch.createStarted();
        int attempt = 0;
        do {
            try {
                doSleep(attempt++, path);
                final Stopwatch stopwatch = Stopwatch.createStarted();
                TransactionWrapper.INSTANCE.execute(() -> chain.doFilter(req, resp));
                counter.apply(path == null ? "" : path, stopwatch.elapsed(TimeUnit.MILLISECONDS));
                return;
            } catch (TransactionException e) {
                // TODO
                throw e;
            } catch (ConcurrentOperationException ignored) {
                LOG.info("lock is acquired allready, go to the next circle!");
            } catch (RuntimeException e) {
                //noinspection ChainOfInstanceofChecks
                if (e.getCause() instanceof IOException) {
                    throw (IOException) e.getCause();
                }
                if (e.getCause() instanceof ServletException) {
                    throw (ServletException) e.getCause();
                }
                throw e;
            }

        } while (filterStopwatch.elapsed(TimeUnit.MILLISECONDS) < MAX_LOCK_TIMEOUT);

        throw new ConcurrentOperationException("Didn't get lock for reqId in '" + MAX_LOCK_TIMEOUT + "' ms.");
    }

    @Override
    public void destroy() {
    }

    private void doSleep(final int attempt, final String path) {
        if (attempt == 0) {
            return;
        }
        final long avg = 50L;
        //Запросы от нирваны вызывают смещение, так что будем ждать константу
        final double coeff = getTimeoutCoeff(attempt);
        final long time = (long) Math.max(10, Math.min(coeff * avg, 100L));
        LOG.warn("going to sleep for {} ms. avg: {} attempt: {} coeff: {}", time, avg, attempt, coeff);
        try {
            Thread.sleep(time);
        } catch (InterruptedException e) {
            LOG.error("Sleep failed", e);
        }
    }

    private double getTimeoutCoeff(final int attempt) {
        if (attempt == 0) {
            return 0;
        }
        return Math.max(1.1 / attempt, 0.1);

    }

    static class AvgCounter {
        private ConcurrentHashMap<String, long[]> times = new ConcurrentHashMap<>();
        private AtomicInteger index = new AtomicInteger();
        private final int len;

        AvgCounter(final int len) {
            this.len = len;
        }

        public void apply(final String path, final long l) {
            if (!times.containsKey(path)) {
                times.putIfAbsent(path, new long[len]);
            }
            times.get(path)[index.getAndIncrement() % len] = l;
        }

        public long avg(final String path) {
            if (!times.containsKey(path)) {
                return 50L;
            }
            return (long) Arrays.stream(times.get(path)).filter(l -> l != 0).summaryStatistics().getAverage() + 1;
        }
    }
}
