package io.apiman.plugins.urlwhitelist;

import io.apiman.gateway.engine.beans.ApiRequest;
import io.apiman.gateway.engine.beans.PolicyFailure;
import io.apiman.gateway.engine.beans.PolicyFailureType;
import io.apiman.gateway.engine.beans.exceptions.ConfigurationParseException;
import io.apiman.gateway.engine.policies.AbstractMappedPolicy;
import io.apiman.gateway.engine.policy.IPolicyChain;
import io.apiman.gateway.engine.policy.IPolicyContext;
import io.apiman.plugins.urlwhitelist.beans.UrlWhitelistBean;
import io.apiman.plugins.urlwhitelist.beans.WhitelistEntryBean;
import io.apiman.plugins.urlwhitelist.util.Messages;

import java.net.HttpURLConnection;
import java.net.URI;
import java.net.URISyntaxException;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;
import java.util.regex.Pattern;

/**
 * A policy that only permits requests matching a whitelist.
 *
 * @author Pete Cornish {@literal <outofcoffee@gmail.com>}
 */
public class UrlWhitelistPolicy extends AbstractMappedPolicy<UrlWhitelistBean> {
    private static final Messages MESSAGES = new Messages("io.apiman.plugins.urlwhitelist", "UrlWhitelistPolicy"); //$NON-NLS-1$ //$NON-NLS-2$
    private static final String APIMAN_GATEWAY = "/apiman-gateway"; //$NON-NLS-1$

    /**
     * Cache of precompiled URL patterns. Note that {@link Pattern} is thread-safe.
     */
    private static final Map<String, Pattern> PATTERN_MAP = new ConcurrentHashMap<>();

    /**
     * @see io.apiman.gateway.engine.policies.AbstractMappedPolicy#getConfigurationClass()
     */
    @Override
    public Class<UrlWhitelistBean> getConfigurationClass() {
        return UrlWhitelistBean.class;
    }

    /**
     * @see io.apiman.gateway.engine.policies.AbstractMappedPolicy#parseConfiguration(String)
     */
    @Override
    public UrlWhitelistBean parseConfiguration(String jsonConfiguration) throws ConfigurationParseException {
        final UrlWhitelistBean config = super.parseConfiguration(jsonConfiguration);

        // precompile patterns for performance
        for (WhitelistEntryBean whitelistEntry : config.getWhitelist()) {
            try {
                PATTERN_MAP.put(whitelistEntry.getRegex(), Pattern.compile(whitelistEntry.getRegex()));
            } catch (Exception e) {
                throw new ConfigurationParseException(MESSAGES.format("Error.CompilingPattern", whitelistEntry.getRegex()), e); //$NON-NLS-1$
            }
        }

        return config;
    }

    /**
     * @see io.apiman.gateway.engine.policies.AbstractMappedPolicy#doApply(io.apiman.gateway.engine.beans.ApiRequest, io.apiman.gateway.engine.policy.IPolicyContext, java.lang.Object, io.apiman.gateway.engine.policy.IPolicyChain)
     */
    @Override
    protected void doApply(ApiRequest request, IPolicyContext context, UrlWhitelistBean config,
                           IPolicyChain<ApiRequest> chain) {

        // normalise, for safety
        final String normalisedPath;
        try {
            normalisedPath = getNormalisedPath(config, request);
        } catch (Exception e) {
            chain.throwError(new RuntimeException(MESSAGES.format("Error.NormalisingPath", request.getUrl()), e)); //$NON-NLS-1$
            return;
        }

        final boolean requestPermitted;
        try {
            requestPermitted = isRequestPermitted(config, normalisedPath, request.getType());
        } catch (Exception e) {
            chain.throwError(new RuntimeException(MESSAGES.format(
                    "Error.CheckingRequest", request.getType(), normalisedPath), e)); //$NON-NLS-1$
            return;
        }

        if (requestPermitted) {
            chain.doApply(request);
        } else {
            chain.doFailure(new PolicyFailure(PolicyFailureType.Authorization,
                    HttpURLConnection.HTTP_FORBIDDEN, MESSAGES.format("Failure.UrlNotPermitted", normalisedPath))); //$NON-NLS-1$
        }
    }

    /**
     * Normalise the request URL before evaluating any access control rules, for safety. Returns the path
     * component of the normalised URL.
     *
     * @param config  the policy configuration
     * @param request the incoming request
     * @return the normalised path
     * @throws URISyntaxException
     */
    private String getNormalisedPath(UrlWhitelistBean config, ApiRequest request) throws URISyntaxException {
        // normalise, for safety
        final URI normalisedUrl = new URI(request.getUrl()).normalize();

        String path = normalisedUrl.getPath();
        if (config.isRemovePathPrefix()) {
            if (path.startsWith(APIMAN_GATEWAY)) {
                path = path.substring(APIMAN_GATEWAY.length());
            }

            // remove org/API/version prefix, e.g. '/myorg/myapi/1.0'
            final String apiPrefix = String.format("/%s/%s/%s", //$NON-NLS-1$
                    request.getApiOrgId(),
                    request.getApiId(),
                    request.getApiVersion());

            path = path.substring(apiPrefix.length());
        }

        return path;
    }

    /**
     * Evaluates whether the request for the {@code normalisedPath} and {@code method} is permitted by
     * the rules in the {@code config}.
     *
     * @param config         the policy configuration
     * @param normalisedPath the normalised request path
     * @param method         the HTTP method
     * @return {@code true} if the request is permitted, otherwise {@code false}
     */
    private boolean isRequestPermitted(UrlWhitelistBean config, String normalisedPath, String method) {
        for (WhitelistEntryBean whitelistEntry : config.getWhitelist()) {
            final Pattern pattern = PATTERN_MAP.get(whitelistEntry.getRegex());
            if (null != pattern && pattern.matcher(normalisedPath).matches()) {
                return isMethodPermitted(whitelistEntry, method);
            }
        }
        return false;
    }

    /**
     * Evaluates whether the request for the {@code method} is permitted by the configuration of
     * the {@code whitelistEntry}.
     *
     * @param whitelistEntry the whitelist entry matching the request URL
     * @param method         the HTTP method
     * @return {@code true} if the method is permitted, otherwise {@code false}
     */
    private boolean isMethodPermitted(WhitelistEntryBean whitelistEntry, String method) {
        switch (method.toUpperCase()) {
            case "GET": //$NON-NLS-1$
                return whitelistEntry.isMethodGet();
            case "POST": //$NON-NLS-1$
                return whitelistEntry.isMethodPost();
            case "PUT": //$NON-NLS-1$
                return whitelistEntry.isMethodPut();
            case "PATCH": //$NON-NLS-1$
                return whitelistEntry.isMethodPatch();
            case "DELETE": //$NON-NLS-1$
                return whitelistEntry.isMethodDelete();
            case "HEAD": //$NON-NLS-1$
                return whitelistEntry.isMethodHead();
            case "OPTIONS": //$NON-NLS-1$
                return whitelistEntry.isMethodOptions();
            case "TRACE": //$NON-NLS-1$
                return whitelistEntry.isMethodTrace();

            default:
                throw new UnsupportedOperationException(MESSAGES.format("Error.MethodNotSupported", method)); //$NON-NLS-1$
        }
    }
}
