/*
 * Copyright (c) 2019, WSO2 Inc. (http://www.wso2.org) All Rights Reserved.
 *
 * WSO2 Inc. licenses this file to you 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.
 */

package org.apache.synapse.commons.emulator.http.dsl.dto.consumer;

import io.netty.handler.codec.http.HttpMethod;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.apache.synapse.commons.emulator.http.dsl.dto.Header;
import org.apache.synapse.commons.emulator.http.consumer.HttpRequestContext;
import org.apache.synapse.commons.emulator.http.dsl.dto.QueryParameter;

import java.util.ArrayList;
import java.util.List;
import java.util.Map;
import java.util.regex.Pattern;

public class IncomingMessage {

    private static final Log logger = LogFactory.getLog(IncomingMessage.class.getName());

    private static IncomingMessage incoming;
    private HttpMethod method;
    private String path;
    private String body;
    private String context;
    private Pattern pathRegex;
    private Header header;
    private List<QueryParameter> queryParameters;


    private static IncomingMessage getInstance() {
        incoming = new IncomingMessage();
        return incoming;
    }

    public static IncomingMessage request() {
        return getInstance();
    }

    public IncomingMessage withMethod(HttpMethod method) {
        this.method = method;
        return this;
    }

    public IncomingMessage withPath(String path) {
        this.path = path;
        return this;
    }

    public IncomingMessage withBody(String body) {
        this.body = body;
        return this;
    }

    public IncomingMessage withHeader(String name, String value) {
        header = new Header(name, value);
        return this;
    }

    public IncomingMessage withQueryParameter(String name, String value) {
        if (queryParameters == null) {
            this.queryParameters = new ArrayList<>();
        }
        this.queryParameters.add(new QueryParameter(name, value));
        return this;
    }

    public boolean isMatch(HttpRequestContext requestContext) {
        if (isContextMatch(requestContext) && isHttpMethodMatch(requestContext) &&
                isRequestContentMatch(requestContext) &&
                isHeadersMatch(requestContext) && isQueryParameterMatch(requestContext)) {
            return true;
        }
        return false;
    }

    public void buildPathRegex(String context) {
        this.context = context;
        String regex = buildRegex(context, path);
        this.pathRegex = Pattern.compile(regex);
    }

    private boolean isContextMatch(HttpRequestContext requestContext) {
        this.context = extractContext(requestContext.getUri());
        return pathRegex.matcher(context).find();
    }


    private boolean isHttpMethodMatch(HttpRequestContext requestContext) {
        if (method == null) {
            return true;
        }

        if (method.equals(requestContext.getHttpMethod())) {
            return true;
        }

        logger.error("Incoming request's service method '" + requestContext.getHttpMethod()
                + "' is not matched with the expected service method '" + method + "' for the mock service path "
                + context.substring(0, context.length() - 1) + path);
        return false;
    }

    private boolean isRequestContentMatch(HttpRequestContext requestContext) {
        if (body == null || body.isEmpty()) {
            return true;
        }

        if (body.equalsIgnoreCase(requestContext.getRequestBody())) {
            return true;
        }

        String servicePath = context.substring(0, context.length() - 1) + path;
        logger.error("Incoming request and expected request payloads are not matched");
        logger.error("Incoming request payload for the mock service path "
                + servicePath + " is - " + requestContext.getRequestBody());
        logger.error("Expected request payload for the mock service path " + servicePath + " is - " + body);

        return false;
    }

    private boolean isHeadersMatch(HttpRequestContext requestContext) {
        if (header == null) {
            return true;
        }

        Map<String, List<String>> headerParameters = requestContext.getHeaderParameters();
        List<String> headerValues = headerParameters.get(header.getName());

        if (headerParameters == null || headerValues == null || headerValues.isEmpty()) {
            return false;
        }

        for (String value : headerValues) {
            if (value.equalsIgnoreCase(header.getValue())) {
                return true;
            }
        }

        logger.error("Could not find expected request header "  + header.getName() + ":" + header.getValue() +
                " in incoming header list for the mock service path "
                + context.substring(0, context.length() - 1) + path);
        return false;
    }

    private boolean isQueryParameterMatch(HttpRequestContext requestContext) {

        Map<String, List<String>> requestQueryParameters = requestContext.getQueryParameters();
        if ((queryParameters == null || queryParameters.isEmpty())
                && (requestQueryParameters == null || requestQueryParameters.isEmpty())) {
            return true;
        } else if (queryParameters == null || queryParameters.isEmpty()
                || queryParameters.size() != requestQueryParameters.size()) {
            return false;
        }

        for (QueryParameter definedQueryParams : queryParameters) {
            if (!requestQueryParameters.containsKey(definedQueryParams.getName())) {
                return false;
            }

            List<String> queryValues = requestQueryParameters.get(definedQueryParams.getName());
            if (queryValues == null || queryValues.isEmpty()) {
                return false;
            }
            for (String value : queryValues) {
                if (!value.equalsIgnoreCase(definedQueryParams.getValue())) {
                    return false;
                }
            }
        }

        return true;
    }

    private String buildRegex(String context, String path) {
        String fullPath = "";

        if ((context == null || context.isEmpty()) && (path == null || path.isEmpty())) {
            return ".*";
        }

        if (context != null && !context.isEmpty()) {
            fullPath = context;

            if (!fullPath.startsWith("/")) {
                fullPath = "/" + fullPath;
            }

            if (!fullPath.endsWith("/")) {
                fullPath = fullPath + "/";
            }
        } else {
            fullPath = ".*";
        }

        if (path != null && !path.isEmpty()) {
            if (fullPath.endsWith("/") && path.startsWith("/")) {
                fullPath = fullPath + path.substring(1);
            } else if (fullPath.endsWith("/") && !path.startsWith("/")) {
                fullPath = fullPath + path;
            } else if (!fullPath.endsWith("/") && path.startsWith("/")) {
                fullPath = fullPath + path;
            } else {
                fullPath = fullPath + "/" + path;
            }
        } else {
            fullPath = fullPath + ".*";
        }

        if (fullPath.endsWith("/")) {
            fullPath = fullPath.substring(0, fullPath.length() - 1);
        }

        return "^" + fullPath + "/$";
    }

    private String extractContext(String uri) {
        if (uri == null || uri.isEmpty()) {
            return null;
        }
        if (!uri.contains("?")) {
            if (!uri.endsWith("/")) {
                uri = uri + "/";
            }
            return uri;
        }
        uri = uri.split("\\?")[0];
        if (!uri.endsWith("/")) {
            uri = uri + "/";
        }
        return uri;
    }
}
