/*
 * Licensed to the Apache Software Foundation (ASF) under one
 * or more contributor license agreements.  See the NOTICE file
 * distributed with this work for additional information
 * regarding copyright ownership.  The ASF 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.brooklyn.entity.proxy.nginx;

import static java.lang.String.format;

import java.util.Collection;

import org.apache.brooklyn.entity.proxy.ProxySslConfig;
import org.apache.brooklyn.util.text.Strings;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import com.google.common.collect.LinkedHashMultimap;
import com.google.common.collect.Multimap;

/**
 * Generates the {@code server.conf} configuration file using sensors on an {@link NginxController}.
 */
public class NginxDefaultConfigGenerator implements NginxConfigFileGenerator {

    private static final Logger LOG = LoggerFactory.getLogger(NginxDefaultConfigGenerator.class);

    public NginxDefaultConfigGenerator() { }

    @Override
    public String generateConfigFile(NginxDriver driver, NginxController nginx) {
        StringBuilder config = new StringBuilder();
        config.append("\n");
        config.append(format("pid %s;\n", driver.getPidFile()));
        config.append("events {\n");
        config.append("  worker_connections 8196;\n");
        config.append("}\n");
        config.append("http {\n");

        ProxySslConfig globalSslConfig = nginx.getSslConfig();

        if (nginx.isSsl()) {
            verifyConfig(globalSslConfig);
            appendSslConfig("global", config, "    ", globalSslConfig, true, true);
        }

        // If no servers, then defaults to returning 404
        // TODO Give nicer page back
        if (nginx.getDomain()!=null || nginx.getServerPoolAddresses() == null || nginx.getServerPoolAddresses().isEmpty()) {
            config.append("  server {\n");
            config.append(getCodeForServerConfig());
            config.append("    listen "+nginx.getPort()+";\n");
            config.append(getCodeFor404());
            config.append("  }\n");
        }

        // For basic round-robin across the server-pool
        if (nginx.getServerPoolAddresses() != null && nginx.getServerPoolAddresses().size() > 0) {
            config.append(format("  upstream "+nginx.getId()+" {\n"));
            if (nginx.isSticky()){
                config.append("    sticky;\n");
            }
            for (String address : nginx.getServerPoolAddresses()) {
                config.append("    server "+address+";\n");
            }
            config.append("  }\n");
            config.append("  server {\n");
            config.append(getCodeForServerConfig());
            if (globalSslConfig != null) {
                appendCodeForProxySSLConfig(nginx.getId(), config, "    ", globalSslConfig);
            }
            config.append("    listen "+nginx.getPort()+";\n");
            if (nginx.getDomain()!=null)
                config.append("    server_name "+nginx.getDomain()+";\n");
            config.append("    location / {\n");
            config.append("      proxy_pass "+(globalSslConfig != null && globalSslConfig.getTargetIsSsl() ? "https" : "http")+"://"+nginx.getId()+";\n");
            config.append("    }\n");
            config.append("  }\n");
        }

        // For mapping by URL
        Iterable<UrlMapping> mappings = nginx.getUrlMappings();
        Multimap<String, UrlMapping> mappingsByDomain = LinkedHashMultimap.create();
        for (UrlMapping mapping : mappings) {
            Collection<String> addrs = mapping.getAttribute(UrlMapping.TARGET_ADDRESSES);
            if (addrs != null && addrs.size() > 0) {
                mappingsByDomain.put(mapping.getDomain(), mapping);
            }
        }

        for (UrlMapping um : mappings) {
            Collection<String> addrs = um.getAttribute(UrlMapping.TARGET_ADDRESSES);
            if (addrs != null && addrs.size() > 0) {
                config.append(format("  upstream "+um.getUniqueLabel()+" {\n"));
                if (nginx.isSticky()){
                    config.append("    sticky;\n");
                }
                for (String address: addrs) {
                    config.append("    server "+address+";\n");
                }
                config.append("  }\n");
            }
        }

        for (String domain : mappingsByDomain.keySet()) {
            config.append("  server {\n");
            config.append(getCodeForServerConfig());
            config.append("    listen "+nginx.getPort()+";\n");
            config.append("    server_name "+domain+";\n");
            boolean hasRoot = false;

            // set up SSL
            ProxySslConfig localSslConfig = null;
            for (UrlMapping mappingInDomain : mappingsByDomain.get(domain)) {
                ProxySslConfig sslConfig = mappingInDomain.getConfig(UrlMapping.SSL_CONFIG);
                if (sslConfig!=null) {
                    verifyConfig(sslConfig);
                    if (localSslConfig!=null) {
                        if (localSslConfig.equals(sslConfig)) {
                            //ignore identical config specified on multiple mappings
                        } else {
                            LOG.warn("{} mapping {} provides SSL config for {} when a different config had already been provided by another mapping, ignoring this one",
                                    new Object[] {this, mappingInDomain, domain});
                        }
                    } else if (globalSslConfig!=null) {
                        if (globalSslConfig.equals(sslConfig)) {
                            //ignore identical config specified on multiple mappings
                        } else {
                            LOG.warn("{} mapping {} provides SSL config for {} when a different config had been provided at root nginx scope, ignoring this one",
                                    new Object[] {this, mappingInDomain, domain});
                        }
                    } else {
                        //new config, is okay
                        localSslConfig = sslConfig;
                    }
                }
            }
            if (localSslConfig != null) {
                appendSslConfig(domain, config, "    ", localSslConfig, true, true);
                appendCodeForProxySSLConfig(domain, config, "    ", localSslConfig);
            }

            for (UrlMapping mappingInDomain : mappingsByDomain.get(domain)) {
                // TODO Currently only supports "~" for regex. Could add support for other options,
                // such as "~*", "^~", literals, etc.
                boolean isRoot = mappingInDomain.getPath()==null || mappingInDomain.getPath().length()==0 || mappingInDomain.getPath().equals("/");
                if (isRoot && hasRoot) {
                    LOG.warn(""+this+" mapping "+mappingInDomain+" provides a duplicate / proxy, ignoring");
                } else {
                    hasRoot |= isRoot;
                    String location = isRoot ? "/" : "~ " + mappingInDomain.getPath();
                    config.append("    location "+location+" {\n");
                    Collection<UrlRewriteRule> rewrites = mappingInDomain.getConfig(UrlMapping.REWRITES);
                    if (rewrites != null && rewrites.size() > 0) {
                        for (UrlRewriteRule rule: rewrites) {
                            config.append("      rewrite \"^"+rule.getFrom()+"$\" \""+rule.getTo()+"\"");
                            if (rule.isBreak()) config.append(" break");
                            config.append(" ;\n");
                        }
                    }
                    config.append("      proxy_pass "+
                        (localSslConfig != null && localSslConfig.getTargetIsSsl() ? "https" :
                         (localSslConfig == null && globalSslConfig != null && globalSslConfig.getTargetIsSsl()) ? "https" :
                         "http")+
                        "://"+mappingInDomain.getUniqueLabel()+" ;\n");
                    config.append("    }\n");
                }
            }
            if (!hasRoot) {
                //provide a root block giving 404 if there isn't one for this server
                config.append("    location / { \n"+getCodeFor404()+"    }\n");
            }
            config.append("  }\n");
        }

        config.append("}\n");

        return config.toString();
    }

