/*-
 * =========================LICENSE_START==================================
 * heimdall-gateway
 * ========================================================================
 * Copyright (C) 2018 Conductor Tecnologia SA
 * ========================================================================
 * Licensed under the Apache License, Version 2.0 (the "License")
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *      http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 * ==========================LICENSE_END===================================
 */
package br.com.conductor.heimdall.gateway.filter;

import br.com.conductor.heimdall.core.enums.HttpMethod;
import br.com.conductor.heimdall.core.trace.FilterDetail;
import br.com.conductor.heimdall.core.trace.TraceContextHolder;
import br.com.conductor.heimdall.core.util.Constants;
import br.com.conductor.heimdall.core.util.ConstantsPath;
import br.com.conductor.heimdall.core.util.UrlUtil;
import br.com.conductor.heimdall.gateway.router.Credential;
import br.com.conductor.heimdall.gateway.router.CredentialRepository;
import br.com.conductor.heimdall.gateway.router.EnvironmentInfo;
import br.com.conductor.heimdall.gateway.router.EnvironmentInfoRepository;
import br.com.conductor.heimdall.gateway.util.RequestHelper;
import br.com.conductor.heimdall.gateway.zuul.route.HeimdallRoute;
import br.com.conductor.heimdall.gateway.zuul.route.ProxyRouteLocator;
import com.netflix.zuul.context.RequestContext;
import lombok.extern.slf4j.Slf4j;
import org.springframework.cloud.netflix.zuul.filters.ProxyRequestHelper;
import org.springframework.cloud.netflix.zuul.filters.Route;
import org.springframework.cloud.netflix.zuul.filters.ZuulProperties;
import org.springframework.cloud.netflix.zuul.filters.ZuulProperties.ZuulRoute;
import org.springframework.cloud.netflix.zuul.filters.pre.PreDecorationFilter;
import org.springframework.cloud.netflix.zuul.util.RequestUtils;
import org.springframework.http.HttpHeaders;
import org.springframework.http.HttpStatus;
import org.springframework.http.MediaType;
import org.springframework.util.AntPathMatcher;
import org.springframework.util.PathMatcher;
import org.springframework.util.StringUtils;
import org.springframework.web.util.UrlPathHelper;

import javax.servlet.http.HttpServletRequest;
import java.util.List;
import java.util.Map.Entry;
import java.util.Objects;
import java.util.Optional;

import static br.com.conductor.heimdall.core.util.Constants.INTERRUPT;
import static br.com.conductor.heimdall.gateway.util.ConstantsContext.*;
import static org.springframework.cloud.netflix.zuul.filters.support.FilterConstants.*;

/**
 * Extends the {@link PreDecorationFilter}.
 *
 * @author Marcos Filho
 * @author Marcelo Aguiar Rodrigues
 * @author <a href="https://dijalmasilva.github.io" target="_blank">Dijalma Silva</a>
 */
@Slf4j
public class HeimdallDecorationFilter extends PreDecorationFilter {

    private ProxyRouteLocator routeLocator;

    private String dispatcherServletPath;

    private String zuulServletPath;

    private ZuulProperties properties;

    private UrlPathHelper urlPathHelper = new UrlPathHelper();

    private ProxyRequestHelper proxyRequestHelper;

    private PathMatcher pathMatcher = new AntPathMatcher();

    private RequestHelper requestHelper;

    private FilterDetail detail = new FilterDetail();
    
    private CredentialRepository credentialRepository;
    
    private EnvironmentInfoRepository environmentInfoRepository;

    public HeimdallDecorationFilter(ProxyRouteLocator routeLocator, String dispatcherServletPath, ZuulProperties properties, ProxyRequestHelper proxyRequestHelper, RequestHelper requestHelper, CredentialRepository credentialRepository, EnvironmentInfoRepository environmentInfoRepository) {

        super(routeLocator, dispatcherServletPath, properties, proxyRequestHelper);
        this.routeLocator = routeLocator;
        this.properties = properties;
        this.urlPathHelper.setRemoveSemicolonContent(properties.isRemoveSemicolonContent());
        this.dispatcherServletPath = dispatcherServletPath;
        this.proxyRequestHelper = proxyRequestHelper;
        this.zuulServletPath = properties.getServletPath();
        this.requestHelper = requestHelper;
        this.credentialRepository = credentialRepository;
        this.environmentInfoRepository = environmentInfoRepository;
    }

