/**
 *  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.openshift.agent;

import java.io.File;
import java.net.MalformedURLException;
import java.util.Arrays;
import java.util.List;
import java.util.Map;
import java.util.concurrent.Callable;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.ScheduledExecutorService;
import java.util.concurrent.TimeUnit;

import com.openshift.client.IApplication;
import com.openshift.client.IOpenShiftConnection;
import com.openshift.client.IOpenShiftSSHKey;

import org.apache.curator.framework.CuratorFramework;
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.Reference;
import org.eclipse.jgit.errors.UnsupportedCredentialItem;
import org.eclipse.jgit.transport.CredentialItem;
import org.eclipse.jgit.transport.CredentialsProvider;
import org.eclipse.jgit.transport.URIish;

import io.fabric8.common.util.Maps;
import io.fabric8.common.util.Strings;
import io.fabric8.agent.download.DownloadManager;
import io.fabric8.agent.download.DownloadManagers;
import io.fabric8.api.Container;
import io.fabric8.api.DataStore;
import io.fabric8.api.FabricService;
import io.fabric8.api.Profile;
import io.fabric8.api.Profiles;
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.groups.Group;
import io.fabric8.groups.GroupListener;
import io.fabric8.groups.internal.ZooKeeperGroup;
import io.fabric8.openshift.CreateOpenshiftContainerOptions;
import io.fabric8.openshift.OpenShiftConstants;
import io.fabric8.openshift.OpenShiftUtils;
import io.fabric8.zookeeper.ZkPath;

import org.osgi.service.cm.ConfigurationAdmin;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

/**
 * A Fabric agent which manages arbitrary Java cartridges on OpenShift; so that changes to the Fabric profile
 * metadata (such as WARs, bundles, features) leads to the git configuration being updated for the managed
 * cartridges.
 */
@ThreadSafe
@Component(name = "io.fabric8.openshift.agent", label = "Fabric8 agent for deploying applications into external OpenShift cartridges", policy = ConfigurationPolicy.OPTIONAL, immediate = true, metatype = true)
public final class OpenShiftDeployAgent extends AbstractComponent implements GroupListener<ControllerNode> {

    private static final Logger LOGGER = LoggerFactory.getLogger(OpenShiftDeployAgent.class);
    private static final String REALM_PROPERTY_NAME = "realm";
    private static final String ROLE_PROPERTY_NAME = "role";
    private static final String DEFAULT_REALM = "karaf";
    private static final String DEFAULT_ROLE = "admin";

    @Reference(referenceInterface = ConfigurationAdmin.class)
    private final ValidatingReference<ConfigurationAdmin> configAdmin = new ValidatingReference<ConfigurationAdmin>();
    @Reference(referenceInterface = CuratorFramework.class)
    private final ValidatingReference<CuratorFramework> curator = new ValidatingReference<CuratorFramework>();
    @Reference(referenceInterface = FabricService.class)
    private final ValidatingReference<FabricService> fabricService = new ValidatingReference<FabricService>();

    private final ScheduledExecutorService downloadExecutor = Executors.newSingleThreadScheduledExecutor();
    private final Runnable runnable = new Runnable() {
        @Override
        public void run() {
            onConfigurationChanged();
        }
    };

    @GuardedBy("volatile") private volatile Group<ControllerNode> group;

    @Activate
    void activate(Map<String, ?> configuration) {
        //this.realm =  properties != null && properties.containsKey(REALM_PROPERTY_NAME) ? properties.get(REALM_PROPERTY_NAME) : DEFAULT_REALM;
        //this.role =  properties != null && properties.containsKey(ROLE_PROPERTY_NAME) ? properties.get(ROLE_PROPERTY_NAME) : DEFAULT_ROLE;
        group = new ZooKeeperGroup(curator.get(), ZkPath.OPENSHIFT.getPath(), ControllerNode.class);
        group.add(this);
        group.update(createState());
        group.start();
        activateComponent();
    }

    @Deactivate
    void deactivate() {
        deactivateComponent();
        try {
            if (group != null) {
                group.close();
            }
        } catch (Exception e) {
            LOGGER.warn("Failed to remove git server from registry.", e);
        }
        shutdownExecutor(downloadExecutor);
    }

    private void shutdownExecutor(ExecutorService executor) {
        executor.shutdown();
        try {
            executor.awaitTermination(1, TimeUnit.MINUTES);
        } catch (InterruptedException e) {
            // Ignore
        }
        executor.shutdownNow();
    }

    @Override
    public void groupEvent(Group<ControllerNode> group, GroupEvent event) {
        if (isValid()) {
            if (group.isMaster()) {
                LOGGER.info("OpenShiftDeployAgent is the master");
            } else {
                LOGGER.info("OpenShiftDeployAgent is not the master");
            }
            try {
                DataStore dataStore = null;
                if (fabricService != null) {
                    dataStore = fabricService.get().adapt(DataStore.class);
                } else {
                    LOGGER.warn("No fabricService yet!");
                }
                if (group.isMaster()) {
                    ControllerNode state = createState();
                    group.update(state);
                }
                if (dataStore != null) {
                    if (group.isMaster()) {
                        dataStore.trackConfiguration(runnable);
                        onConfigurationChanged();
                    } else {
                        dataStore.untrackConfiguration(runnable);
                    }
                }
            } catch (IllegalStateException e) {
                // Ignore
            }
        }
    }

