package com.blade.mvc.route;

import com.blade.exception.BladeException;
import com.blade.exception.MethodNotAllowedException;
import com.blade.ioc.annotation.Order;
import com.blade.kit.*;
import com.blade.mvc.RouteContext;
import com.blade.mvc.handler.RouteHandler;
import com.blade.mvc.handler.RouteHandler0;
import com.blade.mvc.handler.WebSocketHandler;
import com.blade.mvc.hook.Signature;
import com.blade.mvc.hook.WebHook;
import com.blade.mvc.http.HttpMethod;
import com.blade.mvc.http.Request;
import com.blade.mvc.http.Response;
import com.blade.mvc.route.mapping.FastRouteMappingInfo;
import com.blade.mvc.route.mapping.RegexMapping;
import com.blade.mvc.route.mapping.StaticMapping;
import lombok.NonNull;
import lombok.extern.slf4j.Slf4j;

import java.lang.reflect.Method;
import java.net.URI;
import java.net.URISyntaxException;
import java.util.*;
import java.util.concurrent.ConcurrentHashMap;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import java.util.stream.Collectors;
import java.util.stream.Stream;

import static com.blade.kit.BladeKit.logAddRoute;
import static com.blade.kit.BladeKit.logWebSocket;

/**
 * Default Route Matcher
 *
 * @author <a href="mailto:[email protected]" target="_blank">biezhi</a>
 * @since 1.7.1-release
 */
@Slf4j
public class RouteMatcher {

    private static final Pattern PATH_VARIABLE_PATTERN = Pattern.compile("/(?:([^:/]*):([^/]+))|(\\.\\*)");
    private static final String  METHOD_NAME           = "handle";

    // Storage URL and route
    private Map<String, Route>       routes          = new HashMap<>();
    private Map<String, List<Route>> hooks           = new HashMap<>();
    private List<Route>              middleware      = null;
    private Map<String, Method[]>    classMethodPool = new ConcurrentHashMap<>();
    private Map<Class<?>, Object>    controllerPool  = new ConcurrentHashMap<>();

    private RegexMapping  regexMapping  = new RegexMapping();
    private StaticMapping staticMapping = new StaticMapping();

    /**
     * WebSocket Handlers
     */
    private Map<String, WebSocketHandler> webSockets = new HashMap<>(4);

    @Deprecated
    private Route addRoute(HttpMethod httpMethod, String path, RouteHandler0 handler, String methodName) throws NoSuchMethodException {
        Class<?> handleType = handler.getClass();
        Method   method     = handleType.getMethod(methodName, Request.class, Response.class);
        return addRoute(httpMethod, path, handler, RouteHandler0.class, method);
    }

    private Route addRoute(HttpMethod httpMethod, String path, RouteHandler handler, String methodName) throws NoSuchMethodException {
        Class<?> handleType = handler.getClass();
        Method   method     = handleType.getMethod(methodName, RouteContext.class);
        return addRoute(httpMethod, path, handler, RouteHandler.class, method);
    }

    Route addRoute(Route route) {
        String     path           = route.getPath();
        HttpMethod httpMethod     = route.getHttpMethod();
        Object     controller     = route.getTarget();
        Class<?>   controllerType = route.getTargetType();
        Method     method         = route.getAction();
        return addRoute(httpMethod, path, controller, controllerType, method);
    }

    private Route addRoute(HttpMethod httpMethod, String path, Object controller, Class<?> controllerType, Method method) {

        // [/** | /*]
        path = "*".equals(path) ? "/.*" : path;
        path = path.replace("/**", "/.*").replace("/*", "/.*");

        String key = path + "#" + httpMethod.toString();

        // exist
        if (this.routes.containsKey(key)) {
            log.warn("\tRoute {} -> {} has exist", path, httpMethod.toString());
        }

        Route route = new Route(httpMethod, path, controller, controllerType, method);
        if (BladeKit.isWebHook(httpMethod)) {
            Order order = controllerType.getAnnotation(Order.class);
            if (null != order) {
                route.setSort(order.value());
            }
            if (this.hooks.containsKey(key)) {
                this.hooks.get(key).add(route);
            } else {
                List<Route> empty = new ArrayList<>();
                empty.add(route);
                this.hooks.put(key, empty);
            }
        } else {
            this.routes.put(key, route);
        }
        return route;
    }

    @Deprecated
    public Route addRoute(String path, RouteHandler0 handler, HttpMethod httpMethod) {
        try {
            return addRoute(httpMethod, path, handler, METHOD_NAME);
        } catch (Exception e) {
            log.error("", e);
        }
        return null;
    }

