package ru.yandex.webmaster3.viewer.http.concurrency;

import java.util.Timer;
import java.util.concurrent.BlockingQueue;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.LinkedBlockingQueue;
import java.util.concurrent.TimeoutException;
import java.util.concurrent.atomic.AtomicBoolean;
import java.util.function.Supplier;

import org.joda.time.Duration;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Required;

import ru.yandex.webmaster3.core.WebmasterException;
import ru.yandex.webmaster3.core.concurrency.AsyncCtx;
import ru.yandex.webmaster3.core.concurrency.AsyncTask;
import ru.yandex.webmaster3.core.http.Action;
import ru.yandex.webmaster3.core.http.ActionRequest;
import ru.yandex.webmaster3.core.http.ActionResponse;
import ru.yandex.webmaster3.core.http.ActionRouter;
import ru.yandex.webmaster3.core.http.RequestTracer;
import ru.yandex.webmaster3.core.tracer.YdbTracer;
import ru.yandex.webmaster3.core.util.ContextTracker;

/**
 * Для action'ов, которые нужно распараллелить по доступу к БД. Реализовано минимальное подмножество фич ForkJoin пула
 * Как пользоваться - наследуемся, пишем код. Если хочется вызвать что-то асинхронно - делаем AsyncTask<...> task = ctx.fork(лямбда). Когда хотим
 * забрать результат - делаем task.join()
 * При необходимости fork можно вызывать изнутри другого форка - это норм, даже хорошо.
 * Реализация гарантирует корректный перенос MDC-контекста для логирования всех действий как действий Action'а, и перенос
 * RequestTracer-контекста, для сбора статистики про запросы к БД.
 * Ничего не гарантируется про то, что будет, если не заджойнить форкнутую таску - лучше так не делать.
 *
 * Реализация экспериментальная, поэтому после использования нужно обязательно смотреть в приборы:
 * 1. Тайминги остальных action'ов, использующих AsyncAction
 * 2. Загруженность пула https://solomon.yandex-team.ru/?project=webmaster&cluster=webmaster_production&service=viewer&l.indicator=async_pool_stat&l.host=wmc-back*:33585&l.pool_cpu=*&graph=auto&checks=-wmc-back01g.search.yandex.net%3A33585%2C%20total%3Bwmc-back02e.search.yandex.net%3A33585%2C%20total%3Bwmc-back02g.search.yandex.net%3A33585%2C%20total%3Bwmc-back03e.search.yandex.net%3A33585%2C%20total%3Bwmc-back03g.search.yandex.net%3A33585%2C%20total%3Bwmc-back04e.search.yandex.net%3A33585%2C%20total%3Bwmc-back04g.search.yandex.net%3A33585%2C%20total%3Bwmc-back05g.search.yandex.net%3A33585%2C%20total&stack=false
 * Если usage приближается к total, то производительность может деградировать
 *
 * Факт использования FJP внизу - деталь реализации. Просто заводить неограниченное количество тредов нельзя, чтобы не
 * положить БД, а обычный ExecutorService (чисто гипотетически) будет давать неконтролируемый рост latency для медленных
 * ручек, если с бд не очень хорошо. FJP же (опять же, гипотетически), должен деградировать в последовательное исполнение
 * кода каждого action'а при большой нагрузке
 *
 * @author avhaliullin
 */
public abstract class AsyncAction<Req extends ActionRequest, Res extends ActionResponse> extends Action<Req, Res> {
    private static final Logger log = LoggerFactory.getLogger(AsyncAction.class);

    private static final ContextTracker CONTEXT_TRACKER = ContextTracker.aggregate(ContextTracker.MDC_TRACKER, RequestTracer.CONTEXT_TRACKER, YdbTracer.CONTEXT_TRACKER);
    private static final Timer ASYNC_ACTION_TIMER = new Timer("AsyncActionTimer", true);

    private Duration timeout = Duration.standardSeconds(15);
    private AsyncPool asyncPool;

    @Override
    public Res process(Req request) throws WebmasterException {
        AsyncCtxImpl ctx = new AsyncCtxImpl();
        AsyncTask<Res> resultTask = null;
        try {
            resultTask = asyncPool.submit(() -> processAsyncInternal(ctx, request), CONTEXT_TRACKER);
            ctx.registerTask(resultTask);
            return resultTask.get(timeout);
        } catch (InterruptedException e) {
            Thread.currentThread().interrupt();
            throw new RuntimeException(e);
        } catch (ExecutionException e) {
            throw new RuntimeException(e.getCause());
        } catch (TimeoutException e) {
            throw new RuntimeException(e);
        } finally {
            ctx.shutdown();
        }
    }

    private Res processAsyncInternal(AsyncCtxImpl ctx, Req request) {
        // timer for killing thread
        ActionRouter.ActionRequestReaperTask timerTask = new ActionRouter.ActionRequestReaperTask(Thread.currentThread());
        ASYNC_ACTION_TIMER.schedule(timerTask, timeout.getMillis());
        try {
            return processAsync(ctx, request);
        } finally {
            timerTask.cancel();
        }
    }

    protected abstract Res processAsync(AsyncCtx ctx, Req request);

    private class AsyncCtxImpl implements AsyncCtx {
        private final BlockingQueue<AsyncTask<?>> tasks = new LinkedBlockingQueue<>();
        private final AtomicBoolean running = new AtomicBoolean(true);

        private void registerTask(AsyncTask<?> fjTask) {
            tasks.add(fjTask);
            boolean _running = running.get();
            if (!_running) {
                fjTask.cancel();
                throw new IllegalStateException("Context is shut down");
            }
        }

        @Override
        public <T> AsyncTask<T> fork(Supplier<T> task) {
            AsyncTask<T> asyncTask = asyncPool.fork(task, CONTEXT_TRACKER);
            registerTask(asyncTask);
            return asyncTask;
        }

        @Override
        public AsyncTask<Void> fork(Runnable task) {
            return fork(() -> {
                task.run();
                return null;
            });
        }

        private void shutdown() {
            if (running.compareAndSet(true, false)) {
                AsyncTask<?> task;
                while ((task = tasks.poll()) != null) {
                    task.cancel();
                }
            }
        }
    }

    @Required
    public void setAsyncPool(AsyncPool asyncPool) {
        this.asyncPool = asyncPool;
    }

    public void setTimeout(Duration timeout) {
        this.timeout = timeout;
    }
}
