/**
 *  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.service.jclouds;

import static io.fabric8.internal.ContainerProviderUtils.buildStartScript;
import static io.fabric8.internal.ContainerProviderUtils.buildStopScript;

import java.net.MalformedURLException;
import java.net.URISyntaxException;
import java.util.Map;
import java.util.Set;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.ConcurrentMap;

import org.apache.curator.framework.CuratorFramework;
import org.apache.felix.scr.annotations.Component;
import org.apache.felix.scr.annotations.Properties;
import org.apache.felix.scr.annotations.Property;
import org.apache.felix.scr.annotations.Reference;
import org.apache.felix.scr.annotations.ReferenceCardinality;
import org.apache.felix.scr.annotations.ReferencePolicy;
import org.apache.felix.scr.annotations.Service;
import io.fabric8.api.Container;
import io.fabric8.api.ContainerProvider;
import io.fabric8.api.CreateContainerMetadata;
import io.fabric8.api.CreationStateListener;
import io.fabric8.api.jcip.GuardedBy;
import io.fabric8.api.jcip.ThreadSafe;
import io.fabric8.api.scr.AbstractComponent;
import io.fabric8.api.scr.ValidatingReference;
import io.fabric8.api.visibility.VisibleForExternal;
import io.fabric8.internal.ContainerProviderUtils;
import io.fabric8.service.jclouds.firewall.FirewallManagerFactory;
import io.fabric8.service.jclouds.functions.ToRunScriptOptions;
import io.fabric8.service.jclouds.functions.ToTemplate;
import io.fabric8.service.jclouds.internal.CloudUtils;
import org.jclouds.compute.ComputeService;
import org.jclouds.compute.RunNodesException;
import org.jclouds.compute.domain.ExecResponse;
import org.jclouds.compute.domain.NodeMetadata;
import org.jclouds.compute.domain.Template;
import org.jclouds.compute.options.RunScriptOptions;
import org.jclouds.karaf.core.CredentialStore;
import org.osgi.framework.BundleContext;
import org.osgi.service.cm.ConfigurationAdmin;
import org.osgi.service.component.annotations.Activate;
import org.osgi.service.component.annotations.Deactivate;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import com.google.common.base.Optional;
import com.google.common.base.Strings;

/**
 * A concrete {@link io.fabric8.api.ContainerProvider} that creates {@link io.fabric8.api.Container}s via jclouds {@link ComputeService}.
 */
@ThreadSafe
@Component(name = "io.fabric8.container.provider.jclouds", label = "Fabric8 Jclouds Container Provider", immediate = true, metatype = false)
@Service(ContainerProvider.class)
@Properties(
        @Property(name = "fabric.container.protocol", value = JcloudsContainerProvider.SCHEME)
)
public class JcloudsContainerProvider extends AbstractComponent implements ContainerProvider<CreateJCloudsContainerOptions, CreateJCloudsContainerMetadata> {

    private static final Logger LOGGER = LoggerFactory.getLogger(JcloudsContainerProvider.class);

    private static final String NODE_CREATED_FORMAT = "Node %s has been succesfully created.";
    private static final String NODE_ERROR_FORMAT = "Error creating node %s. Status: .";
    private static final String OVERVIEW_FORMAT = "Creating %s nodes on %s. It may take a while ...";


     static final String SCHEME = "jclouds";

    @Reference(cardinality = ReferenceCardinality.OPTIONAL_MULTIPLE, bind = "bindComputeService", unbind = "unbindComputeService", referenceInterface = ComputeService.class, policy = ReferencePolicy.DYNAMIC)
    private final ConcurrentMap<String, ComputeService> computeServiceMap = new ConcurrentHashMap<String, ComputeService>();

    @Reference(referenceInterface = ComputeRegistry.class)
    private final ValidatingReference<ComputeRegistry> computeRegistry = new ValidatingReference<ComputeRegistry>();
    @Reference(referenceInterface = FirewallManagerFactory.class)
    private final ValidatingReference<FirewallManagerFactory> firewallManagerFactory = new ValidatingReference<FirewallManagerFactory>();
    @Reference(referenceInterface = CredentialStore.class)
    private final ValidatingReference<CredentialStore> credentialStore = new ValidatingReference<CredentialStore>();
    @Reference(referenceInterface = ConfigurationAdmin.class)
    private final ValidatingReference<ConfigurationAdmin> configAdmin = new ValidatingReference<ConfigurationAdmin>();
    @Reference(referenceInterface = CuratorFramework.class)
    private final ValidatingReference<CuratorFramework> curator = new ValidatingReference<CuratorFramework>();

    @GuardedBy("volatile & assertValid()") private volatile BundleContext bundleContext;

    @Activate
    void activate(BundleContext bundleContext) {
        this.bundleContext = bundleContext;
        activateComponent();
    }