    public Route addRoute(String path, RouteHandler handler, HttpMethod httpMethod) {
        try {
            return addRoute(httpMethod, path, handler, METHOD_NAME);
        } catch (Exception e) {
            log.error("", e);
        }
        return null;
    }

    public void route(String path, Class<?> clazz, String methodName) {
        Assert.notNull(methodName, "Method name not is null");
        HttpMethod httpMethod = HttpMethod.ALL;
        if (methodName.contains(":")) {
            String[] methodArr = methodName.split(":");
            httpMethod = HttpMethod.valueOf(methodArr[0].toUpperCase());
            methodName = methodArr[1];
        }
        this.route(path, clazz, methodName, httpMethod);
    }

    public void route(String path, Class<?> clazz, String methodName, HttpMethod httpMethod) {
        try {
            Assert.notNull(path, "Route path not is null!");
            Assert.notNull(clazz, "Route type not is null!");
            Assert.notNull(methodName, "Method name not is null");
            Assert.notNull(httpMethod, "Request Method not is null");

            Method[] methods = classMethodPool.computeIfAbsent(clazz.getName(), k -> clazz.getMethods());
            if (null == methods) {
                return;
            }
            for (Method method : methods) {
                if (method.getName().equals(methodName)) {
                    Object controller = controllerPool.computeIfAbsent(clazz, k -> ReflectKit.newInstance(clazz));
                    addRoute(httpMethod, path, controller, clazz, method);
                }
            }
        } catch (Exception e) {
            log.error("Add route method error", e);
        }
    }

    public Route lookupRoute(String httpMethod, String path) {
        Route route = staticMapping.findRoute(path, httpMethod);
        if (null != route) {
            return route;
        }
        path = parsePath(path);
        route = staticMapping.findRoute(path, httpMethod);
        if (null != route) {
            return route;
        }
        route = staticMapping.findRoute(path, HttpMethod.ALL.name());
        if (null != route) {
            return route;
        } else {
            if (staticMapping.hasPath(path)) {
                throw new MethodNotAllowedException("[" + httpMethod + "] Method Not Allowed");
            }
        }

        Map<String, String> uriVariables = new LinkedHashMap<>();

        HttpMethod requestMethod = HttpMethod.valueOf(httpMethod);
        try {
            Pattern pattern = regexMapping.findPattern(requestMethod);
            if (null == pattern) {
                pattern = regexMapping.findPattern(HttpMethod.ALL);
                if (null != pattern) {
                    requestMethod = HttpMethod.ALL;
                }
            }
            if (null == pattern) {
                return null;
            }
            Matcher matcher = null;
            if (path != null) {
                matcher = pattern.matcher(path);
            }
            boolean matched = false;
            if (matcher != null) {
                matched = matcher.matches();
            }
            if (!matched) {
                requestMethod = HttpMethod.ALL;
                pattern = regexMapping.findPattern(requestMethod);
                if (null == pattern) {
                    return null;
                }
                if (path != null) {
                    matcher = pattern.matcher(path);
                }
                matched = matcher != null && matcher.matches();
            }
            if (matched) {
                int i;
                for (i = 1; matcher.group(i) == null; i++) ;
                FastRouteMappingInfo mappingInfo = regexMapping.findMappingInfo(requestMethod, i);
                route = mappingInfo.getRoute();

                // find path variable
                String uriVariable;
                int    j = 0;
                while (++i <= matcher.groupCount() && (uriVariable = matcher.group(i)) != null) {
                    String pathVariable = cleanPathVariable(mappingInfo.getVariableNames().get(j++));
                    uriVariables.put(pathVariable, uriVariable);
                }
                route.setPathParams(uriVariables);
                log.trace("lookup path: " + path + " uri variables: " + uriVariables);
            }
            return route;
        } catch (Exception e) {
            throw e;
        }
    }

    private String cleanPathVariable(String pathVariable) {
        if (pathVariable.contains(".")) {
            return pathVariable.substring(0, pathVariable.indexOf('.'));
        }
        return pathVariable;
    }

    public boolean hasBeforeHook() {
        return hooks.values().stream()
                .flatMap(Collection::stream).anyMatch(route -> route.getHttpMethod().equals(HttpMethod.BEFORE));
    }

    public boolean hasAfterHook() {
        return hooks.values().stream()
                .flatMap(Collection::stream).anyMatch(route -> route.getHttpMethod().equals(HttpMethod.AFTER));
    }

    /**
     * Find all in before of the hook
     *
     * @param path request path
     */
    public List<Route> getBefore(String path) {
        String cleanPath = parsePath(path);
        List<Route> collect = hooks.values().stream()
                .flatMap(Collection::stream)
                .sorted(Comparator.comparingInt(Route::getSort))
                .filter(route -> route.getHttpMethod() == HttpMethod.BEFORE && matchesPath(route.getPath(), cleanPath))
                .collect(Collectors.toList());

        this.giveMatch(path, collect);
        return collect;
    }

