/*
 * Copyright (c) 2019, WSO2 Inc. (http://www.wso2.org) All Rights Reserved.
 *
 * 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 org.wso2.carbon.apimgt.gateway.utils;

import java.util.ArrayList;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;

import org.apache.commons.lang.StringUtils;
import org.apache.synapse.MessageContext;
import org.apache.synapse.core.axis2.Axis2MessageContext;
import org.wso2.carbon.apimgt.impl.APIConstants;

import io.swagger.v3.oas.models.OpenAPI;
import io.swagger.v3.oas.models.Operation;
import io.swagger.v3.oas.models.PathItem;
import io.swagger.v3.oas.models.security.SecurityRequirement;

public class OpenAPIUtils {

    /**
     * Return the resource authentication scheme of the API resource.
     *
     * @param openAPI OpenAPI of the API
     * @param synCtx  The message containing resource request
     * @return the resource authentication scheme
     */
    public static String getResourceAuthenticationScheme(OpenAPI openAPI, MessageContext synCtx) {
        String authType = null;
        Map<String, Object> vendorExtensions = getPathItemExtensions(synCtx, openAPI);
        if (vendorExtensions != null) {
            authType = (String) vendorExtensions.get(APIConstants.SWAGGER_X_AUTH_TYPE);
        }

        if (StringUtils.isNotBlank(authType)) {
            if (APIConstants.OASResourceAuthTypes.APPLICATION_OR_APPLICATION_USER.equals(authType)) {
                authType = APIConstants.AUTH_APPLICATION_OR_USER_LEVEL_TOKEN;
            } else if (APIConstants.OASResourceAuthTypes.APPLICATION_USER.equals(authType)) {
                authType = APIConstants.AUTH_APPLICATION_USER_LEVEL_TOKEN;
            } else if (APIConstants.OASResourceAuthTypes.NONE.equals(authType)) {
                authType = APIConstants.AUTH_NO_AUTHENTICATION;
            } else if (APIConstants.OASResourceAuthTypes.APPLICATION.equals(authType)) {
                authType = APIConstants.AUTH_APPLICATION_LEVEL_TOKEN;
            } else {
                authType = APIConstants.AUTH_APPLICATION_OR_USER_LEVEL_TOKEN;
            }
            return authType;
        }
        //Return 'Any' type (meaning security is on) if the authType is null or empty.
        return APIConstants.AUTH_APPLICATION_OR_USER_LEVEL_TOKEN;
    }

    /**
     * Return the scopes bound to the API resource.
     *
     * @param openAPI OpenAPI of the API
     * @param synCtx  The message containing resource request
     * @return the scopes
     */
    public static String getScopesOfResource(OpenAPI openAPI, MessageContext synCtx) {
        Map<String, Object> vendorExtensions = getPathItemExtensions(synCtx, openAPI);
        if (vendorExtensions != null) {
            String resourceScope = (String) vendorExtensions.get(APIConstants.SWAGGER_X_SCOPE);
            if (resourceScope == null) {
                // If x-scope not found in swagger, check for the scopes in security
                List<String> securityScopes = getPathItemSecurityScopes(synCtx, openAPI);
                if (securityScopes == null || securityScopes.isEmpty()) {
                    return null;
                } else {
                    // We support only one scope for gateway authentication. Hence using the first scope from the list
                    // TODO:// add support multiple scopes per resource
                    return securityScopes.get(0);
                }
            } else {
                return resourceScope;
            }
        }
        return null;
    }

    /**
     * Return the roles of a given scope attached to a resource using the API swagger.
     *
     * @param openAPI OpenAPI of the API
     * @param synCtx  The message containing resource request
     * @param resourceScope  The scope of the resource
     * @return the roles of the scope in the comma separated format
     */
    public static String getRolesOfScope(OpenAPI openAPI, MessageContext synCtx, String resourceScope) {
        String resourceRoles = null;

        Map<String, Object> vendorExtensions = getPathItemExtensions(synCtx, openAPI);
        if (vendorExtensions != null) {
            if (StringUtils.isNotBlank(resourceScope)) {
                if (openAPI.getExtensions() != null &&
                        openAPI.getExtensions().get(APIConstants.SWAGGER_X_WSO2_SECURITY) != null) {
                    LinkedHashMap swaggerWSO2Security = (LinkedHashMap) openAPI.getExtensions()
                            .get(APIConstants.SWAGGER_X_WSO2_SECURITY);
                    if (swaggerWSO2Security != null &&
                            swaggerWSO2Security.get(APIConstants.SWAGGER_OBJECT_NAME_APIM) != null) {
                        LinkedHashMap swaggerObjectAPIM = (LinkedHashMap) swaggerWSO2Security
                                .get(APIConstants.SWAGGER_OBJECT_NAME_APIM);
                        if (swaggerObjectAPIM != null && swaggerObjectAPIM.get(APIConstants.SWAGGER_X_WSO2_SCOPES) != null) {
                            ArrayList<LinkedHashMap> apiScopes =
                                    (ArrayList<LinkedHashMap>) swaggerObjectAPIM.get(APIConstants.SWAGGER_X_WSO2_SCOPES);
                            for (LinkedHashMap scope: apiScopes) {
                                if (resourceScope.equals(scope.get(APIConstants.SWAGGER_SCOPE_KEY))) {
                                    resourceRoles = (String) scope.get(APIConstants.SWAGGER_ROLES);
                                    break;
                                }
                            }
                        }
                    }
                }
            }
        }

        if (resourceRoles == null) {
            LinkedHashMap<String, Object> scopeBindings = null;
            Map<String, Object> extensions = openAPI.getComponents().getSecuritySchemes()
                    .get(APIConstants.SWAGGER_APIM_DEFAULT_SECURITY).getExtensions();
            if (extensions != null && extensions.get(APIConstants.SWAGGER_X_SCOPES_BINDINGS) != null) {
                scopeBindings = (LinkedHashMap<String, Object>) extensions.get(APIConstants.SWAGGER_X_SCOPES_BINDINGS);
            } else {
                scopeBindings = (LinkedHashMap<String, Object>) openAPI.getComponents().getSecuritySchemes().
                        get(APIConstants.SWAGGER_APIM_DEFAULT_SECURITY).getFlows().getImplicit().getExtensions().
                        get(APIConstants.SWAGGER_X_SCOPES_BINDINGS);
            }
            if (scopeBindings != null) {
                return (String) scopeBindings.get(resourceScope);
            }
        }

        return null;
    }

    /**
     * Return the throttling tier of the API resource.
     *
     * @param openAPI OpenAPI of the API
     * @param synCtx  The message containing resource request
     * @return the resource throttling tier
     */
    public static String getResourceThrottlingTier(OpenAPI openAPI, MessageContext synCtx) {
        String throttlingTier = null;
        Map<String, Object> vendorExtensions = getPathItemExtensions(synCtx, openAPI);
        if (vendorExtensions != null) {
            throttlingTier = (String) vendorExtensions.get(APIConstants.SWAGGER_X_THROTTLING_TIER);
        }
        if (StringUtils.isNotBlank(throttlingTier)) {
            return throttlingTier;
        }
        return APIConstants.UNLIMITED_TIER;
    }
    
    /**
     * Check whether the tier for the API is content aware or not.
     * @param openAPI OpenAPI of the API
     * @param synCtx The message containing resource request
     * @return whether tier is content aware or not.
     */
    public static boolean isContentAwareTierAvailable(OpenAPI openAPI, MessageContext synCtx) {
        boolean status = false;
        Map<String, Object> vendorExtensions;
        if (openAPI != null) {
            vendorExtensions = openAPI.getExtensions();
            if (vendorExtensions != null
                    && vendorExtensions.get(APIConstants.SWAGGER_X_THROTTLING_BANDWIDTH) != null
                    && (boolean) vendorExtensions.get(APIConstants.SWAGGER_X_THROTTLING_BANDWIDTH)) {
                // check for api level policy
                status = true;
            } 
            // if there is api level policy. no need to check for resource level. if not, check for resource level
            if(!status) {
                vendorExtensions = getPathItemExtensions(synCtx, openAPI);
                if (vendorExtensions != null
                        && vendorExtensions.get(APIConstants.SWAGGER_X_THROTTLING_BANDWIDTH) != null
                        && (boolean) vendorExtensions.get(APIConstants.SWAGGER_X_THROTTLING_BANDWIDTH)) {
                    // check for resource level policy
                    status = true;
                } 
            }
        }
        return status;
    }

    private static Map<String, Object> getPathItemExtensions(MessageContext synCtx, OpenAPI openAPI) {
        if (openAPI != null) {
            String apiElectedResource = (String) synCtx.getProperty(APIConstants.API_ELECTED_RESOURCE);
            org.apache.axis2.context.MessageContext axis2MessageContext =
                    ((Axis2MessageContext) synCtx).getAxis2MessageContext();
            String httpMethod = (String) axis2MessageContext.getProperty(APIConstants.DigestAuthConstants.HTTP_METHOD);
            PathItem path = openAPI.getPaths().get(apiElectedResource);

            if (path != null) {
                switch (httpMethod) {
                    case APIConstants.HTTP_GET:
                        return path.getGet().getExtensions();
                    case APIConstants.HTTP_POST:
                        return path.getPost().getExtensions();
                    case APIConstants.HTTP_PUT:
                        return path.getPut().getExtensions();
                    case APIConstants.HTTP_DELETE:
                        return path.getDelete().getExtensions();
                    case APIConstants.HTTP_HEAD:
                        return path.getHead().getExtensions();
                    case APIConstants.HTTP_OPTIONS:
                        return path.getOptions().getExtensions();
                    case APIConstants.HTTP_PATCH:
                        return path.getPatch().getExtensions();
                }
            }
        }
        return null;
    }
    
    private static List<String> getPathItemSecurityScopes(MessageContext synCtx, OpenAPI openAPI) {
        if (openAPI != null) {
            String apiElectedResource = (String) synCtx.getProperty(APIConstants.API_ELECTED_RESOURCE);
            org.apache.axis2.context.MessageContext axis2MessageContext =
                    ((Axis2MessageContext) synCtx).getAxis2MessageContext();
            String httpMethod = (String) axis2MessageContext.getProperty(APIConstants.DigestAuthConstants.HTTP_METHOD);
            PathItem path = openAPI.getPaths().get(apiElectedResource);

            if (path != null) {
                Operation operation = path.readOperationsMap().get(PathItem.HttpMethod.valueOf(httpMethod));
                return getDefaultSecurityScopes(operation.getSecurity());
            }
        }
        return null;
    }

    /**
     * Extract the scopes of "default" security definition
     *
     * @param requirements security requirements of the operation
     * @return extracted scopes of "default" security definition
     */
    private static List<String> getDefaultSecurityScopes(List<SecurityRequirement> requirements) {
        if (requirements != null) {
            for (SecurityRequirement requirement: requirements) {
                if (requirement.get(APIConstants.SWAGGER_APIM_DEFAULT_SECURITY) != null) {
                    return requirement.get(APIConstants.SWAGGER_APIM_DEFAULT_SECURITY);
                }
            }
        }
        return new ArrayList<>();
    }
}