    @Deactivate
    void deactivate() {
        deactivateComponent();
    }

    @Override
    public CreateJCloudsContainerOptions.Builder newBuilder() {
        return CreateJCloudsContainerOptions.builder();
    }

    @Override
    public CreateJCloudsContainerMetadata create(CreateJCloudsContainerOptions input, CreationStateListener listener) throws MalformedURLException, RunNodesException, URISyntaxException, InterruptedException {
        assertValid();
        CreateJCloudsContainerOptions options = input.updateComputeService(getOrCreateComputeService(input));

        listener.onStateChange("Looking up for compute service.");
        ComputeService computeService = getOrCreateComputeService(options);
        if (computeService == null) {
            throw new IllegalStateException("Compute service could not be found or created.");
        }

        Template template = ToTemplate.apply(options);

        listener.onStateChange(String.format(OVERVIEW_FORMAT, 1, options.getContextName()));

        try {
            Set<? extends NodeMetadata> metadata = computeService.createNodesInGroup(options.getGroup(), 1, template);
            if (metadata == null || metadata.size() != 1) {
                throw new IllegalStateException("JClouds created " + metadata.size() + " containers instead of 1");
            }
            NodeMetadata nodeMetadata = metadata.iterator().next();
            switch (nodeMetadata.getStatus()) {
                case RUNNING:
                    listener.onStateChange(String.format(NODE_CREATED_FORMAT, nodeMetadata.getName()));
                    break;
                default:
                    listener.onStateChange(String.format(NODE_ERROR_FORMAT, nodeMetadata.getStatus()));
            }

            CloudContainerInstallationTask installationTask = new CloudContainerInstallationTask(
                    options.getName(),
                    nodeMetadata,
                    options,
                    computeService,
                    firewallManagerFactory.get(),
                    template.getOptions(),
                    listener);
            return installationTask.install();
        } catch (Throwable ex) {
            CreateJCloudsContainerMetadata failureMetadata = new CreateJCloudsContainerMetadata();
            failureMetadata.setCreateOptions(options);
            failureMetadata.setFailure(ex);
            return failureMetadata;
        }
    }

    @Override
    public void start(Container container) {
        assertValid();
        CreateContainerMetadata metadata = container.getMetadata();
        if (!(metadata instanceof CreateJCloudsContainerMetadata)) {
            throw new IllegalStateException("Container doesn't have valid create container metadata type");
        } else {
            CreateJCloudsContainerMetadata jCloudsContainerMetadata = (CreateJCloudsContainerMetadata) metadata;
            CreateJCloudsContainerOptions options = jCloudsContainerMetadata.getCreateOptions();
            ComputeService computeService = getOrCreateComputeService(options);
            try {

                String nodeId = jCloudsContainerMetadata.getNodeId();
                Optional<RunScriptOptions> runScriptOptions = ToRunScriptOptions.withComputeService(computeService).apply(jCloudsContainerMetadata);
                String script = buildStartScript(container.getId(), options);
                ExecResponse response;

                if (runScriptOptions.isPresent()) {
                    response = computeService.runScriptOnNode(nodeId, script, runScriptOptions.get());
                } else {
                    response = computeService.runScriptOnNode(nodeId, script);
                }

                if (response == null) {
                    jCloudsContainerMetadata.setFailure(new Exception("No response received for fabric install script."));
                } else if (response.getOutput() != null && response.getOutput().contains(ContainerProviderUtils.FAILURE_PREFIX)) {
                    jCloudsContainerMetadata.setFailure(new Exception(ContainerProviderUtils.parseScriptFailure(response.getOutput())));
                }
            } catch (Throwable t) {
                jCloudsContainerMetadata.setFailure(t);
            }
        }
    }

    @Override
    public void stop(Container container) {
        assertValid();
        CreateContainerMetadata metadata = container.getMetadata();
        if (!(metadata instanceof CreateJCloudsContainerMetadata)) {
            throw new IllegalStateException("Container doesn't have valid create container metadata type");
        } else {
            CreateJCloudsContainerMetadata jCloudsContainerMetadata = (CreateJCloudsContainerMetadata) metadata;
            CreateJCloudsContainerOptions options = jCloudsContainerMetadata.getCreateOptions();
            try {
                ComputeService computeService = getOrCreateComputeService(options);
                String nodeId = jCloudsContainerMetadata.getNodeId();
                Optional<RunScriptOptions> runScriptOptions = ToRunScriptOptions.withComputeService(computeService).apply(jCloudsContainerMetadata);
                String script = buildStopScript(container.getId(), options);
                ExecResponse response;

                container.setProvisionResult(Container.PROVISION_STOPPING);
                if (runScriptOptions.isPresent()) {
                    response = computeService.runScriptOnNode(nodeId, script, runScriptOptions.get());
                } else {
                    response = computeService.runScriptOnNode(nodeId, script);
                }

                if (response == null) {
                    jCloudsContainerMetadata.setFailure(new Exception("No response received for fabric install script."));
                } else if (response.getOutput() != null && response.getOutput().contains(ContainerProviderUtils.FAILURE_PREFIX)) {
                    jCloudsContainerMetadata.setFailure(new Exception(ContainerProviderUtils.parseScriptFailure(response.getOutput())));
                }
            } catch (Throwable t) {
                container.setProvisionResult(Container.PROVISION_STOPPED);
                jCloudsContainerMetadata.setFailure(t);
            }
        }
    }

