/*
 * Licensed to Elasticsearch under one or more contributor
 * license agreements. See the NOTICE file distributed with
 * this work for additional information regarding copyright
 * ownership. Elasticsearch 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.elasticsearch.http.netty4.cors;

import io.netty.handler.codec.http.DefaultHttpHeaders;
import io.netty.handler.codec.http.EmptyHttpHeaders;
import io.netty.handler.codec.http.HttpHeaders;
import io.netty.handler.codec.http.HttpMethod;

import java.util.Collections;
import java.util.LinkedHashSet;
import java.util.Map;
import java.util.Optional;
import java.util.Set;
import java.util.concurrent.Callable;
import java.util.regex.Pattern;

/**
 * Configuration for Cross-Origin Resource Sharing (CORS).
 *
 * This class was lifted from the Netty project:
 *  https://github.com/netty/netty
 */
public final class Netty4CorsConfig {

    private final Optional<Set<String>> origins;
    private final Optional<Pattern> pattern;
    private final boolean anyOrigin;
    private final boolean enabled;
    private final boolean allowCredentials;
    private final long maxAge;
    private final Set<HttpMethod> allowedRequestMethods;
    private final Set<String> allowedRequestHeaders;
    private final Map<CharSequence, Callable<?>> preflightHeaders;
    private final boolean shortCircuit;

    Netty4CorsConfig(final Netty4CorsConfigBuilder builder) {
        origins = builder.origins.map(s -> new LinkedHashSet<>(s));
        pattern = builder.pattern;
        anyOrigin = builder.anyOrigin;
        enabled = builder.enabled;
        allowCredentials = builder.allowCredentials;
        maxAge = builder.maxAge;
        allowedRequestMethods = builder.requestMethods;
        allowedRequestHeaders = builder.requestHeaders;
        preflightHeaders = builder.preflightHeaders;
        shortCircuit = builder.shortCircuit;
    }

    /**
     * Determines if support for CORS is enabled.
     *
     * @return {@code true} if support for CORS is enabled, false otherwise.
     */
    public boolean isCorsSupportEnabled() {
        return enabled;
    }

    /**
     * Determines whether a wildcard origin, '*', is supported. This also means that null origins are
     * supported.
     *
     * @return {@code boolean} true if any origin is allowed.
     */
    public boolean isAnyOriginSupported() {
        return anyOrigin;
    }

    /**
     * Returns the set of allowed origins.
     *
     * @return {@code Set} the allowed origins.
     */
    public Optional<Set<String>> origins() {
        return origins;
    }

    /**
     * Returns whether the input origin is allowed by this configuration.
     *
     * @return {@code true} if the origin is allowed, otherwise {@code false}
     */
    public boolean isOriginAllowed(final String origin) {
        if (origins.isPresent()) {
            return origins.get().contains(origin);
        } else if (pattern.isPresent()) {
            return pattern.get().matcher(origin).matches();
        }
        return false;
    }

    /**
     * Determines if credentials are supported for CORS requests.
     *
     * By default credentials are not included in CORS requests but if isCredentialsAllowed returns
     * true credentials will be added to CORS requests. Setting this value to true will set the
     * CORS 'Access-Control-Allow-Credentials' response header to true.
     *
     * Please note that credentials support needs to be enabled on the client side as well.
     * The client needs to opt-in to send credentials by calling:
     * <pre>
     * xhr.withCredentials = true;
     * </pre>
     * The default value for 'withCredentials' is false in which case no credentials are sent.
     * Setting this to true will included credentials in cross origin requests.
     *
     * @return {@code true} if credentials are supported.
     */
    public boolean isCredentialsAllowed() {
        return allowCredentials;
    }

    /**
     * Gets the maxAge setting.
     *
     * When making a preflight request the client has to perform two request with can be inefficient.
     * This setting will set the CORS 'Access-Control-Max-Age' response header and enables the
     * caching of the preflight response for the specified time. During this time no preflight
     * request will be made.
     *
     * @return {@code long} the time in seconds that a preflight request may be cached.
     */
    public long maxAge() {
        return maxAge;
    }

    /**
     * Returns the allowed set of Request Methods. The Http methods that should be returned in the
     * CORS 'Access-Control-Request-Method' response header.
     *
     * @return {@code Set} of {@link HttpMethod}s that represent the allowed Request Methods.
     */
    public Set<HttpMethod> allowedRequestMethods() {
        return Collections.unmodifiableSet(allowedRequestMethods);
    }

    /**
     * Returns the allowed set of Request Headers.
     *
     * The header names returned from this method will be used to set the CORS
     * 'Access-Control-Allow-Headers' response header.
     *
     * @return {@code Set<String>} of strings that represent the allowed Request Headers.
     */
    public Set<String> allowedRequestHeaders() {
        return Collections.unmodifiableSet(allowedRequestHeaders);
    }

    /**
     * Returns HTTP response headers that should be added to a CORS preflight response.
     *
     * @return {@link HttpHeaders} the HTTP response headers to be added.
     */
    public HttpHeaders preflightResponseHeaders() {
        if (preflightHeaders.isEmpty()) {
            return EmptyHttpHeaders.INSTANCE;
        }
        final HttpHeaders preflightHeaders = new DefaultHttpHeaders();
        for (Map.Entry<CharSequence, Callable<?>> entry : this.preflightHeaders.entrySet()) {
            final Object value = getValue(entry.getValue());
            if (value instanceof Iterable) {
                preflightHeaders.add(entry.getKey().toString(), (Iterable<?>) value);
            } else {
                preflightHeaders.add(entry.getKey().toString(), value);
            }
        }
        return preflightHeaders;
    }

    /**
     * Determines whether a CORS request should be rejected if it's invalid before being
     * further processing.
     *
     * CORS headers are set after a request is processed. This may not always be desired
     * and this setting will check that the Origin is valid and if it is not valid no
     * further processing will take place, and a error will be returned to the calling client.
     *
     * @return {@code true} if a CORS request should short-circuit upon receiving an invalid Origin header.
     */
    public boolean isShortCircuit() {
        return shortCircuit;
    }

    private static <T> T getValue(final Callable<T> callable) {
        try {
            return callable.call();
        } catch (final Exception e) {
            throw new IllegalStateException("Could not generate value for callable [" + callable + ']', e);
        }
    }

    @Override
    public String toString() {
        return  "CorsConfig[enabled=" + enabled +
            ", origins=" + origins +
            ", anyOrigin=" + anyOrigin +
            ", isCredentialsAllowed=" + allowCredentials +
            ", maxAge=" + maxAge +
            ", allowedRequestMethods=" + allowedRequestMethods +
            ", allowedRequestHeaders=" + allowedRequestHeaders +
            ", preflightHeaders=" + preflightHeaders + ']';
    }

}
