/**
 *  Copyright 2005-2016 Red Hat, Inc.
 *
 *  Red Hat 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 io.fabric8.gateway.fabric.http;

import io.fabric8.api.FabricService;
import io.fabric8.api.scr.AbstractComponent;
import io.fabric8.api.scr.Configurer;
import io.fabric8.api.scr.ValidatingReference;
import io.fabric8.gateway.handlers.http.HttpMappingRule;
import io.fabric8.internal.Objects;
import io.fabric8.zookeeper.ZkPath;
import io.fabric8.zookeeper.internal.SimplePathTemplate;

import java.io.IOException;
import java.util.Map;

import org.apache.curator.framework.CuratorFramework;
import org.apache.curator.framework.recipes.cache.NodeCache;
import org.apache.curator.framework.recipes.cache.NodeCacheListener;
import org.apache.felix.scr.annotations.Activate;
import org.apache.felix.scr.annotations.Component;
import org.apache.felix.scr.annotations.ConfigurationPolicy;
import org.apache.felix.scr.annotations.Deactivate;
import org.apache.felix.scr.annotations.Modified;
import org.apache.felix.scr.annotations.Property;
import org.apache.felix.scr.annotations.PropertyOption;
import org.apache.felix.scr.annotations.Reference;
import io.fabric8.gateway.fabric.support.http.HttpMappingRuleBase;
import io.fabric8.gateway.fabric.support.http.HttpMappingZooKeeperTreeCache;
import io.fabric8.gateway.loadbalancer.LoadBalancer;
import io.fabric8.gateway.loadbalancer.LoadBalancers;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

/**
 * A mapping rule for use with the {@link FabricHTTPGateway}
 */
@Component(name = "io.fabric8.gateway.http.mapping", immediate = true, metatype = true, policy = ConfigurationPolicy.REQUIRE,
        label = "Fabric8 HTTP Gateway Mapping Rule",
        description = "Provides a mapping between part of the fabric cluster and a HTTP URI template")
public final class HttpMappingRuleConfiguration extends AbstractComponent {

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

    @Reference
    private Configurer configurer;
    @Reference(referenceInterface = CuratorFramework.class)
    private final ValidatingReference<CuratorFramework> curator = new ValidatingReference<CuratorFramework>();
    @Reference(referenceInterface = FabricHTTPGateway.class)
    private final ValidatingReference<FabricHTTPGateway> gateway = new ValidatingReference<FabricHTTPGateway>();
    @Reference(referenceInterface = FabricService.class)
    private final ValidatingReference<FabricService> fabricService = new ValidatingReference<FabricService>();

    @Property(name = "zooKeeperPath", value = "/fabric/registry/clusters/webapps",
            label = "ZooKeeper path", description = "The path in ZooKeeper which is monitored to discover the available web services or web applications")
    private String zooKeeperPath;

    @Property(name = "uriTemplate", value = "{contextPath}/",
            label = "URI template", description = "The URI template mapping the URI to the underlying service implementation.\nThis can use a number of URI template values such as 'contextPath', 'version', 'serviceName'")
    private String uriTemplate;

    @Property(name = "enabledVersion",
            label = "Enable version", description = "Specify the exact profile version to expose; if none is specified then the gateways current profile version is used.\nIf a {version} URI template is used then all versions are exposed.")
    private String enabledVersion;

    @Property(name = "reverseHeaders", boolValue = true,
            label = "Reverse headers", description = "If enabled then the URL in the Location, Content-Location and URI headers from the proxied HTTP responses are rewritten from the back end service URL to match the front end URL on the gateway.\nThis is equivalent to the ProxyPassReverse directive in mod_proxy.")
    private boolean reverseHeaders = true;

    @Property(name = "loadBalancerType",
            value = LoadBalancers.ROUND_ROBIN_LOAD_BALANCER,
            options = {
                    @PropertyOption(name = LoadBalancers.RANDOM_LOAD_BALANCER, value = "Random"),
                    @PropertyOption(name = LoadBalancers.ROUND_ROBIN_LOAD_BALANCER, value = "Round Robin"),
                    @PropertyOption(name = LoadBalancers.STICKY_LOAD_BALANCER, value = "Sticky")
            },
            label = "Load Balancer", description = "The kind of load balancing strategy used")
    private String loadBalancerType;

    @Property(name = "stickyLoadBalancerCacheSize", intValue = LoadBalancers.STICKY_LOAD_BALANCER_DEFAULT_CACHE_SIZE,
            label = "Sticky Load Balancer Cache Size", description = "The number of unique client keys to cache for the sticky load balancer (using an LRU caching algorithm)")
    private int stickyLoadBalancerCacheSize = LoadBalancers.STICKY_LOAD_BALANCER_DEFAULT_CACHE_SIZE;

    @Property(name = "immediateUpdate", boolValue = false,
            label = "Immediate Update", description = "Monitors ZooKeeper registry to immediately reflects updates. Keep this disabled if you are using roll up updates.")
    private boolean immediateUpdate = false;

    private NodeCache nodeCache;


    private HttpMappingRuleBase httpMappingRuleBase;

    private HttpMappingZooKeeperTreeCache mappingTree;