    @Override
    public boolean shouldFilter() {

        long startTime = System.currentTimeMillis();

        boolean should = super.shouldFilter();

        long endTime = System.currentTimeMillis();
        long duration = (endTime - startTime);

        detail.setTimeInMillisShould(duration);
        return should;
    }

    @Override
    public Object run() {

        long startTime = System.currentTimeMillis();
        try {
            process();
            detail.setStatus(Constants.SUCCESS);
        } catch (Exception e) {
            detail.setStatus(Constants.FAILED);
            throw e;
        } finally {
            long endTime = System.currentTimeMillis();

            long duration = (endTime - startTime);

            detail.setTimeInMillisRun(duration);
            TraceContextHolder.getInstance().getActualTrace().addFilter(this.getClass().getSimpleName(), detail);
        }

        return null;

    }

    protected void process() {

        RequestContext ctx = RequestContext.getCurrentContext();
        final String requestURI = getPathWithoutStripSuffix(ctx.getRequest());

        if (pathMatcher.match(ConstantsPath.PATH_MANAGER_PATTERN, requestURI) || "/error".equals(requestURI)) {
            ctx.set(FORWARD_TO_KEY, requestURI);
            return;
        }

        final String method = ctx.getRequest().getMethod().toUpperCase();
        HeimdallRoute heimdallRoute = getMatchingHeimdallRoute(requestURI, method, ctx);

        if (heimdallRoute != null) {

            if (heimdallRoute.isMethodNotAllowed()) {
                ctx.setSendZuulResponse(false);
                ctx.setResponseStatusCode(HttpStatus.METHOD_NOT_ALLOWED.value());
                ctx.setResponseBody(HttpStatus.METHOD_NOT_ALLOWED.getReasonPhrase());
                return;
            }

            if (heimdallRoute.getRoute() == null || heimdallRoute.getRoute().getLocation() == null) {
                log.warn("Environment not configured for this location: {} and inbound: {}", ctx.getRequest().getRequestURL(), requestURI);
                ctx.setSendZuulResponse(false);
                ctx.setResponseStatusCode(HttpStatus.FORBIDDEN.value());
                ctx.setResponseBody("Environment not configured for this inbound");
                ctx.getResponse().setContentType(MediaType.TEXT_PLAIN_VALUE);
                TraceContextHolder.getInstance().getActualTrace().setRequest(requestHelper.dumpRequest());
                return;
            }

            String location = heimdallRoute.getRoute().getLocation();
            ctx.put(REQUEST_URI_KEY, heimdallRoute.getRoute().getPath());
            ctx.put(PROXY_KEY, heimdallRoute.getRoute().getId());
            if (!heimdallRoute.getRoute().isCustomSensitiveHeaders()) {
                this.proxyRequestHelper.addIgnoredHeaders(this.properties.getSensitiveHeaders().toArray(new String[0]));
            } else {
                this.proxyRequestHelper.addIgnoredHeaders(heimdallRoute.getRoute().getSensitiveHeaders().toArray(new String[0]));
            }

            if (heimdallRoute.getRoute().getRetryable() != null) {
                ctx.put(RETRYABLE_KEY, heimdallRoute.getRoute().getRetryable());
            }

            if (location.startsWith(HTTP_SCHEME + ":") || location.startsWith(HTTPS_SCHEME + ":")) {
                ctx.setRouteHost(UrlUtil.getUrl(location));
                ctx.addOriginResponseHeader(SERVICE_HEADER, location);
            } else if (location.startsWith(FORWARD_LOCATION_PREFIX)) {
                ctx.set(FORWARD_TO_KEY, StringUtils.cleanPath(location.substring(FORWARD_LOCATION_PREFIX.length()) + heimdallRoute.getRoute().getPath()));
                ctx.setRouteHost(null);
                return;
            } else {
                // set serviceId for use in filters.route.RibbonRequest
                ctx.set(SERVICE_ID_KEY, location);
                ctx.setRouteHost(null);
                ctx.addOriginResponseHeader(SERVICE_ID_HEADER, location);
            }
            if (this.properties.isAddProxyHeaders()) {
                addProxyHeaders(ctx);
                String xforwardedfor = ctx.getRequest().getHeader(X_FORWARDED_FOR_HEADER);
                String remoteAddr = ctx.getRequest().getRemoteAddr();
                if (xforwardedfor == null) {
                    xforwardedfor = remoteAddr;
                } else if (!xforwardedfor.contains(remoteAddr)) { // Prevent duplicates
                    xforwardedfor += ", " + remoteAddr;
                }
                ctx.addZuulRequestHeader(X_FORWARDED_FOR_HEADER, xforwardedfor);
            }
            if (this.properties.isAddHostHeader()) {
                ctx.addZuulRequestHeader(HttpHeaders.HOST, toHostHeader(ctx.getRequest()));
            }
        } else {
            log.warn("No route found for uri: " + requestURI);
            ctx.setSendZuulResponse(false);
            ctx.setResponseStatusCode(HttpStatus.NOT_FOUND.value());
            ctx.setResponseBody(HttpStatus.NOT_FOUND.getReasonPhrase());
            ctx.getResponse().setContentType(MediaType.TEXT_PLAIN_VALUE);
            TraceContextHolder.getInstance().getActualTrace().setRequest(requestHelper.dumpRequest());
        }
    }

