/*
 * 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.apache.synapse.transport.http.conn;

import java.util.Collections;
import java.util.LinkedHashSet;
import java.util.Locale;
import java.util.Set;
import java.util.Map;
import java.util.HashMap;
import java.util.HashSet;

import org.apache.axis2.AxisFault;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.apache.http.HttpHost;
import org.apache.http.auth.MalformedChallengeException;
import org.apache.http.auth.UsernamePasswordCredentials;

public class ProxyConfig {

    private static final Log log = LogFactory.getLog(ProxyConfig.class);
    private final HttpHost proxy;
    private final UsernamePasswordCredentials creds;
    private final Set<String> proxyBypass;

    // The set of known hosts to bypass proxy
    private Set<String> knownDirectHosts = new HashSet<String>();

     // The set of known hosts to go via proxy
    private Set<String> knownProxyHosts = new HashSet<String>();

    // Map to hold the known proxy profile configuration
    private Map<String, ProxyProfileConfig> knownProxyConfigMap = new HashMap<String,ProxyProfileConfig>();

    // Map to hold the custom proxy profile details
    private Map<String, ProxyProfileConfig> proxyProfileMap = new HashMap<String, ProxyProfileConfig>();

    public ProxyConfig(
            final HttpHost proxy,
            final UsernamePasswordCredentials creds,
            final String[] proxyBypass,
            final Map<String, ProxyProfileConfig> proxyProfileMap) {
        super();
        this.proxy = proxy;
        this.creds = creds;
        if (proxyBypass != null) {
            this.proxyBypass = new LinkedHashSet<String>(proxyBypass.length);
            for (String s: proxyBypass) {
                this.proxyBypass.add(s.trim().toLowerCase(Locale.US));
            }
        } else {
            this.proxyBypass = Collections.<String>emptySet();
        }

        if (proxyProfileMap != null) {
            this.proxyProfileMap = proxyProfileMap;
        } else {
            this.proxyProfileMap = Collections.emptyMap();
        }
    }

    public HttpHost getProxy() {
        return proxy;
    }

    public UsernamePasswordCredentials getCreds() {
        return creds;
    }

    public Set<String> getProxyBypass() {
        return proxyBypass;
    }

    /**
     * Selects the configured proxy server
     * @param target request endpoint
     * @return proxy host based on the proxy profile or http.proxyHost,
     *         null when no proxy is configured or if the target is matched with proxy bypass
     */
    public HttpHost selectProxy(final HttpHost target) {
        if (isProxyProfileConfigured()) {
            return getProxyForTargetHost(target.getHostName());
        }

        if (this.proxy != null) {
            if (knownProxyHosts.contains(target.getHostName().toLowerCase(Locale.US))) {
                return this.proxy;
            } else if (knownDirectHosts.contains(target.getHostName().toLowerCase(Locale.US))) {
                return null;
            } else {
                // we are encountering this host for the first time
                if (isBypass(target.getHostName().toLowerCase(Locale.US))) {
                    return null;
                } else {
                    return this.proxy;
                }
            }
        }
        return this.proxy;
    }

    /**
     * checks weather the proxy profile map is empty
     *
     * @return true if proxy profile map is not empty, false otherwise
     */
    public boolean isProxyProfileConfigured() {
        return !this.proxyProfileMap.isEmpty();
    }

    /**
     * select the appropriate proxy for the given targetHost
     *
     * @param targetHost targeted end point
     * @return proxy mapped for the end point, if not returns null
     */
    private HttpHost getProxyForTargetHost(String targetHost) {
        HttpHost proxy = null;

        ProxyProfileConfig proxyProfileForTargetHost = getProxyProfileForTargetHost(targetHost);
        if (proxyProfileForTargetHost != null) {
            proxy = proxyProfileForTargetHost.getProxy();
        }

        return proxy;
    }

    /**
     * Selects the appropriate proxyProfileConfiguration for the given targetHost
     *
     * First, checks in the knownProxyConfigMap and returns the proxyProfile. If the profile is not in the
     * knowProxyConfigMap checks the knowDirectHosts and returns null (since the targetHost is not associated with
     * any proxy).
     *
     * If the request hits the ESB for the first time, checks whether the default profile is configured. If so, a flag
     * is set true. Then the targetHost is matched against the proxyProfileMaps key set
     * i.e check any of the key patten is matching with the targetHost. If the targetHost is matched against a key then
     * calls getProxyProfileConfig(String, String) and returns the proxyProfileConfig.
     *
     * If the targetHost is not matched against the proxyProfileMap key set and default profile flag is set true
     * then calls getProxyProfileConfig(String, String) and returns the defaultProfile.
     *
     * @param targetHost request's targeted host
     * @return ProxyProfileConfig for the given targetHost
     */
    private ProxyProfileConfig getProxyProfileForTargetHost(String targetHost) {
        if (knownProxyConfigMap.containsKey(targetHost)) {
            return knownProxyConfigMap.get(targetHost);
        }

        if (knownDirectHosts.contains(targetHost)) {
            return null;
        }

        boolean defaultProfile = false;
        for (String key : proxyProfileMap.keySet()) {
            if ("*".equals(key)) {
                log.debug("Default proxy profile found");
                defaultProfile = true;
                continue;
            }
            if (targetHost.matches(key)) {
                return getProxyProfileConfig(targetHost, key);
            }
        }

        if (defaultProfile) {
            return getProxyProfileConfig(targetHost, "*");
        }

        return null;
    }

    /**
     * Selects the proxyProfile for the given key and gets the bypass set. Matches the targetHost against the
     * bypass set. If it is matched then adds the targetHost to the knownDirectHosts List and returns null.
     * Otherwise (i.e the targetHost is not matched in the bypass proxy) puts the proxyProfile against the targetHost
     * into the knownProxyConfigMap and returns the proxyProfileConfig
     *
     * @param targetHost request's targeted host
     * @param key proxyProfileMap's key, if default profile then the key is "*"
     * @return proxyProfileConfig
     */
    private ProxyProfileConfig getProxyProfileConfig(String targetHost, String key) {
        ProxyProfileConfig proxyProfileConfig = proxyProfileMap.get(key);
        Set<String> proxyByPass = proxyProfileConfig.getProxyByPass();
        for (String bypass : proxyByPass) {
            if (targetHost.matches(bypass)) {
                knownDirectHosts.add(targetHost);
                return null;
            }
        }
        knownProxyConfigMap.put(targetHost, proxyProfileConfig);
        return proxyProfileConfig;
    }


    /**
     * select the proxy credential for the targetHost
     *
     * @param targetHost targeted host
     * @return proxy credential for the given end point, if not returns null
     */
    public UsernamePasswordCredentials getCredentialsForTargetHost(String targetHost) {
        UsernamePasswordCredentials credentials = null;
        ProxyProfileConfig proxyProfileForTargetHost = getProxyProfileForTargetHost(targetHost);

        if (proxyProfileForTargetHost != null) {
            credentials = proxyProfileForTargetHost.getCredentials();
        }

        return credentials;
    }

    /**
     * returns appropriate log message based on the proxy configuration
     * whether loading proxy profile or single proxy server or no proxy configured
     *
     * @return log message
     */
    public String logProxyConfig() {
        if (isProxyProfileConfigured()) {
            return "HTTP Sender using proxy profile";
        }

        if (this.proxy != null) {
            return "HTTP Sender using Proxy " + getProxy() + " and  bypassing " + getProxyBypass();
        } else {
            return "No proxy configuration found";
        }
    }

    /**
     * checks whether proxy configured (either proxy profile or single server)
     *
     * @return true when either proxy profile is configure or default proxy server is configure, false otherwise
     */
    private boolean isProxyConfigured() {
        return proxy != null || isProxyProfileConfigured();
    }

    /**
     * checks the proxy configuration and profile configuration whether proxy is configured with credential
     *
     * @return true when at least one proxy has configured with credential, false otherwise
     */
    private boolean isProxyHasCredential() {
        if (!isProxyConfigured()) {
            return false;
        }

        if (!isProxyProfileConfigured()) {
            return getCreds() != null;
        }

        for (Map.Entry<String, ProxyProfileConfig> proxyProfile : this.proxyProfileMap.entrySet()) {
            if (proxyProfile.getValue().getCredentials() != null) {
                return true;
            }
        }
        return false;
    }

    /**
     * returns DefaultProxyAuthenticator if single proxy server is configured
     * ProfileProxyAuthenticator if proxy profile is configured
     *
     * @return ProxyAuthenticator, if proxy is not configured null
     * @throws AxisFault
     */
    public ProxyAuthenticator createProxyAuthenticator() throws AxisFault {
        if (!isProxyHasCredential()) {
            return null;
        }

        ProxyAuthenticator proxyAuthenticator;

        try {
            if (isProxyProfileConfigured()) {
                proxyAuthenticator = new ProfileProxyAuthenticator(this);
            } else {
                proxyAuthenticator = new DefaultProxyAuthenticator(getCreds());
            }

        } catch (MalformedChallengeException e) {
            throw new AxisFault("Error while creating proxy authenticator", e);
        }

        return proxyAuthenticator;
    }

    @Override
    public String toString() {
        return "[proxy=" + proxy + ", proxyCredential=" + creds + ", proxyBypass=" + proxyBypass +
                ", proxyProfileMap=" + proxyProfileMap + "]";
    }

    private boolean isBypass(String hostName) {
        for (String entry : this.proxyBypass) {
            if (hostName.matches(entry)) {
                knownDirectHosts.add(hostName);
                return true;
            }
        }
        knownProxyHosts.add(hostName);
        return false;
    }
}