    protected String getCodeForServerConfig() {
        // See http://wiki.nginx.org/HttpProxyModule
        return "" +
            // this prevents nginx from reporting version number on error pages
            "    server_tokens off;\n"+

            // this prevents nginx from using the internal proxy_pass codename as Host header passed upstream.
            // Not using $host, as that causes integration test to fail with a "connection refused" testing
            // url-mappings, at URL "http://localhost:${port}/atC0" (with a trailing slash it does work).
            "    proxy_set_header Host $http_host;\n"+

            // following added, as recommended for wordpress in:
            // http://zeroturnaround.com/labs/wordpress-protips-go-with-a-clustered-approach/#!/
            "    proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;\n"+
            "    proxy_set_header X-Real-IP $remote_addr;\n";
    }

    protected void appendCodeForProxySSLConfig(String id, StringBuilder out, String prefix, ProxySslConfig ssl) {
        if (ssl.getTargetIsSsl() && ssl.getVerifyClient()) {
            // Send 403 if not verified, otherwise pass on the client certificate we received
            out.append(prefix).append("if ($ssl_client_verify != SUCCESS) { return 403; }\n");
            out.append(prefix).append("proxy_set_header ssl.client_cert $ssl_client_cert;\n");

            // Use the configured SSL certificate and key for the proxied server
            String cert;
            if (Strings.isEmpty(ssl.getCertificateDestination())) {
                cert = "" + id + ".crt";
            } else {
                cert = ssl.getCertificateDestination();
            }
            out.append(prefix);
            out.append("proxy_ssl_certificate " + cert + ";\n");

            String key;
            if (!Strings.isEmpty(ssl.getKeyDestination())) {
                key = ssl.getKeyDestination();
            } else if (!Strings.isEmpty(ssl.getKeySourceUrl())) {
                key = "" + id + ".key";
            } else {
                key = null;
            }
            if (key != null) {
                out.append(prefix);
                out.append("proxy_ssl_certificate_key " + key + ";\n");
            }
        }
    }

    protected String getCodeFor404() {
        return "    return 404;\n";
    }

    protected void verifyConfig(ProxySslConfig proxySslConfig) {
          if(Strings.isEmpty(proxySslConfig.getCertificateDestination()) && Strings.isEmpty(proxySslConfig.getCertificateSourceUrl())){
            throw new IllegalStateException("ProxySslConfig can't have a null certificateDestination and null certificateSourceUrl. One or both need to be set");
        }
    }

    protected boolean appendSslConfig(String id, StringBuilder out, String prefix, ProxySslConfig ssl,
                                   boolean sslBlock, boolean certificateBlock) {
        if (ssl == null) return false;
        if (sslBlock) {
            out.append(prefix);
            out.append("ssl on;\n");
        }
        if (ssl.getReuseSessions()) {
            out.append(prefix);
            out.append("");
        }
        if (certificateBlock) {
            String cert;
            if (Strings.isEmpty(ssl.getCertificateDestination())) {
                cert = "" + id + ".crt";
            } else {
                cert = ssl.getCertificateDestination();
            }
            out.append(prefix);
            out.append("ssl_certificate " + cert + ";\n");

            String key;
            if (!Strings.isEmpty(ssl.getKeyDestination())) {
                key = ssl.getKeyDestination();
            } else if (!Strings.isEmpty(ssl.getKeySourceUrl())) {
                key = "" + id + ".key";
            } else {
                key = null;
            }
            if (key != null) {
                out.append(prefix);
                out.append("ssl_certificate_key " + key + ";\n");
            }

            if (ssl.getVerifyClient()) {
                out.append("ssl_verify_client on;\n");

                String client;
                if (Strings.isEmpty(ssl.getClientCertificateDestination())) {
                    client = "" + id + ".cli";
                } else {
                    client = ssl.getClientCertificateDestination();
                }
                if (client != null) {
                    out.append(prefix);
                    out.append("ssl_client_certificate " + client + ";\n");
                }
            }

            out.append("ssl_protocols TLSv1 TLSv1.1 TLSv1.2;\n");
        }
        return true;
    }

    @Override
    public String toString(){
        return getClass().getName();
    }

}