    @Override
    public void destroy(Container container) {
        assertValid();
        CreateContainerMetadata metadata = container.getMetadata();
        if (!(metadata instanceof CreateJCloudsContainerMetadata)) {
            throw new IllegalStateException("Container doesn't have valid create container metadata type");
        } else {
            container.setProvisionResult(Container.PROVISION_DELETING);
            CreateJCloudsContainerMetadata jCloudsContainerMetadata = (CreateJCloudsContainerMetadata) metadata;
            CreateJCloudsContainerOptions options = jCloudsContainerMetadata.getCreateOptions();
            String nodeId = jCloudsContainerMetadata.getNodeId();
            ComputeService computeService = getOrCreateComputeService(options);
            computeService.destroyNode(nodeId);
        }
    }

    /**
     * Gets an existing {@link ComputeService} that matches configuration or creates a new one.
     */
    private synchronized ComputeService getOrCreateComputeService(CreateJCloudsContainerOptions options) {
        ComputeService computeService = null;
        if (options != null) {
            computeService = options.getComputeService();
            if (computeService == null && options.getContextName() != null) {
                computeService = computeRegistry.get().getIfPresent(options.getContextName());
            }
            if (computeService == null) {
//                listener.onStateChange("Compute Service not found. Creating ...");
                //validate options and make sure a compute service can be created.
                if (Strings.isNullOrEmpty(options.getProviderName()) || Strings.isNullOrEmpty(options.getIdentity()) || Strings.isNullOrEmpty(options.getCredential())) {
                    throw new IllegalArgumentException("Cannot create compute service. A registered cloud provider or the provider name, identity and credential options are required");
                }

                Map<String, String> serviceOptions = options.getServiceOptions();
                try {
                    if (options.getProviderName() != null) {
                        CloudUtils.registerProvider(curator.get(), configAdmin.get(), options.getContextName(), options.getProviderName(), options.getIdentity(), options.getCredential(), serviceOptions);
                    } else if (options.getApiName() != null) {
                        CloudUtils.registerApi(curator.get(), configAdmin.get(), options.getContextName(), options.getApiName(), options.getEndpoint(), options.getIdentity(), options.getCredential(), serviceOptions);
                    }
                    computeService = CloudUtils.waitForComputeService(bundleContext, options.getContextName());
                } catch (Exception e) {
                    LOGGER.warn("Did not manage to register compute cloud provider.");
                }
            }
        }
        return computeService;
    }

    @Override
    public String getScheme() {
        return SCHEME;
    }

    @Override
    public boolean isValidProvider() {
        return true;
    }

    @Override
    public Class<CreateJCloudsContainerOptions> getOptionsType() {
        return CreateJCloudsContainerOptions.class;
    }

    @Override
    public Class<CreateJCloudsContainerMetadata> getMetadataType() {
        return CreateJCloudsContainerMetadata.class;
    }

    @VisibleForExternal
    public void bindFirewallManagerFactory(FirewallManagerFactory factory) {
        this.firewallManagerFactory.bind(factory);
    }

    @VisibleForExternal
    public void unbindFirewallManagerFactory(FirewallManagerFactory factory) {
        this.firewallManagerFactory.unbind(factory);
    }

    void bindCredentialStore(CredentialStore credentialStore) {
        this.credentialStore.bind(credentialStore);
    }

    void unbindCredentialStore(CredentialStore credentialStore) {
        this.credentialStore.unbind(credentialStore);
    }

    void bindComputeRegistry(ComputeRegistry service) {
        this.computeRegistry.bind(service);
    }

    void unbindComputeRegistry(ComputeRegistry service) {
        this.computeRegistry.unbind(service);
    }

    void bindConfigAdmin(ConfigurationAdmin service) {
        this.configAdmin.bind(service);
    }

    void unbindConfigAdmin(ConfigurationAdmin service) {
        this.configAdmin.unbind(service);
    }

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

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

    void bindComputeService(ComputeService computeService) {
        String name = computeService.getContext().unwrap().getName();
        if (name != null) {
            computeServiceMap.put(name, computeService);
        }
    }

    void unbindComputeService(ComputeService computeService) {
        String serviceId = computeService.getContext().unwrap().getName();
        if (serviceId != null) {
            computeServiceMap.remove(serviceId);
        }
    }
}