    protected void onConfigurationChanged() {
        LOGGER.info("Configuration has changed; so checking the Fabric managed Java cartridges on OpenShift are up to date");
        Container[] containers = fabricService.get().getContainers();
        for (Container container : containers) {
            Profile effectiveProfile = Profiles.getEffectiveProfile(fabricService.get(), container.getOverlayProfile());
            Map<String, String> openshiftConfiguration = effectiveProfile.getConfiguration(OpenShiftConstants.OPENSHIFT_PID);
            if (openshiftConfiguration != null) {
                DeploymentUpdater deployTask = null;
                try {
                    deployTask = createDeployTask(container, openshiftConfiguration);
                } catch (MalformedURLException e) {
                    LOGGER.error("Failed to create DeploymentUpdater. " + e, e);
                }
                if (deployTask != null && OpenShiftUtils.isFabricManaged(openshiftConfiguration)) {
                    String containerId = container.getId();
                    IOpenShiftConnection connection = OpenShiftUtils.createConnection(container);
                    CreateOpenshiftContainerOptions options = OpenShiftUtils.getCreateOptions(container);
                    if (connection == null || options == null) {
                        LOGGER.warn(
                                "Ignoring container which has no openshift connection or options. connection: "
                                        + connection + " options: " + options);
                    } else {
                        try {
                            IApplication application = OpenShiftUtils.getApplication(container, connection);
                            if (application != null) {
                                final String gitUrl = application.getGitUrl();
                                if (gitUrl != null) {
                                    LOGGER.info("Git URL is " + gitUrl);

                                    final CartridgeGitRepository repo = new CartridgeGitRepository(containerId);

                                    final List<IOpenShiftSSHKey> sshkeys = application.getDomain().getUser()
                                            .getSSHKeys();

                                    final CredentialsProvider credentials = new CredentialsProvider() {
                                        @Override
                                        public boolean supports(CredentialItem... items) {
                                            return true;
                                        }

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

                                        @Override
                                        public boolean get(URIish uri, CredentialItem... items)
                                                throws UnsupportedCredentialItem {

                                            LOGGER.info("Credential request " + uri + " items " + Arrays.asList(items));
                                            int i = -1;
                                            for (CredentialItem item : items) {
                                                if (item instanceof CredentialItem.StringType) {
                                                    CredentialItem.StringType stringType
                                                            = (CredentialItem.StringType)item;

                                                    int idx = ++i < sshkeys.size() ? i : 0;
                                                    if (idx < sshkeys.size()) {
                                                        IOpenShiftSSHKey sshKey = sshkeys
                                                                .get(idx);
                                                        String passphrase = sshKey.getPublicKey();
                                                        LOGGER.info("For item " + item + " index " + i
                                                                + " using passphrase: " + passphrase);
                                                        stringType.setValue(passphrase);
                                                    } else {
                                                        LOGGER.warn("No ssh keys we can pass into git!");
                                                    }
                                                    continue;
                                                } else {
                                                    LOGGER.warn("Unknown CredentialItem " + item);
                                                }
                                            }
                                            return true;
                                        }
                                    };

                                    final DeploymentUpdater finalDeployTask = deployTask;
                                    SshSessionFactoryUtils.useOpenShiftSessionFactory(new Callable<Object>() {

                                        @Override
                                        public Object call()
                                                throws Exception {
                                            repo.cloneOrPull(gitUrl, credentials);
                                            finalDeployTask.updateDeployment(repo.getGit(), repo.getLocalRepo(),
                                                            credentials);
                                            return null;
                                        }
                                    });
                                }
                            }
                        } catch (Exception e) {
                            LOGGER.error("Failed to update container " + containerId + ". Reason: " + e, e);
                        } finally {
                            OpenShiftUtils.close(connection);
                        }
                    }
                }
            }
        }
    }

    private DeploymentUpdater createDeployTask(Container container, Map<String,String> openshiftConfiguration) throws MalformedURLException {
        String webappDir = relativePath(container, openshiftConfiguration, OpenShiftConstants.PROPERTY_DEPLOY_WEBAPPS);
        String deployDir = relativePath(container, openshiftConfiguration, OpenShiftConstants.PROPERTY_DEPLOY_JARS);
        if (webappDir != null || deployDir != null) {
            FabricService fabric = fabricService.get();
            DownloadManager downloadManager = DownloadManagers.createDownloadManager(fabric, downloadExecutor);
            DeploymentUpdater deploymentUpdater = new DeploymentUpdater(downloadManager, fabric, container, webappDir, deployDir);
            deploymentUpdater.setRepositories(Maps.stringValue(openshiftConfiguration, OpenShiftConstants.PROPERTY_REPOSITORIES, OpenShiftConstants.DEFAULT_REPOSITORIES));
            deploymentUpdater.setCopyFilesIntoGit(Maps.booleanValue(openshiftConfiguration, OpenShiftConstants.PROPERTY_COPY_BINARIES_TO_GIT, false));
            return deploymentUpdater;
        }
        return null;
    }


    private String relativePath(Container container, Map<String, String> configuration, String propertyName) {
        String value = Maps.stringValue(configuration, propertyName);
        if (Strings.isNotBlank(value)) {
            if (value.startsWith("..") || value.startsWith("/") || value.startsWith(File.separator)) {
                throw new IllegalStateException("Invalid relative path '" + value + "' for property " + propertyName + " for container " + container.getId());
            } else {
                return value;
            }
        }
        return null;
    }

    private ControllerNode createState() {
        ControllerNode state = new ControllerNode();
        return state;
    }

    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 bindFabricService(FabricService fabricService) {
        this.fabricService.bind(fabricService);
    }

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