/**
 *  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.itests.paxexam.support;

import io.fabric8.api.Container;
import io.fabric8.api.ContainerProvider;
import io.fabric8.api.CreateChildContainerOptions;
import io.fabric8.api.CreateContainerBasicOptions;
import io.fabric8.api.FabricComplete;
import io.fabric8.api.FabricException;
import io.fabric8.api.FabricService;
import io.fabric8.api.GitContext;
import io.fabric8.api.ServiceProxy;
import io.fabric8.git.GitDataStore;
import io.fabric8.git.GitService;
import io.fabric8.service.jclouds.CreateJCloudsContainerOptions;
import io.fabric8.service.ssh.CreateSshContainerOptions;
import io.fabric8.tooling.testing.pax.exam.karaf.ServiceLocator;

import java.util.Arrays;
import java.util.Collection;
import java.util.Collections;
import java.util.HashSet;
import java.util.Set;
import java.util.concurrent.CompletionService;
import java.util.concurrent.ExecutorCompletionService;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.Future;
import java.util.concurrent.TimeUnit;

import org.eclipse.jgit.transport.PushResult;
import org.osgi.framework.BundleContext;
import org.osgi.framework.FrameworkUtil;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

public abstract class ContainerBuilder<T extends ContainerBuilder, B extends CreateContainerBasicOptions.Builder> {

    public static Logger LOG = LoggerFactory.getLogger(ContainerBuilder.class);

    public static final Long CREATE_TIMEOUT = 10 * 60000L;
    public static final Long PROVISION_TIMEOUT = 5 * 60000L;
    public static final String CONTAINER_TYPE_PROPERTY = "FABRIC_ITEST_CONTAINER_TYPE";
    public static final String CONTAINER_NUMBER_PROPERTY = "FABRIC_ITEST_CONTAINER_NUMBER";

    private final B optionsBuilder;
    private final Set<String> profileNames = new HashSet<String>();
    private final ServiceProxy<FabricService> fabricServiceServiceProxy;

    private boolean waitForProvisioning;
    private boolean assertProvisioningResult;

    private long provisionTimeOut = PROVISION_TIMEOUT;
    private ExecutorService executorService = Executors.newCachedThreadPool();


    protected ContainerBuilder(ServiceProxy<FabricService> proxy, B optionsBuilder) {
        this.optionsBuilder = optionsBuilder;
        this.fabricServiceServiceProxy = proxy;
    }

    public static ContainerBuilder create(ServiceProxy<FabricService> proxy) {
        return create(proxy, 1);
    }

    public static ContainerBuilder create(ServiceProxy<FabricService> proxy, int minimumNumber) {
        return create(proxy, minimumNumber, 0);
    }

    public static ContainerBuilder create(ServiceProxy<FabricService> proxy, int minimumNumber, int maximumNumber) {
        String containerType = System.getProperty(CONTAINER_TYPE_PROPERTY, "child");
        int numberOfContainers = Math.max(minimumNumber, Integer.parseInt(System.getProperty(CONTAINER_NUMBER_PROPERTY, "1")));

        if (maximumNumber < numberOfContainers && maximumNumber != 0) {
            numberOfContainers = minimumNumber;
        }

        if ("child".equals(containerType)) {
            return child(proxy, numberOfContainers);
        } else if ("jclouds".equals(containerType)) {
            return jclouds(proxy, numberOfContainers);
        } else if ("ssh".equals(containerType)) {
            return ssh(proxy, numberOfContainers);
        } else {
            return child(proxy,1);
        }
    }

    /**
     * Creates a {@link ChildContainerBuilder}.
     *
     * @return
     */
    public static ChildContainerBuilder child(ServiceProxy<FabricService> proxy) {
        return child(proxy, 1);
    }

    /**
     * Creates a {@link ChildContainerBuilder}.
     *
     * @param numberOfContainers The number of {@link Container}s that the builder will create.
     * @return
     */
    public static ChildContainerBuilder child(ServiceProxy<FabricService> proxy, int numberOfContainers) {
        return new ChildContainerBuilder(proxy, CreateChildContainerOptions.builder().number(numberOfContainers));
    }

    /**
     * Creates a {@link JcloudsContainerBuilder}.
     *
     * @return
     */
    public static JcloudsContainerBuilder jclouds(ServiceProxy<FabricService> proxy) {
        return new JcloudsContainerBuilder(proxy, CreateJCloudsContainerOptions.builder());
    }

    /**
     * Creates a {@link JcloudsContainerBuilder}
     *
     * @param numberOfContainers The number of {@link Container}s the builder will create.
     * @return
     */
    public static JcloudsContainerBuilder jclouds(ServiceProxy<FabricService> proxy, int numberOfContainers) {
        return new JcloudsContainerBuilder(proxy, CreateJCloudsContainerOptions.builder().number(numberOfContainers));
    }

    /**
     * Creates a {@link SshContainerBuilder}.
     *
     * @return
     */
    public static SshContainerBuilder ssh(ServiceProxy<FabricService> proxy) {
        return new SshContainerBuilder(proxy, CreateSshContainerOptions.builder());
    }

    /**
     * Creates an {@link SshContainerBuilder}.
     *
     * @param numberOfContainers The number of contaienrs the builder will create.
     * @return
     */
    public static SshContainerBuilder ssh(ServiceProxy<FabricService> proxy, int numberOfContainers) {
        return new SshContainerBuilder(proxy, CreateSshContainerOptions.builder().number(1));
    }

    public ContainerBuilder<T, B> withName(String name) {
        getOptionsBuilder().name(name);
        return this;
    }

    public ContainerBuilder<T, B> withJvmOpts(String jvmOpts) {
        getOptionsBuilder().jvmOpts(jvmOpts);
        return this;
    }

    public ContainerBuilder<T, B> withResolver(String resolver) {
        getOptionsBuilder().resolver(resolver);
        return this;
    }

    public ContainerBuilder<T, B> withProfiles(String profile) {
        profileNames.add(profile);
        return this;
    }

    public ContainerBuilder<T, B> waitForProvisioning() {
        this.waitForProvisioning = true;
        return this;
    }

    public ContainerBuilder<T, B> waitForProvisioning(Long timeout) {
        this.waitForProvisioning = true;
        this.provisionTimeOut = timeout;
        return this;
    }

    public ContainerBuilder<T, B> assertProvisioningResult() {
        this.assertProvisioningResult = true;
        return this;
    }

    public B getOptionsBuilder() {
        return optionsBuilder;
    }


    /**
     * Create the containers.
     */
    public Set<ContainerProxy> build(Collection<B> buildersList) {
        return buildInternal(buildersList);
    }

    /**
     * Create the containers.
     */
    public Set<ContainerProxy> build() {
        return buildInternal(Arrays.<B> asList(getOptionsBuilder()));
    }

    private Set<ContainerProxy> buildInternal(Collection<B> buildersList) {
        Set<ContainerProxy> containers = new HashSet<ContainerProxy>();
        BundleContext bundleContext = getBundleContext();
        ServiceLocator.awaitService(getBundleContext(), FabricComplete.class);
        synchronizeGitRepositories();
        FabricService fabricService = fabricServiceServiceProxy.getService();
        CompletionService<Set<ContainerProxy>> completionService = new ExecutorCompletionService<Set<ContainerProxy>>(executorService);

        int tasks = 0;
        for (B builder : buildersList) {
            builder.profiles(profileNames);
            if (!builder.isEnsembleServer()) {
                builder.zookeeperUrl(fabricService.getZookeeperUrl());
                CreateContainerBasicOptions options = builder.build();
                ServiceLocator.awaitService(bundleContext, ContainerProvider.class, "(fabric.container.protocol="+ options.getProviderType()+")",CREATE_TIMEOUT, TimeUnit.MILLISECONDS);
                completionService.submit(new CreateContainerTask(fabricServiceServiceProxy, options));
                tasks++;
            }
        }
        try {
            for (int i = 0; i < tasks; i++) {
                Future<Set<ContainerProxy>> futureContainerSet = completionService.poll(CREATE_TIMEOUT, TimeUnit.MILLISECONDS);
                Set<ContainerProxy> containerSet = futureContainerSet.get();
                containers.addAll(containerSet);
            }
            try {
                if (waitForProvisioning) {
                    Provision.containerStatus(containers, provisionTimeOut);
                }
                if (assertProvisioningResult) {
                    Provision.provisioningSuccess(containers, provisionTimeOut, ContainerCallback.DISPLAY_ALL);
                }
            } catch (Exception e) {
                throw FabricException.launderThrowable(e);
            }
        } catch (Exception e) {
            throw FabricException.launderThrowable(e);
        }

        return Collections.unmodifiableSet(containers);
    }

    /**
     * ENTESB-6368: Ensures that local git repository is synchronized with fabric-wide git repository.
     */
    protected void synchronizeGitRepositories() {
        GitService gitService = io.fabric8.api.gravia.ServiceLocator.awaitService(GitService.class);
        GitDataStore gitDataStore = io.fabric8.api.gravia.ServiceLocator.awaitService(GitDataStore.class);

        for (int i = 0; i < 10; i++) {
            try {
                LOG.info("Synchronizing Git repositories");
                Iterable<PushResult> results = gitDataStore.doPush(gitService.getGit(), new GitContext().requirePush());
                if (results.iterator().hasNext()) {
                    return;
                }
                throw new Exception("No reference was pushed");
            } catch (Exception e) {
                LOG.warn("Synchronization of Git repositories failed: " + e.getMessage());
                try {
                    Thread.sleep(1000);
                } catch (InterruptedException ie) {
                    Thread.currentThread().interrupt();
                    throw new RuntimeException(ie);
                }
            }
        }
    }

    /**
     * Destroy the given containers
     */
    public static void destroy(Set<? extends Container> containers) {
        ServiceProxy<FabricService> fabricProxy = ServiceProxy.createServiceProxy(getBundleContext(), FabricService.class);
        try {
            FabricService fabricService = fabricProxy.getService();
            for (Container aux : containers) {
                try {
                    //We want to use the latest metadata
                    Container container = fabricService.getContainer(aux.getId());
                    container.destroy(true);
                } catch (Exception ex) {
                    //noop
                }
            }
        } finally {
            fabricProxy.close();
        }
    }

    /**
     * Stop the given containers.
     * The container directory will not get deleted.
     */
    public static void stop(Set<? extends Container> containers) {
        ServiceProxy<FabricService> fabricProxy = ServiceProxy.createServiceProxy(getBundleContext(), FabricService.class);
        try {
            FabricService fabricService = fabricProxy.getService();
            for (Container aux : containers) {
                try {
                    //We want to use the latest metadata
                    Container updated = fabricService.getContainer(aux.getId());
                    updated.stop(true);
                } catch (Exception ex) {
                    ex.printStackTrace(System.err);
                    //noop
                }
            }
        } finally {
            fabricProxy.close();
        }
    }

    static BundleContext getBundleContext() {
        return FrameworkUtil.getBundle(ContainerBuilder.class).getBundleContext();
    }

}
