/*
 * Copyright (c) 2015, 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.wso2.carbon.apimgt.keymgt.handlers;

import org.apache.commons.lang.StringUtils;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.wso2.carbon.apimgt.api.APIManagementException;
import org.wso2.carbon.apimgt.api.model.AccessTokenInfo;
import org.wso2.carbon.apimgt.impl.APIConstants;
import org.wso2.carbon.apimgt.impl.dto.APIKeyValidationInfoDTO;
import org.wso2.carbon.apimgt.impl.utils.APIUtil;
import org.wso2.carbon.apimgt.keymgt.APIKeyMgtException;
import org.wso2.carbon.apimgt.keymgt.SubscriptionDataHolder;
import org.wso2.carbon.apimgt.keymgt.model.SubscriptionDataStore;
import org.wso2.carbon.apimgt.keymgt.model.entity.API;
import org.wso2.carbon.apimgt.keymgt.model.entity.ApiPolicy;
import org.wso2.carbon.apimgt.keymgt.model.entity.Application;
import org.wso2.carbon.apimgt.keymgt.model.entity.ApplicationKeyMapping;
import org.wso2.carbon.apimgt.keymgt.model.entity.ApplicationPolicy;
import org.wso2.carbon.apimgt.keymgt.model.entity.Subscription;
import org.wso2.carbon.apimgt.keymgt.model.entity.SubscriptionPolicy;
import org.wso2.carbon.apimgt.keymgt.model.exception.DataLoadingException;
import org.wso2.carbon.apimgt.keymgt.model.impl.SubscriptionDataLoaderImpl;
import org.wso2.carbon.apimgt.keymgt.service.TokenValidationContext;
import org.wso2.carbon.apimgt.keymgt.token.TokenGenerator;
import org.wso2.carbon.apimgt.keymgt.util.APIKeyMgtDataHolder;
import org.wso2.carbon.utils.multitenancy.MultitenantConstants;
import org.wso2.carbon.utils.multitenancy.MultitenantUtils;

import java.util.ArrayList;
import java.util.List;

public abstract class AbstractKeyValidationHandler implements KeyValidationHandler {

    private static final Log log = LogFactory.getLog(AbstractKeyValidationHandler.class);

    @Override
    public boolean validateSubscription(TokenValidationContext validationContext) throws APIKeyMgtException {

        if (validationContext == null || validationContext.getValidationInfoDTO() == null) {
            return false;
        }

        if (validationContext.isCacheHit()) {
            return true;
        }

        APIKeyValidationInfoDTO dto = validationContext.getValidationInfoDTO();


        if (validationContext.getTokenInfo() != null) {
            if (validationContext.getTokenInfo().isApplicationToken()) {
                dto.setUserType(APIConstants.ACCESS_TOKEN_USER_TYPE_APPLICATION);
            } else {
                dto.setUserType("APPLICATION_USER");
            }

            AccessTokenInfo tokenInfo = validationContext.getTokenInfo();

            // This block checks if a Token of Application Type is trying to access a resource protected with
            // Application Token
            if (!hasTokenRequiredAuthLevel(validationContext.getRequiredAuthenticationLevel(), tokenInfo)) {
                dto.setAuthorized(false);
                dto.setValidationStatus(APIConstants.KeyValidationStatus.API_AUTH_INCORRECT_ACCESS_TOKEN_TYPE);
                return false;
            }
        }

        boolean state = false;

        try {
            if (log.isDebugEnabled()) {
                log.debug("Before validating subscriptions : " + dto);
                log.debug("Validation Info : { context : " + validationContext.getContext() + " , " + "version : "
                        + validationContext.getVersion() + " , consumerKey : " + dto.getConsumerKey() + " }");
            }

            state = validateSubscriptionDetails(validationContext.getContext(), validationContext.getVersion(),
                    dto.getConsumerKey(), dto.getKeyManager(), dto);

            if (log.isDebugEnabled()) {
                log.debug("After validating subscriptions : " + dto);
            }


        } catch (APIManagementException e) {
            log.error("Error Occurred while validating subscription.", e);
        }

        return state;
    }

    /**
     * Determines whether the provided token is an ApplicationToken.
     *
     * @param tokenInfo - Access Token Information
     */
    protected void setTokenType(AccessTokenInfo tokenInfo) {


    }

    /**
     * Resources protected with Application token type can only be accessed using Application Access Tokens. This method
     * verifies if a particular resource can be accessed using the obtained token.
     *
     * @param authScheme Type of token required by the resource (Application | User Token)
     * @param tokenInfo  Details about the Token
     * @return {@code true} if token is of the type required, {@code false} otherwise.
     */
    protected boolean hasTokenRequiredAuthLevel(String authScheme,
                                                AccessTokenInfo tokenInfo) {

        if (authScheme == null || authScheme.isEmpty() || tokenInfo == null) {
            return false;
        }

        if (APIConstants.AUTH_APPLICATION_LEVEL_TOKEN.equals(authScheme)) {
            return tokenInfo.isApplicationToken();
        } else if (APIConstants.AUTH_APPLICATION_USER_LEVEL_TOKEN.equals(authScheme)) {
            return !tokenInfo.isApplicationToken();
        }

        return true;

    }

    @Override
    public boolean generateConsumerToken(TokenValidationContext validationContext) throws APIKeyMgtException {

      TokenGenerator generator = APIKeyMgtDataHolder.getTokenGenerator();

        try {
            String jwt = generator.generateToken(validationContext);
            validationContext.getValidationInfoDTO().setEndUserToken(jwt);
            return true;

        } catch (APIManagementException e) {
            log.error("Error occurred while generating JWT. ", e);
        }

        return false;
    }

    @Override
    public APIKeyValidationInfoDTO validateSubscription(String apiContext, String apiVersion, String consumerKey,
                                                        String keyManager) {
        APIKeyValidationInfoDTO apiKeyValidationInfoDTO =  new APIKeyValidationInfoDTO();

        try {
            if (log.isDebugEnabled()) {
                log.debug("Before validating subscriptions");
                log.debug("Validation Info : { context : " + apiContext + " , " + "version : "
                        + apiVersion + " , consumerKey : " + consumerKey + " }");
            }
            validateSubscriptionDetails(apiContext, apiVersion, consumerKey, keyManager, apiKeyValidationInfoDTO);
            if (log.isDebugEnabled()) {
                log.debug("After validating subscriptions");
            }
        } catch (APIManagementException e) {
            log.error("Error Occurred while validating subscription.", e);
        }
        return apiKeyValidationInfoDTO;
    }
    
    
    private boolean validateSubscriptionDetails(String context, String version, String consumerKey, String keyManager,
            APIKeyValidationInfoDTO infoDTO) throws APIManagementException {
        boolean defaultVersionInvoked = false;
        String apiTenantDomain = MultitenantUtils.getTenantDomainFromRequestURL(context);
        if (apiTenantDomain == null) {
            apiTenantDomain = MultitenantConstants.SUPER_TENANT_DOMAIN_NAME;
        }
        int apiOwnerTenantId = APIUtil.getTenantIdFromTenantDomain(apiTenantDomain);
        // Check if the api version has been prefixed with _default_
        if (version != null && version.startsWith(APIConstants.DEFAULT_VERSION_PREFIX)) {
            defaultVersionInvoked = true;
            // Remove the prefix from the version.
            version = version.split(APIConstants.DEFAULT_VERSION_PREFIX)[1];
        }

        validateSubscriptionDetails(infoDTO, context, version, consumerKey, keyManager, defaultVersionInvoked);
        return infoDTO.isAuthorized();
    }
    
    private APIKeyValidationInfoDTO validateSubscriptionDetails(APIKeyValidationInfoDTO infoDTO, String context,
            String version, String consumerKey, String keyManager, boolean defaultVersionInvoked) {
        String apiTenantDomain = MultitenantUtils.getTenantDomainFromRequestURL(context);
        if (apiTenantDomain == null) {
            apiTenantDomain = MultitenantConstants.SUPER_TENANT_DOMAIN_NAME;
        }
        int tenantId = APIUtil.getTenantIdFromTenantDomain(apiTenantDomain);
        API api = null;
        ApplicationKeyMapping key = null;
        Application app = null;
        Subscription sub = null;
        
        SubscriptionDataStore datastore = SubscriptionDataHolder.getInstance()
                .getTenantSubscriptionStore(apiTenantDomain);
        //TODO add a check to see whether datastore is initialized an load data using rest api if it is not loaded
        if (datastore != null) {
            api = datastore.getApiByContextAndVersion(context, version);
            if (api != null) {
                key = datastore.getKeyMappingByKeyAndKeyManager(consumerKey, keyManager);
                if (key != null) {
                    app = datastore.getApplicationById(key.getApplicationId());
                    if (app != null) {
                        sub = datastore.getSubscriptionById(app.getId(), api.getApiId());
                        if (sub != null) {
                            if (log.isDebugEnabled()) {
                                log.debug("All information is retrieved from the inmemory data store.");
                            }
                        } else {
                            if (log.isDebugEnabled()) {
                                log.debug("Valid subscription not found for appId " + app.getId() + " and apiId "
                                        + api.getApiId());
                            }
                            loadInfoFromRestAPIAndValidate(api, app, key, sub, context, version, consumerKey,
                                    keyManager, datastore, apiTenantDomain, infoDTO, tenantId);
                        }
                    } else {
                        if (log.isDebugEnabled()) {
                            log.debug("Application not found in the datastore for id " + key.getApplicationId());
                        }
                        loadInfoFromRestAPIAndValidate(api, app, key, sub, context, version, consumerKey, keyManager,
                                datastore, apiTenantDomain, infoDTO, tenantId);
                    }
                } else {
                    if (log.isDebugEnabled()) {
                        log.debug(
                                "Application keymapping not found in the datastore for id consumerKey " + consumerKey);
                    }
                    loadInfoFromRestAPIAndValidate(api, app, key, sub, context, version, consumerKey, keyManager,
                            datastore, apiTenantDomain, infoDTO, tenantId);
                }
            } else {
                if (log.isDebugEnabled()) {
                    log.debug("API not found in the datastore for " + context + ":" + version);
                }
                loadInfoFromRestAPIAndValidate(api, app, key, sub, context, version, consumerKey, keyManager, datastore,
                        apiTenantDomain, infoDTO, tenantId);
            }
        } else {
            log.error("Subscription datastore is null for tenant domain " + apiTenantDomain);
            loadInfoFromRestAPIAndValidate(api, app, key, sub, context, version, consumerKey, keyManager, datastore,
                    apiTenantDomain, infoDTO, tenantId);
        }
        
        if (api != null && app != null && key != null && sub != null) {
            validate(infoDTO, apiTenantDomain, tenantId, datastore, api, key, app, sub, keyManager);
        } else if (!infoDTO.isAuthorized() && infoDTO.getValidationStatus() == 0) {
            //Scenario where validation failed and message is not set
            infoDTO.setValidationStatus(APIConstants.KeyValidationStatus.API_AUTH_RESOURCE_FORBIDDEN);
        }

        return infoDTO;
    }

    private void loadInfoFromRestAPIAndValidate(API api, Application app, ApplicationKeyMapping key, Subscription sub,
            String context, String version, String consumerKey, String keyManager, SubscriptionDataStore datastore, String apiTenantDomain, APIKeyValidationInfoDTO infoDTO, int tenantId) {
        // TODO Load using a single single rest api.
        if(log.isDebugEnabled()) {
            log.debug("Loading missing information in the datastore by invoking the Rest API");
        }
        try {
            // only loading if the api is not found previously
            if (api == null) {
                api = new SubscriptionDataLoaderImpl().getApi(context, version);
                if (api != null && api.getApiId() != 0) {
                    // load to the memory
                    log.debug("Loading API to the in-memory datastore.");
                    datastore.addOrUpdateAPI(api);
                }
            }
            // only loading if the key is not found previously
            if (key == null) {
                key = new SubscriptionDataLoaderImpl().getKeyMapping(consumerKey);
                if (key != null && !StringUtils.isEmpty(key.getConsumerKey())) {
                    // load to the memory
                    log.debug("Loading Keymapping to the in-memory datastore.");
                    datastore.addOrUpdateApplicationKeyMapping(key);
                }
            }
            // check whether still api and keys are not found
            if(api == null || key == null) {
                // invalid request. nothing to do. return without any further processing
                if (log.isDebugEnabled()) {
                    if (api == null) {
                        log.debug("API not found for the " + context + " " + version);
                    }
                    if (key == null) {
                        log.debug("KeyMapping not found for the " + consumerKey);
                    }
                }
                return;
            } else {
                //go further and load missing objects
                if(app == null) {
                    app = new SubscriptionDataLoaderImpl().getApplicationById(key.getApplicationId());
                    if(app != null && app.getId() != 0) {
                        // load to the memory
                        log.debug("Loading Application to the in-memory datastore.");
                        datastore.addOrUpdateApplication(app);
                    } else {
                        log.debug("Application not found.");
                    }
                }
                if (app != null) {
                    sub = new SubscriptionDataLoaderImpl().getSubscriptionById(Integer.toString(api.getApiId()),
                            Integer.toString(app.getId()));
                    if(sub != null && !StringUtils.isEmpty(sub.getSubscriptionId())) {
                        // load to the memory
                        log.debug("Loading Subscription to the in-memory datastore.");
                        datastore.addOrUpdateSubscription(sub);
                        validate(infoDTO, apiTenantDomain, tenantId, datastore, api, key, app, sub, keyManager);
                    }
                }
            }
        } catch (DataLoadingException e) {
            log.error("Error while connecting the backend for loading subscription related data ", e);
        }
    }

    private APIKeyValidationInfoDTO validate(APIKeyValidationInfoDTO infoDTO, String apiTenantDomain, int tenantId,
            SubscriptionDataStore datastore, API api, ApplicationKeyMapping key, Application app, Subscription sub,
            String keyManager) {
        String subscriptionStatus = sub.getSubscriptionState();
        String type = key.getKeyType();
        if (APIConstants.SubscriptionStatus.BLOCKED.equals(subscriptionStatus)) {
            infoDTO.setValidationStatus(APIConstants.KeyValidationStatus.API_BLOCKED);
            infoDTO.setAuthorized(false);
            return infoDTO;
        } else if (APIConstants.SubscriptionStatus.ON_HOLD.equals(subscriptionStatus)
                || APIConstants.SubscriptionStatus.REJECTED.equals(subscriptionStatus)) {
            infoDTO.setValidationStatus(APIConstants.KeyValidationStatus.SUBSCRIPTION_INACTIVE);
            infoDTO.setAuthorized(false);
            return infoDTO;
        } else if (APIConstants.SubscriptionStatus.PROD_ONLY_BLOCKED.equals(subscriptionStatus)
                && !APIConstants.API_KEY_TYPE_SANDBOX.equals(type)) {
            infoDTO.setValidationStatus(APIConstants.KeyValidationStatus.API_BLOCKED);
            infoDTO.setType(type);
            infoDTO.setAuthorized(false);
            return infoDTO;
        }
        infoDTO.setTier(sub.getPolicyId());
        infoDTO.setSubscriber(app.getSubName());
        infoDTO.setApplicationId(app.getId().toString());
        infoDTO.setApiName(api.getApiName());
        infoDTO.setApiPublisher(api.getApiProvider());
        infoDTO.setApplicationName(app.getName());
        infoDTO.setApplicationTier(app.getPolicy());
        infoDTO.setType(type);

        // Advanced Level Throttling Related Properties
        if (APIUtil.isAdvanceThrottlingEnabled()) {
            String apiTier = api.getApiTier();
            String subscriberUserId = sub.getSubscriptionId();
            String subscriberTenant = MultitenantUtils.getTenantDomain(app.getSubName());

            ApplicationPolicy appPolicy = datastore.getApplicationPolicyByName(app.getPolicy(),
                    tenantId);
            if (appPolicy == null) {
                try {
                    appPolicy = new SubscriptionDataLoaderImpl()
                            .getApplicationPolicy(app.getPolicy(), apiTenantDomain);
                    datastore.addOrUpdateApplicationPolicy(appPolicy);
                } catch (DataLoadingException e) {
                    log.error("Error while loading ApplicationPolicy");
                }
            }
            SubscriptionPolicy subPolicy = datastore.getSubscriptionPolicyByName(sub.getPolicyId(),
                    tenantId);
            if (subPolicy == null) {
                try {
                    subPolicy = new SubscriptionDataLoaderImpl()
                            .getSubscriptionPolicy(sub.getPolicyId(), apiTenantDomain);
                    datastore.addOrUpdateSubscriptionPolicy(subPolicy);
                } catch (DataLoadingException e) {
                    log.error("Error while loading SubscriptionPolicy");
                }
            }
            ApiPolicy apiPolicy = datastore.getApiPolicyByName(api.getApiTier(), tenantId);

            boolean isContentAware = false;
            if (appPolicy.isContentAware() || subPolicy.isContentAware()
                    || (apiPolicy != null && apiPolicy.isContentAware())) {
                isContentAware = true;
            }
            infoDTO.setContentAware(isContentAware);

            // TODO this must implement as a part of throttling implementation.
            int spikeArrest = 0;
            String apiLevelThrottlingKey = "api_level_throttling_key";

            if (subPolicy.getRateLimitCount() > 0) {
                spikeArrest = subPolicy.getRateLimitCount();
            }

            String spikeArrestUnit = null;

            if (subPolicy.getRateLimitTimeUnit() != null) {
                spikeArrestUnit = subPolicy.getRateLimitTimeUnit();
            }
            boolean stopOnQuotaReach = subPolicy.isStopOnQuotaReach();
            int graphQLMaxDepth = 0;
            if (subPolicy.getGraphQLMaxDepth() > 0) {
                graphQLMaxDepth = subPolicy.getGraphQLMaxDepth();
            }
            int graphQLMaxComplexity = 0;
            if (subPolicy.getGraphQLMaxComplexity() > 0) {
                graphQLMaxComplexity = subPolicy.getGraphQLMaxComplexity();
            }
            List<String> list = new ArrayList<String>();
            list.add(apiLevelThrottlingKey);
            infoDTO.setSpikeArrestLimit(spikeArrest);
            infoDTO.setSpikeArrestUnit(spikeArrestUnit);
            infoDTO.setStopOnQuotaReach(stopOnQuotaReach);
            infoDTO.setSubscriberTenantDomain(subscriberTenant);
            infoDTO.setGraphQLMaxDepth(graphQLMaxDepth);
            infoDTO.setGraphQLMaxComplexity(graphQLMaxComplexity);
            if (apiTier != null && apiTier.trim().length() > 0) {
                infoDTO.setApiTier(apiTier);
            }
            // We also need to set throttling data list associated with given API. This need to have
            // policy id and
            // condition id list for all throttling tiers associated with this API.
            infoDTO.setThrottlingDataList(list);
        }
        infoDTO.setAuthorized(true);
        return infoDTO;
    }
}