/*
 * Copyright (c) 2016 Network New Technologies Inc.
 *
 * 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.
 */

package com.networknt.openapi;

import java.util.Collections;
import java.util.HashMap;
import java.util.Map;
import java.util.Optional;

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import com.networknt.audit.AuditHandler;
import com.networknt.config.Config;
import com.networknt.handler.Handler;
import com.networknt.handler.MiddlewareHandler;
import com.networknt.httpstring.AttachmentConstants;
import com.networknt.oas.model.Operation;
import com.networknt.oas.model.Path;
import com.networknt.openapi.parameter.ParameterDeserializer;
import com.networknt.utility.Constants;
import com.networknt.utility.ModuleRegistry;

import io.undertow.Handlers;
import io.undertow.server.HttpHandler;
import io.undertow.server.HttpServerExchange;
import io.undertow.util.AttachmentKey;
import io.undertow.util.HeaderMap;
import io.undertow.util.HeaderValues;
import io.undertow.util.HttpString;

/**
 * This is the handler that parses the OpenApi object based on uri and method
 * of the request and attached the operation to the request so that security
 * and validator can use it without parsing it.
 *
 * @author Steve Hu
 */
public class OpenApiHandler implements MiddlewareHandler {
    static final Logger logger = LoggerFactory.getLogger(OpenApiHandler.class);

    public static final String CONFIG_NAME = "openapi";
    
	public static final AttachmentKey<Map<String, Object>> DESERIALIZED_QUERY_PARAMETERS = AttachmentKey.create(Map.class);
	public static final AttachmentKey<Map<String, Object>> DESERIALIZED_PATH_PARAMETERS = AttachmentKey.create(Map.class);
	public static final AttachmentKey<Map<String, Object>> DESERIALIZED_HEADER_PARAMETERS = AttachmentKey.create(Map.class);
	public static final AttachmentKey<Map<String, Object>> DESERIALIZED_COOKIE_PARAMETERS = AttachmentKey.create(Map.class);

    static final String STATUS_INVALID_REQUEST_PATH = "ERR10007";
    static final String STATUS_METHOD_NOT_ALLOWED = "ERR10008";

    private volatile HttpHandler next;

    public OpenApiHandler() {

    }


    @Override
    public void handleRequest(final HttpServerExchange exchange) throws Exception {
        final NormalisedPath requestPath = new ApiNormalisedPath(exchange.getRequestURI());
        final Optional<NormalisedPath> maybeApiPath = OpenApiHelper.findMatchingApiPath(requestPath);
        if (!maybeApiPath.isPresent()) {
            setExchangeStatus(exchange, STATUS_INVALID_REQUEST_PATH, requestPath.normalised());
            return;
        }

        final NormalisedPath openApiPathString = maybeApiPath.get();
        final Path path = OpenApiHelper.openApi3.getPath(openApiPathString.original());

        final String httpMethod = exchange.getRequestMethod().toString().toLowerCase();
        final Operation operation = path.getOperation(httpMethod);

        if (operation == null) {
            setExchangeStatus(exchange, STATUS_METHOD_NOT_ALLOWED);
            return;
        }

        // This handler can identify the openApiOperation and endpoint only. Other info will be added by JwtVerifyHandler.
        final OpenApiOperation openApiOperation = new OpenApiOperation(openApiPathString, path, httpMethod, operation);
        
        try {
        	ParameterDeserializer.deserialize(exchange, openApiOperation);
        }catch (Throwable t) {// do not crash the handler
        	logger.error(t.getMessage(), t);
        }
        
        String endpoint = openApiPathString.normalised() + "@" + httpMethod.toString().toLowerCase();
        Map<String, Object> auditInfo = new HashMap<>();
        auditInfo.put(Constants.ENDPOINT_STRING, endpoint);
        auditInfo.put(Constants.OPENAPI_OPERATION_STRING, openApiOperation);
        exchange.putAttachment(AttachmentConstants.AUDIT_INFO, auditInfo);

        Handler.next(exchange, next);
    }

    @Override
    public HttpHandler getNext() {
        return next;
    }

