package com.acs.waveserver.core;


import com.acs.waveserver.core.constants.ResponseStatus;
import com.acs.waveserver.core.functional.ErrorCodeHandler;
import com.acs.waveserver.core.functional.ExceptionHandler;
import com.acs.waveserver.core.functional.RequestFilter;
import com.acs.waveserver.core.functional.RequestHandler;
import com.acs.waveserver.core.utils.CheckUtils;
import com.acs.waveserver.core.utils.LogLevel;
import com.acs.waveserver.core.utils.StopWatch;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import java.util.Collections;
import java.util.List;
import java.util.Map;
import java.util.Optional;
import java.util.function.Supplier;
import java.util.stream.Collectors;

public class Router {

    private final Logger log = LoggerFactory.getLogger(getClass());

    public final List<Route<RequestFilter>> filters;
    public final List<Route<RequestHandler>> handlers;
    public final Map<ResponseStatus, ErrorCodeHandler> errorCodeHandlers;
    public final ErrorCodeHandler defaultErrorCodeHandler;
    public final ExceptionHandler exceptionHandler;

    Router(List<Route<RequestFilter>> filters, List<Route<RequestHandler>> handlers, Map<ResponseStatus, ErrorCodeHandler> errorCodeHandlers, ErrorCodeHandler defaultErrorCodeHandler, ExceptionHandler exceptionHandler) {
        CheckUtils.checkNull("filters", filters);
        CheckUtils.checkNull("handlers", handlers);
        CheckUtils.checkNull("errorCodeHandlers", errorCodeHandlers);
        CheckUtils.checkNull("defaultErrorCodeHandler", defaultErrorCodeHandler);
        CheckUtils.checkNull("exceptionHandler", exceptionHandler);

        this.filters = Collections.unmodifiableList(filters);
        this.handlers = Collections.unmodifiableList(handlers);
        this.errorCodeHandlers = Collections.unmodifiableMap(errorCodeHandlers);
        this.defaultErrorCodeHandler = defaultErrorCodeHandler;
        this.exceptionHandler = exceptionHandler;
    }

    public HTTPResponse process(HTTPRequest httpRequest) {
        HTTPResponseBuilder responseBuilder = new HTTPResponseBuilder(httpRequest);
        log.debug("Request {} {}", httpRequest.method, httpRequest.uri);
        StopWatch stopWatch = new StopWatch().start();
        try {
            return processFilters(httpRequest, responseBuilder)
                    .orElse(processHandler(httpRequest, responseBuilder)
                            .orElse(getErrorResponse(httpRequest, responseBuilder, ResponseStatus.NOT_FOUND)));
        } catch (Exception e) {
            return exceptionHandler.handle(httpRequest, responseBuilder, e);
        } finally {
            stopWatch.printElapseTime("Request " + httpRequest.method + " " + httpRequest.uri, log, LogLevel.DEBUG);
        }
    }

    private Optional<Route<RequestHandler>> getRequestHandler(HTTPRequest httpRequest) {
        return handlers.stream()
                .filter(route -> route.canApply(httpRequest))
                .map(Optional::of)
                .reduce(Optional.empty(), (a, b) -> b);
    }

    private HTTPResponse getErrorResponse(HTTPRequest httpRequest, HTTPResponseBuilder responseBuilder, ResponseStatus responseStatus) {
        ErrorCodeHandler errorCodeHandler = errorCodeHandlers.getOrDefault(responseStatus, defaultErrorCodeHandler);
        return errorCodeHandler.handle(httpRequest, responseBuilder, responseStatus);
    }

    private Optional<HTTPResponse> processFilters(HTTPRequest httpRequest, HTTPResponseBuilder responseBuilder) {
        List<Route<RequestFilter>> routes = filters.stream()
                .filter(route -> route.canApply(httpRequest))
                .collect(Collectors.toList());

        return getSupplier(httpRequest, responseBuilder, routes, 0).get();
    }

    private Supplier<Optional<HTTPResponse>> getSupplier(HTTPRequest httpRequest, HTTPResponseBuilder responseBuilder, List<Route<RequestFilter>> routes, int index) {
        if (index < routes.size()) {
            Route<RequestFilter> route = routes.get(index);
            return () -> {
                log.debug("Filter {} {}", route.method, route.uri);
                StopWatch stopWatch = new StopWatch().start();
                try {
                    return route.handler.handle(httpRequest.ofRoute(route), responseBuilder, getSupplier(httpRequest, responseBuilder, routes, index + 1));
                } finally {
                    stopWatch.printElapseTime("Filter " + route.method + " " + route.uri, log, LogLevel.DEBUG);
                }
            };
        } else {
            return Optional::empty;
        }
    }


    private Optional<HTTPResponse> processHandler(HTTPRequest httpRequest, HTTPResponseBuilder responseBuilder) {
        Optional<Route<RequestHandler>> handleRoute = getRequestHandler(httpRequest);

        return handleRoute.flatMap(route -> {
            log.debug("Handler {} {}", route.method, route.uri);
            StopWatch stopWatch = new StopWatch().start();
            try {
                return route.handler.handle(httpRequest.ofRoute(route), responseBuilder);
            } finally {
                stopWatch.printElapseTime("Handler " + route.method + " " + route.uri, log, LogLevel.DEBUG);
            }
        });
    }
}

@@ -2756,290 +2756,8 @@
  %7D%0A%0A
-    private Optional%3CRoute%3CRequestHandler%3E%3E getRequestHandler(HTTPRequest httpRequest) %7B%0A        return handlers.stream()%0A                .filter(route -%3E route.canApply(httpRequest))%0A                .map(Optional::of)%0A                .reduce(Optional.empty(), (a, b) -%3E b);%0A    %7D%0A%0A
     
@@ -3777,21 +3777,21 @@
     log.
-debug
+trace
 (%22Filter
@@ -4190,37 +4190,37 @@
 , log, LogLevel.
-DEBUG
+TRACE
 );%0A             
@@ -4573,21 +4573,21 @@
     log.
-debug
+trace
 (%22Handle
@@ -4906,37 +4906,37 @@
 , log, LogLevel.
-DEBUG
+TRACE
 );%0A            %7D
@@ -4954,10 +4954,291 @@
 ;%0A    %7D%0A
+%0A    private Optional%3CRoute%3CRequestHandler%3E%3E getRequestHandler(HTTPRequest httpRequest) %7B%0A        return handlers.stream()%0A                .filter(route -%3E route.canApply(httpRequest))%0A                .map(Optional::of)%0A                .reduce(Optional.empty(), (a, b) -%3E b);%0A    
 %7D%0A
+%7D

Put some order