    protected String adjustPath(final String path) {

        String adjustedPath = path;

        if (RequestUtils.isDispatcherServletRequest() && StringUtils.hasText(this.dispatcherServletPath)) {
            if (!this.dispatcherServletPath.equals("/")) {
                adjustedPath = path.substring(this.dispatcherServletPath.length());
                log.debug("Stripped dispatcherServletPath");
            }
        } else if (RequestUtils.isZuulServletRequest()) {
            if (StringUtils.hasText(this.zuulServletPath) && !this.zuulServletPath.equals("/")) {
                adjustedPath = path.substring(this.zuulServletPath.length());
                log.debug("Stripped zuulServletPath");
            }
        } else {
            // do nothing
        }

        log.debug("adjustedPath=" + adjustedPath);
        return adjustedPath;
    }

    protected HeimdallRoute getMatchingHeimdallRoute(String requestURI, String method, RequestContext ctx) {

        boolean auxMatch = false;
        for (Entry<String, ZuulRoute> entry : routeLocator.getAtomicRoutes().get().entrySet()) {
            if (entry.getKey() != null) {
                String pattern = entry.getKey();
                if (this.pathMatcher.match(pattern, requestURI)) {

                    auxMatch = true;
                    List<Credential> credentials = credentialRepository.findByPattern(pattern);
                    Credential credential = null;
                    if (Objects.nonNull(credentials) && !credentials.isEmpty()) {

                        if (method.equals(HttpMethod.OPTIONS.name())) {
                            Optional<Credential> first = credentials.stream().findFirst();
                            if (first.get().isCors()) {
                            	credential = first.get();
                            }
                        }

                        if (Objects.isNull(credential)) {
                        	credential = credentials.stream()
                                    .filter(o -> o.getMethod().equals(HttpMethod.ALL.name()) || method.equals(o.getMethod().toUpperCase()))
                                    .findFirst().orElse(null);
                        }
                    }

                    if (credential != null) {
                        ZuulRoute zuulRoute = entry.getValue();

                        String basePath = credential.getApiBasePath();
                        requestURI = org.apache.commons.lang.StringUtils.removeStart(requestURI, basePath);

                        ctx.put(PATTERN, org.apache.commons.lang.StringUtils.removeStart(pattern, basePath));
                        ctx.put(API_NAME, credential.getApiName());
                        ctx.put(API_ID, credential.getApiId());
                        ctx.put(RESOURCE_ID, credential.getResourceId());
                        ctx.put(OPERATION_ID, credential.getOperationId());
                        ctx.put(OPERATION_PATH, credential.getOperationPath());

                        String host = ctx.getRequest().getHeader("Host");

                        EnvironmentInfo environment;
                        String location = null;
                        if (host != null && !host.isEmpty()) {
                            environment = environmentInfoRepository.findByApiIdAndEnvironmentInboundURL(credential.getApiId(), host.toLowerCase());
                        } else {
                            environment = environmentInfoRepository.findByApiIdAndEnvironmentInboundURL(credential.getApiId(), ctx.getRequest().getRequestURL().toString().toLowerCase());
                        }

                        if (environment != null) {
                            location = environment.getOutboundURL();
                            ctx.put(ENVIRONMENT_VARIABLES, environment.getVariables());
                        }

                        Route route = new Route(zuulRoute.getId(),
                                requestURI,
                                location,
                                "",
                                zuulRoute.getRetryable() != null ? zuulRoute.getRetryable() : false,
                                zuulRoute.isCustomSensitiveHeaders() ? zuulRoute.getSensitiveHeaders() : null);

                        TraceContextHolder traceContextHolder = TraceContextHolder.getInstance();

                        traceContextHolder.getActualTrace().setApiId(credential.getApiId());
                        traceContextHolder.getActualTrace().setApiName(credential.getApiName());
                        traceContextHolder.getActualTrace().setResourceId(credential.getResourceId());
                        traceContextHolder.getActualTrace().setOperationId(credential.getOperationId());

                        return new HeimdallRoute(pattern, route, false);
                    } else {

                        ctx.put(INTERRUPT, true);
                    }
                }
            }
        }

        if (auxMatch) {
            return new HeimdallRoute().methodNotAllowed();
        }

        return null;
    }