    @Override
    public MiddlewareHandler setNext(final HttpHandler next) {
        Handlers.handlerNotNull(next);
        this.next = next;
        return this;
    }

    @Override
    public boolean isEnabled() {
        // just check if swagger.json exists or not.
        return (OpenApiHelper.openApi3 != null);
    }

    @Override
    public void register() {
        ModuleRegistry.registerModule(OpenApiHandler.class.getName(), Config.getInstance().getJsonMapConfig(CONFIG_NAME), null);
    }
    
    /**
     * merge two maps. The values in preferredMap take priority.
     * 
     * @param preferredMap
     * @param alternativeMap
     * @return
     */
    @SuppressWarnings({ "rawtypes", "unchecked" })
	protected static Map<String, ?> mergeMaps(Map preferredMap, Map alternativeMap){
    	Map mergedMap = new HashMap<>();
    	
    	if (null!=alternativeMap)
    		mergedMap.putAll(alternativeMap);
    	
    	if (null!=preferredMap)
    		mergedMap.putAll(preferredMap);
    	
    	return Collections.unmodifiableMap(mergedMap);
    }
    
    protected static Map<String, Object> nonNullMap(Map<String, Object> map){
    	return null==map?Collections.emptyMap():Collections.unmodifiableMap(map);
    }
    
    public static Map<String, ?> getQueryParameters(final HttpServerExchange exchange){
    	return getQueryParameters(exchange, false);
    }
    
    public static Map<String, ?> getQueryParameters(final HttpServerExchange exchange, final boolean deserializedValueOnly){
    	Map<String, Object> deserializedQueryParamters = exchange.getAttachment(DESERIALIZED_QUERY_PARAMETERS);
    	
    	return deserializedValueOnly?nonNullMap(deserializedQueryParamters)
    			:mergeMaps(deserializedQueryParamters, exchange.getQueryParameters());
    }
    
    public static Map<String, ?> getPathParameters(final HttpServerExchange exchange){
    	return getPathParameters(exchange, false);
    }
    
    public static Map<String, ?> getPathParameters(final HttpServerExchange exchange, final boolean deserializedValueOnly){
    	Map<String, Object> deserializedPathParamters = exchange.getAttachment(DESERIALIZED_PATH_PARAMETERS);
    	
    	return deserializedValueOnly?nonNullMap(deserializedPathParamters)
    			:mergeMaps(deserializedPathParamters, exchange.getPathParameters());
    }
    
    public static Map<String, ?> getHeaderParameters(final HttpServerExchange exchange){
    	return getHeaderParameters(exchange, false);
    }
    
    public static Map<String, ?> getHeaderParameters(final HttpServerExchange exchange, final boolean deserializedValueOnly){
    	Map<String, Object> deserializedHeaderParamters = exchange.getAttachment(DESERIALIZED_HEADER_PARAMETERS);
    	
    	if (!deserializedValueOnly) {
    		HeaderMap headers = exchange.getRequestHeaders();
    		
    		if (null==headers) {
    			return Collections.emptyMap();
    		}
    		
    		Map<String, HeaderValues> headerMap = new HashMap<>();
    		
    		for (HttpString headerName: headers.getHeaderNames()) {
    			headerMap.put(headerName.toString(), headers.get(headerName));
    		}
    		
    		return mergeMaps(deserializedHeaderParamters, headerMap);
    	}
    	
    	return nonNullMap(deserializedHeaderParamters);
    }
    
    public static Map<String, ?> getCookieParameters(final HttpServerExchange exchange){
    	return getCookieParameters(exchange, false);
    }  
    
    public static Map<String, ?> getCookieParameters(final HttpServerExchange exchange, final boolean deserializedValueOnly){
    	Map<String, Object> deserializedCookieParamters = exchange.getAttachment(DESERIALIZED_COOKIE_PARAMETERS);
    	
    	return deserializedValueOnly?nonNullMap(deserializedCookieParamters)
    			:mergeMaps(deserializedCookieParamters, exchange.getRequestCookies());
    } 
}