    /**
     * Find all in after of the hooks
     *
     * @param path request path
     */
    public List<Route> getAfter(String path) {
        String cleanPath = parsePath(path);

        List<Route> afters = hooks.values().stream()
                .flatMap(Collection::stream)
                .sorted(Comparator.comparingInt(Route::getSort))
                .filter(route -> route.getHttpMethod() == HttpMethod.AFTER && matchesPath(route.getPath(), cleanPath))
                .collect(Collectors.toList());

        this.giveMatch(path, afters);
        return afters;
    }

    public List<Route> getMiddleware() {
        return this.middleware;
    }

    /**
     * Sort of path
     *
     * @param uri    request uri
     * @param routes route list
     */
    private void giveMatch(final String uri, List<Route> routes) {
        routes.stream().sorted((o1, o2) -> {
            if (o2.getPath().equals(uri)) {
                return o2.getPath().indexOf(uri);
            }
            return -1;
        });
    }

    /**
     * Matching path
     *
     * @param routePath   route path
     * @param pathToMatch match path
     * @return return match is success
     */
    private boolean matchesPath(String routePath, String pathToMatch) {
        routePath = PathKit.VAR_REGEXP_PATTERN.matcher(routePath).replaceAll(PathKit.VAR_REPLACE);
        return pathToMatch.matches("(?i)" + routePath);
    }

    /**
     * Parse PathKit
     *
     * @param path route path
     * @return return parsed path
     */
    private String parsePath(String path) {
        path = PathKit.fixPath(path);
        try {
            URI uri = new URI(path);
            return uri.getPath();
        } catch (URISyntaxException e) {
            //log.error("parse [" + path + "] error", e);
            return path;
        }
    }

    /**
     * register route to container
     */
    public void register() {
        routes.values().forEach(route -> logAddRoute(log, route));
        hooks.values().stream().flatMap(Collection::stream).forEach(route -> logAddRoute(log, route));

        Stream.of(routes.values(), hooks.values().stream().findAny().orElse(new ArrayList<>()))
                .flatMap(Collection::stream).forEach(this::registerRoute);

        regexMapping.register();

        webSockets.keySet().forEach(path -> logWebSocket(log, path));
    }

    private void registerRoute(Route route) {
        String  path    = parsePath(route.getPath());
        Matcher matcher = null;
        if (path != null) {
            matcher = PATH_VARIABLE_PATTERN.matcher(path);
        }
        boolean      find             = false;
        List<String> uriVariableNames = new ArrayList<>();
        while (matcher != null && matcher.find()) {
            if (!find) {
                find = true;
            }
            String regexName  = matcher.group(1);
            String regexValue = matcher.group(2);

            // just a simple path param
            if (StringKit.isBlank(regexName)) {
                uriVariableNames.add(regexValue);
            } else {
                //regex path param
                uriVariableNames.add(regexName);
            }
        }
        HttpMethod httpMethod = route.getHttpMethod();
        if (find || BladeKit.isWebHook(httpMethod)) {
            regexMapping.addRoute(path, httpMethod, route, uriVariableNames);
        } else {
            staticMapping.addRoute(path, httpMethod, route);
        }
    }

    public Map<String, Route> getRoutes() {
        return routes;
    }

    public Map<String,WebSocketHandler> getWebSockets() {
        return webSockets;
    }

    public WebSocketHandler getWebSocket(String path) {
        return webSockets.get(path);
    }

    public Map<String, List<Route>> getHooks() {
        return hooks;
    }

    public StaticMapping getStaticMapping() {
        return staticMapping;
    }

    public void clear() {
        this.routes.clear();
        this.hooks.clear();
        this.classMethodPool.clear();
        this.controllerPool.clear();
        this.staticMapping.clear();
        this.regexMapping.clear();
    }

    public void initMiddleware(List<WebHook> hooks) {
        this.middleware = hooks.stream().map(webHook -> {
            Method method = ReflectKit.getMethod(WebHook.class, "before", Signature.class);
            return new Route(HttpMethod.BEFORE, "/.*", webHook, WebHook.class, method);
        }).collect(Collectors.toList());
    }

    public RouteMatcher addWebSocket(@NonNull String path,@NonNull WebSocketHandler handler) {
        if (null != this.webSockets.get(path)) {
            throw new BladeException(500, "Duplicate WebSocket path [" + path + "]");
        }
        this.webSockets.put(path,handler);
        return this;
    }

}