    @Activate
    void activate(Map<String, ?> configuration) throws Exception {
        activateComponent();
        updateConfiguration(configuration);
        initializeNodeCache(configuration);
    }

    @Modified
    void modified(Map<String, ?> configuration) throws Exception {
        deactivateInternal();
        updateConfiguration(configuration);
        initializeNodeCache(configuration);
    }

    @Deactivate
    void deactivate() {
        gateway.get().removeMappingRuleConfiguration(httpMappingRuleBase);
        httpMappingRuleBase = null;

        deactivateInternal();
        deactivateComponent();
    }

    String getZooKeeperPath() {
        return zooKeeperPath;
    }

    void setZooKeeperPath(String zooKeeperPath) {
        this.zooKeeperPath = zooKeeperPath;
    }

    String getEnabledVersion() {
        return enabledVersion;
    }

    void setEnabledVersion(String enabledVersion) {
        this.enabledVersion = enabledVersion;
    }

    String getUriTemplate() {
        return uriTemplate;
    }

    void setUriTemplate(String uriTemplate) {
        this.uriTemplate = uriTemplate;
    }

    String getLoadBalancerType() {
        return loadBalancerType;
    }

    void setLoadBalancerType(String loadBalancerType) {
        this.loadBalancerType = loadBalancerType;
    }

    private void updateConfiguration(Map<String, ?> configuration) throws Exception {
        LOG.info("activating http mapping rule " + configuration);
        configurer.configure(configuration, this);
        LOG.info("activating http mapping rule " + zooKeeperPath + " on " + gateway.get().getPort());

        String zkPath = getZooKeeperPath();
        Objects.notNull(zkPath, "zooKeeperPath");
        Objects.notNull(getUriTemplate(), "uriTemplate");

        LoadBalancer loadBalancer = LoadBalancers.createLoadBalancer(loadBalancerType, stickyLoadBalancerCacheSize);

        LOG.info("activating http mapping ZooKeeper path: " + zkPath + " with URI template: " + uriTemplate
                + " enabledVersion: " + enabledVersion + " with load balancer: " + loadBalancer);

        if (httpMappingRuleBase != null) {
            gateway.get().removeMappingRuleConfiguration(httpMappingRuleBase);
        }
        httpMappingRuleBase = new HttpMappingRuleBase(
                new SimplePathTemplate(uriTemplate),
                gateway.get().getGatewayVersion(),
                enabledVersion, loadBalancer, reverseHeaders);

        mappingTree = new HttpMappingZooKeeperTreeCache(curator.get(), httpMappingRuleBase, zooKeeperPath);
        mappingTree.init();

        gateway.get().addMappingRuleConfiguration(httpMappingRuleBase);
    }

    private void deactivateInternal() {
        if (mappingTree != null) {
            mappingTree.destroy();
            mappingTree = null;
        }

        if(nodeCache != null){
            try {
                nodeCache.close();
            } catch (IOException e) {
                LOG.warn("Possible problems when disabling immediateUpdate in HTTP Gateway rule ", e);
            }
            nodeCache = null;
        }
    }

    private void initializeNodeCache(final Map<String, ?> configuration) {
        if(immediateUpdate == true){
            if(nodeCache == null){
                final String containerName = fabricService.get().getCurrentContainer().getId();
                final String factoryPid = (String) configuration.get("fabric.zookeeper.pid");
                nodeCache = new NodeCache(curator.get(), ZkPath.CONFIG_CONTAINER.getPath(containerName));
                nodeCache.getListenable().addListener(new NodeCacheListener() {
                    @Override
                    public void nodeChanged() throws Exception {
                        LOG.info("Detected version change in container {} with http gateway profile {}.", containerName, factoryPid);
                        updateConfiguration(configuration);
                    }
                });
                try {
                    nodeCache.start();
                    LOG.info("Enabling immediateUpdate in HTTP Gateway on container {} for factoryPid={}.", containerName, factoryPid);
                } catch (Exception e) {
                    LOG.warn("Possible problems when enabling immediateUpdate in HTTP Gateway rule", e);
                }
            }
        } else {
            if (nodeCache != null){
                try {
                    LOG.info("Disabling immediateUpdate in HTTP Gateway");
                    nodeCache.close();
                    nodeCache = null;
                } catch (IOException e) {
                    LOG.warn("Possible problems when disabling immediateUpdate in HTTP Gateway", e);
                }
            }
        }

    }

    void bindCurator(CuratorFramework curator) {
        this.curator.bind(curator);
    }

    void unbindCurator(CuratorFramework curator) {
        this.curator.unbind(curator);
    }

    void bindGateway(FabricHTTPGateway gateway) {
        this.gateway.bind(gateway);
    }

    void unbindGateway(FabricHTTPGateway gateway) {
        this.gateway.unbind(gateway);
    }

    void bindFabricService(FabricService fabricService) {
        this.fabricService.bind(fabricService);
    }

    void unbindFabricService(FabricService fabricService) {
        this.fabricService.unbind(fabricService);
    }

    @Override
    public String toString() {
        return "HttpMappingRuleConfiguration{" +
                "zooKeeperPath='" + zooKeeperPath + '\'' +
                ", uriTemplate='" + uriTemplate + '\'' +
                ", enabledVersion='" + enabledVersion + '\'' +
                '}';
    }
}