    protected String getPathWithoutStripSuffix(HttpServletRequest request) {

        String URI = this.urlPathHelper.getPathWithinApplication(request);

        if (URI.endsWith(ConstantsPath.PATH_ROOT)) {
            URI = org.apache.commons.lang.StringUtils.removeEnd(URI, ConstantsPath.PATH_ROOT);
        }

        URI = adjustPath(URI);

        return URI;
    }

    protected void addProxyHeaders(RequestContext ctx) {

        HttpServletRequest request = ctx.getRequest();
        String host = toHostHeader(request);
        String port = String.valueOf(request.getServerPort());
        String proto = request.getScheme();
        if (hasHeader(request, X_FORWARDED_HOST_HEADER)) {
            host = request.getHeader(X_FORWARDED_HOST_HEADER) + "," + host;
        }
        if (!hasHeader(request, X_FORWARDED_PORT_HEADER)) {
            if (hasHeader(request, X_FORWARDED_PROTO_HEADER)) {
                StringBuilder builder = new StringBuilder();
                for (String previous : StringUtils.commaDelimitedListToStringArray(request.getHeader(X_FORWARDED_PROTO_HEADER))) {
                    if (builder.length() > 0) {
                        builder.append(",");
                    }
                    builder.append(HTTPS_SCHEME.equals(previous) ? HTTPS_PORT : HTTP_PORT);
                }
                builder.append(",").append(port);
                port = builder.toString();
            }
        } else {
            port = request.getHeader(X_FORWARDED_PORT_HEADER) + "," + port;
        }
        if (hasHeader(request, X_FORWARDED_PROTO_HEADER)) {
            proto = request.getHeader(X_FORWARDED_PROTO_HEADER) + "," + proto;
        }
        ctx.addZuulRequestHeader(X_FORWARDED_HOST_HEADER, host);
        ctx.addZuulRequestHeader(X_FORWARDED_PORT_HEADER, port);
        ctx.addZuulRequestHeader(X_FORWARDED_PROTO_HEADER, proto);
    }

    protected boolean hasHeader(HttpServletRequest request, String name) {

        return StringUtils.hasLength(request.getHeader(name));
    }

    protected String toHostHeader(HttpServletRequest request) {

        int port = request.getServerPort();
        if ((port == HTTP_PORT && HTTP_SCHEME.equals(request.getScheme())) || (port == HTTPS_PORT && HTTPS_SCHEME.equals(request.getScheme()))) {
            return request.getServerName();
        } else {
            return request.getServerName() + ":" + port;
        }
    }
}