/**
 * Copyright 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.maven.core.service;

import io.fabric8.kubernetes.api.model.DoneablePersistentVolumeClaim;
import io.fabric8.kubernetes.api.model.DoneablePod;
import io.fabric8.kubernetes.api.model.DoneableReplicationController;
import io.fabric8.kubernetes.api.model.DoneableSecret;
import io.fabric8.kubernetes.api.model.DoneableService;
import io.fabric8.kubernetes.api.model.HasMetadata;
import io.fabric8.kubernetes.api.model.PersistentVolumeClaim;
import io.fabric8.kubernetes.api.model.Pod;
import io.fabric8.kubernetes.api.model.ReplicationController;
import io.fabric8.kubernetes.api.model.Secret;
import io.fabric8.kubernetes.api.model.Service;
import io.fabric8.kubernetes.client.KubernetesClient;
import io.fabric8.maven.core.util.kubernetes.OpenshiftHelper;
import io.fabric8.maven.core.util.kubernetes.UserConfigurationCompare;
import io.fabric8.maven.docker.util.Logger;
import io.fabric8.openshift.api.model.BuildConfig;
import io.fabric8.openshift.api.model.DoneableBuildConfig;
import io.fabric8.openshift.api.model.DoneableImageStream;
import io.fabric8.openshift.api.model.ImageStream;
import io.fabric8.openshift.client.OpenShiftClient;

import java.util.HashMap;
import java.util.Map;

public class PatchService {
    private final KubernetesClient kubernetesClient;
    private final Logger log;

    private static Map<String, EntityPatcher<? extends HasMetadata>> patchers;

    // Interface for patching entities
    interface EntityPatcher<T extends HasMetadata> {

        /**
         * Patch a given entity with new value and edit it on the server
         *
         * @param client kubernetes client used for patching the entity
         * @param namespace namespace where the entity lives
         * @param newEntity the new, possibly changed entity
         * @param oldEntity the original entity
         * @return the patched entity, or the old entity if nothing has changed.
         */
        T patch(KubernetesClient client, String namespace, T newEntity, T oldEntity);
    }

    static {
        patchers = new HashMap<>();
        patchers.put("Pod", podPatcher());
        patchers.put("ReplicationController", rcPatcher());
        patchers.put("Service", servicePatcher());
        patchers.put("BuildConfig", bcPatcher());
        patchers.put("ImageStream", isPatcher());
        patchers.put("Secret", secretPatcher());
        patchers.put("PersistentVolumeClaim", pvcPatcher());
    }

    public PatchService(KubernetesClient client, Logger log) {
        this.kubernetesClient = client;
        this.log = log;
    }

    public <T extends HasMetadata> T compareAndPatchEntity(String namespace, T newDto, T oldDto) {
        EntityPatcher<T> dispatcher = (EntityPatcher<T>) patchers.get(newDto.getKind());
        if (dispatcher == null) {
            throw new IllegalArgumentException("Internal: No patcher for " + newDto.getKind() + " found");
        }
        return dispatcher.patch(kubernetesClient, namespace, newDto, oldDto);
    }

    private static EntityPatcher<Pod> podPatcher() {
        return (KubernetesClient client, String namespace, Pod newObj, Pod oldObj) -> {
            if (UserConfigurationCompare.configEqual(newObj, oldObj)) {
                return oldObj;
            }

            DoneablePod entity =
                client.pods()
                      .inNamespace(namespace)
                      .withName(oldObj.getMetadata().getName())
                      .edit();

            if (!UserConfigurationCompare.configEqual(newObj.getMetadata(), oldObj.getMetadata())) {
                entity.withMetadata(newObj.getMetadata());
            }

            if(!UserConfigurationCompare.configEqual(newObj.getSpec(), oldObj.getSpec())) {
                    entity.withSpec(newObj.getSpec());
            }
            return entity.done();
        };
    }

    private static EntityPatcher<ReplicationController> rcPatcher() {
        return (KubernetesClient client, String namespace, ReplicationController newObj, ReplicationController oldObj) -> {
            if (UserConfigurationCompare.configEqual(newObj, oldObj)) {
                return oldObj;
            }

            DoneableReplicationController entity =
                client.replicationControllers()
                      .inNamespace(namespace)
                      .withName(oldObj.getMetadata().getName())
                      .edit();

            if (!UserConfigurationCompare.configEqual(newObj.getMetadata(), oldObj.getMetadata())) {
                entity.withMetadata(newObj.getMetadata());
            }

            if(!UserConfigurationCompare.configEqual(newObj.getSpec(), oldObj.getSpec())) {
                    entity.withSpec(newObj.getSpec());
            }
            return entity.done();
        };
    }

    private static EntityPatcher<Service> servicePatcher() {
        return (KubernetesClient client, String namespace, Service newObj, Service oldObj) -> {
            if (UserConfigurationCompare.configEqual(newObj, oldObj)) {
                return oldObj;
            }

            DoneableService entity =
                client.services()
                      .inNamespace(namespace)
                      .withName(newObj.getMetadata().getName())
                      .edit();

            if (!UserConfigurationCompare.configEqual(newObj.getMetadata(), oldObj.getMetadata())) {
                entity.withMetadata(newObj.getMetadata());
            }

            if(!UserConfigurationCompare.configEqual(newObj.getSpec(), oldObj.getSpec())) {
                entity.withSpec(newObj.getSpec());
            }
            return entity.done();
        };
    }

    private static EntityPatcher<Secret> secretPatcher() {
        return (KubernetesClient client, String namespace, Secret newObj, Secret oldObj) -> {
            if (UserConfigurationCompare.configEqual(newObj, oldObj)) {
                return oldObj;
            }
            DoneableSecret entity =
                client.secrets()
                      .inNamespace(namespace)
                      .withName(oldObj.getMetadata().getName())
                      .edit();

            if (!UserConfigurationCompare.configEqual(newObj.getMetadata(), oldObj.getMetadata())) {
                entity.withMetadata(newObj.getMetadata());
            }

            if(!UserConfigurationCompare.configEqual(newObj.getData(), oldObj.getData())) {
                    entity.withData(newObj.getData());
            }
            if(!UserConfigurationCompare.configEqual(newObj.getStringData(), oldObj.getStringData())) {
                entity.withStringData(newObj.getStringData());
            }
            return entity.done();
        };
    }

    private static EntityPatcher<PersistentVolumeClaim> pvcPatcher() {
        return (KubernetesClient client, String namespace, PersistentVolumeClaim newObj, PersistentVolumeClaim oldObj) -> {
            if (UserConfigurationCompare.configEqual(newObj, oldObj)) {
                return oldObj;
            }
            DoneablePersistentVolumeClaim entity =
                client.persistentVolumeClaims()
                      .inNamespace(namespace)
                      .withName(oldObj.getMetadata().getName())
                      .edit();

            if (!UserConfigurationCompare.configEqual(newObj.getMetadata(), oldObj.getMetadata())) {
                entity.withMetadata(newObj.getMetadata());
            }

            if(!UserConfigurationCompare.configEqual(newObj.getSpec(), oldObj.getSpec())) {
                    entity.withSpec(newObj.getSpec());
            }
            return entity.done();
        };
    }

    // ================================================================================
    // OpenShift objects:
    // =================

    private static EntityPatcher<BuildConfig> bcPatcher() {
        return (KubernetesClient client, String namespace, BuildConfig newObj, BuildConfig oldObj) -> {
            if (UserConfigurationCompare.configEqual(newObj, oldObj)) {
                return oldObj;
            }
            OpenShiftClient openShiftClient = OpenshiftHelper.asOpenShiftClient(client);
            if (openShiftClient == null) {
                throw new IllegalArgumentException("BuildConfig can only be patched when connected to an OpenShift cluster");
            }
            DoneableBuildConfig entity =
                openShiftClient.buildConfigs()
                      .inNamespace(namespace)
                      .withName(oldObj.getMetadata().getName())
                      .edit();

            if (!UserConfigurationCompare.configEqual(newObj.getMetadata(), oldObj.getMetadata())) {
                entity.withMetadata(newObj.getMetadata());
            }

            if(!UserConfigurationCompare.configEqual(newObj.getSpec(), oldObj.getSpec())) {
                    entity.withSpec(newObj.getSpec());
            }
            return entity.done();
        };
    }

    private static EntityPatcher<ImageStream> isPatcher() {
        return (KubernetesClient client, String namespace, ImageStream newObj, ImageStream oldObj) -> {
            if (UserConfigurationCompare.configEqual(newObj, oldObj)) {
                return oldObj;
            }
            OpenShiftClient openShiftClient = OpenshiftHelper.asOpenShiftClient(client);
            if (openShiftClient == null) {
                throw new IllegalArgumentException("ImageStream can only be patched when connected to an OpenShift cluster");
            }
            DoneableImageStream entity =
                openShiftClient.imageStreams()
                      .inNamespace(namespace)
                      .withName(oldObj.getMetadata().getName())
                      .edit();

            if (!UserConfigurationCompare.configEqual(newObj.getMetadata(), oldObj.getMetadata())) {
                entity.withMetadata(newObj.getMetadata());
            }

            if(!UserConfigurationCompare.configEqual(newObj.getSpec(), oldObj.getSpec())) {
                    entity.withSpec(newObj.getSpec());
            }
            return entity.done();
        };
    }

}


/**
 * Copyright 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.maven.core.util.kubernetes;

import java.io.File;
import java.io.IOException;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import java.util.Objects;

import io.fabric8.kubernetes.api.model.Config;
import io.fabric8.kubernetes.api.model.Container;
import io.fabric8.kubernetes.api.model.Context;
import io.fabric8.kubernetes.api.model.HasMetadata;
import io.fabric8.kubernetes.api.model.IntOrString;
import io.fabric8.kubernetes.api.model.KubernetesList;
import io.fabric8.kubernetes.api.model.NamedContext;
import io.fabric8.kubernetes.api.model.ObjectMeta;
import io.fabric8.kubernetes.api.model.Pod;
import io.fabric8.kubernetes.api.model.PodCondition;
import io.fabric8.kubernetes.api.model.PodSpec;
import io.fabric8.kubernetes.api.model.PodStatus;
import io.fabric8.kubernetes.client.ConfigBuilder;
import io.fabric8.maven.core.util.ResourceFileType;
import io.fabric8.maven.core.util.ResourceUtil;
import io.fabric8.openshift.api.model.Template;
import org.apache.commons.lang3.StringUtils;

/**
 * @author roland
 * @since 23.05.17
 */
public class KubernetesHelper {

    /**
     * Validates that the given value is valid according to the kubernetes ID parsing rules, throwing an exception if not.
     */
    public static String validateKubernetesId(String currentValue, String description) throws IllegalArgumentException {
        if (StringUtils.isBlank(currentValue)) {
            throw new IllegalArgumentException("No " + description + " is specified!");
        }
        int size = currentValue.length();
        for (int i = 0; i < size; i++) {
            char ch = currentValue.charAt(i);
            if (Character.isUpperCase(ch)) {
                throw new IllegalArgumentException("Invalid upper case letter '" + ch + "' at index " + i + " for " + description + " value: " + currentValue);
            }
        }
        return currentValue;
    }

        /**
     * Loads the Kubernetes JSON and converts it to a list of entities
     */
    @SuppressWarnings("unchecked")
    public static List<HasMetadata> toItemList(Object entity) throws IOException {
        if (entity instanceof List) {
            return (List<HasMetadata>) entity;
        } else if (entity instanceof HasMetadata[]) {
            HasMetadata[] array = (HasMetadata[]) entity;
            return Arrays.asList(array);
        } else if (entity instanceof KubernetesList) {
            KubernetesList config = (KubernetesList) entity;
            return config.getItems();
        } else if (entity instanceof Template) {
            Template objects = (Template) entity;
            return objects.getObjects();
        } else {
            List<HasMetadata> answer = new ArrayList<>();
            if (entity instanceof HasMetadata) {
                answer.add((HasMetadata) entity);
            }
            return answer;
        }
    }

    public static Map<String, String> getOrCreateAnnotations(HasMetadata entity) {
        ObjectMeta metadata = getOrCreateMetadata(entity);
        Map<String, String> answer = metadata.getAnnotations();
        if (answer == null) {
            // use linked so the annotations can be in the FIFO order
            answer = new LinkedHashMap<>();
            metadata.setAnnotations(answer);
        }
        return answer;
    }

    public static ObjectMeta getOrCreateMetadata(HasMetadata entity) {
        ObjectMeta metadata = entity.getMetadata();
        if (metadata == null) {
            metadata = new ObjectMeta();
            entity.setMetadata(metadata);
        }
        return metadata;
    }

    public static Map<String, String> getOrCreateLabels(HasMetadata entity) {
        ObjectMeta metadata = getOrCreateMetadata(entity);
        Map<String, String> answer = metadata.getLabels();
        if (answer == null) {
            // use linked so the annotations can be in the FIFO order
            answer = new LinkedHashMap<>();
            metadata.setLabels(answer);
        }
        return answer;
    }

        /**
     * Returns the resource version for the entity or null if it does not have one
     */
    public static String getResourceVersion(HasMetadata entity) {
        if (entity != null) {
            ObjectMeta metadata = entity.getMetadata();
            if (metadata != null) {
                String resourceVersion = metadata.getResourceVersion();
                if (StringUtils.isNotBlank(resourceVersion)) {
                    return resourceVersion;
                }
            }
        }
        return null;
    }

    public static Map<String, String> getLabels(HasMetadata entity) {
        if (entity != null) {
            return getLabels(entity.getMetadata());
        }
        return Collections.EMPTY_MAP;
    }

    /**
     * Returns the labels of the given metadata object or an empty map if the metadata or labels are null
     */
    @SuppressWarnings("unchecked")
    public static Map<String, String> getLabels(ObjectMeta metadata) {
        if (metadata != null) {
            Map<String, String> labels = metadata.getLabels();
            if (labels != null) {
                return labels;
            }
        }
        return Collections.EMPTY_MAP;
    }

    public static String getName(HasMetadata entity) {
        if (entity != null) {
            return getName(entity.getMetadata());
        } else {
            return null;
        }
    }

    public static String getName(ObjectMeta entity) {
        if (entity != null) {
            for (String name : new String[]{
                entity.getName(),
                getAdditionalPropertyText(entity.getAdditionalProperties(), "id"),
                entity.getUid()
            }) {
                if (StringUtils.isNotBlank(name)) {
                    return name;
                }
            }
        }
        return null;
    }

    public static String getNamespace(ObjectMeta entity) {
        if (entity != null) {
            return entity.getNamespace();
        } else {
            return null;
        }
    }

    public static String getNamespace(HasMetadata entity) {
        if (entity != null) {
            return getNamespace(entity.getMetadata());
        } else {
            return null;
        }
    }

    /**
     * Returns the kind of the entity
     */
    public static String getKind(HasMetadata entity) {
        if (entity != null) {
            // TODO use reflection to find the kind?
            if (entity instanceof KubernetesList) {
                return "List";
            } else {
                return entity.getClass().getSimpleName();
            }
        } else {
            return null;
        }
    }

        /**
     * Creates an IntOrString from the given string which could be a number or a name
     */
    public static IntOrString createIntOrString(int intVal) {
        IntOrString answer = new IntOrString();
        answer.setIntVal(intVal);
        answer.setKind(0);
        return answer;
    }

    /**
     * Creates an IntOrString from the given string which could be a number or a name
     */
    public static IntOrString createIntOrString(String nameOrNumber) {
        if (StringUtils.isBlank(nameOrNumber)) {
            return null;
        } else {
            IntOrString answer = new IntOrString();
            Integer intVal = null;
            try {
                intVal = Integer.parseInt(nameOrNumber);
            } catch (Exception e) {
                // ignore invalid number
            }
            if (intVal != null) {
                answer.setIntVal(intVal);
                answer.setKind(0);
            } else {
                answer.setStrVal(nameOrNumber);
                answer.setKind(1);
            }
            return answer;
        }
    }

    /**
     * Returns true if the pod is running
     */
    public static boolean isPodRunning(Pod pod) {
        return isInPodPhase(pod, "run");
    }

    public static boolean isPodWaiting(Pod pod) {
        return isInPodPhase(pod, "wait");
    }

    /**
     * Returns true if the pod is running and ready
     */
    public static boolean isPodReady(Pod pod) {
        if (!isPodRunning(pod)) {
            return false;
        }

        PodStatus podStatus = pod.getStatus();
        if (podStatus == null) {
            return true;
        }

        List<PodCondition> conditions = podStatus.getConditions();
        if (conditions == null || conditions.isEmpty()) {
            return true;
        }

        // Check "ready" condition
        for (PodCondition condition : conditions) {
            if ("ready".equalsIgnoreCase(condition.getType())) {
                return Boolean.parseBoolean(condition.getStatus());
            }
        }

        return true;
    }

    private static boolean isInPodPhase(Pod pod, String phase) {
        return getPodPhase(pod).toLowerCase().startsWith(phase);
    }

    static String getPodPhase(Pod pod) {
        if (pod != null) {
            PodStatus podStatus = pod.getStatus();
            if (podStatus != null) {
                String actualPhase = podStatus.getPhase();
                return actualPhase != null ? actualPhase : "";
            }
        }
        return "";
    }

    public static List<Container> getContainers(Pod pod) {
        if (pod != null) {
            PodSpec podSpec = pod.getSpec();
            return getContainers(podSpec);

        }
        return Collections.EMPTY_LIST;
    }

    @SuppressWarnings("unchecked")
    public static List<Container> getContainers(PodSpec podSpec) {
        if (podSpec != null) {
            return podSpec.getContainers();
        }
        return Collections.EMPTY_LIST;
    }

    private static String getAdditionalPropertyText(Map<String, Object> additionalProperties, String name) {
        if (additionalProperties != null) {
            Object value = additionalProperties.get(name);
            if (value != null) {
                return value.toString();
            }
        }
        return null;
    }

    public static String getDefaultNamespace() {
        String ns = new ConfigBuilder().build().getNamespace();
        return ns != null ? ns : "default";
    }

    public static String currentUserName() {
        Config config = parseConfigs();
        if (config != null) {
            Context context = getCurrentContext(config);
            if (context != null) {
                String user = context.getUser();
                if (user != null) {
                    String[] parts = user.split("/");
                    if (parts.length > 0) {
                        return parts[0];
                    }
                    return user;
                }
            }
        }
        return null;
    }

    private static Config parseConfigs() {
        File file = getKubernetesConfigFile();
        if (file.exists() && file.isFile()) {
            try {
                return ResourceUtil.load(file, Config.class, ResourceFileType.yaml);
            } catch (IOException e) {
                e.printStackTrace();
            }
        }
        return null;
    }

    /**
     * Returns the current context in the given config
     */
    private static Context getCurrentContext(Config config) {
        String contextName = config.getCurrentContext();
        if (contextName != null) {
            List<NamedContext> contexts = config.getContexts();
            if (contexts != null) {
                for (NamedContext context : contexts) {
                    if (Objects.equals(contextName, context.getName())) {
                        return context.getContext();
                    }
                }
            }
        }
        return null;
    }

    private static File getKubernetesConfigFile() {
        String file = System.getProperty("kubernetes.config.file");
        if (file != null) {
            return new File(file);
        }
        file = System.getenv("KUBECONFIG");
        if (file != null) {
            return new File(file);
        }
        String homeDir = System.getProperty("user.home", ".");
        return new File(homeDir, ".kube/config");
    }
}


/**
 * Copyright 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.maven.enricher.standard.openshift;

import java.util.ArrayList;
import java.util.List;
import java.util.Map;
import java.util.concurrent.atomic.AtomicBoolean;

import io.fabric8.kubernetes.api.builder.TypedVisitor;
import io.fabric8.kubernetes.api.model.IntOrString;
import io.fabric8.kubernetes.api.model.KubernetesListBuilder;
import io.fabric8.kubernetes.api.model.ObjectMeta;
import io.fabric8.kubernetes.api.model.Service;
import io.fabric8.kubernetes.api.model.ServiceBuilder;
import io.fabric8.kubernetes.api.model.ServicePort;
import io.fabric8.kubernetes.api.model.ServiceSpec;
import io.fabric8.maven.core.config.PlatformMode;
import io.fabric8.maven.core.util.kubernetes.Fabric8Annotations;
import io.fabric8.maven.enricher.api.BaseEnricher;
import io.fabric8.maven.enricher.api.MavenEnricherContext;
import io.fabric8.openshift.api.model.Route;
import io.fabric8.openshift.api.model.RouteBuilder;
import io.fabric8.openshift.api.model.RoutePort;

/**
 * Enricher which generates a Route for each exposed Service
 */
public class RouteEnricher extends BaseEnricher {

    public RouteEnricher(MavenEnricherContext buildContext) {
        super(buildContext, "fmp-openshift-route");
    }

    @Override
    public void create(PlatformMode platformMode, final KubernetesListBuilder listBuilder) {
        if(platformMode == PlatformMode.openshift) {
            final List<Route> routes = new ArrayList<>();
            listBuilder.accept(new TypedVisitor<ServiceBuilder>() {

                @Override
                public void visit(ServiceBuilder serviceBuilder) {
                    addRoute(listBuilder, serviceBuilder, routes);
                }
            });

            if (!routes.isEmpty()) {
                Route[] routeArray = new Route[routes.size()];
                routes.toArray(routeArray);
                listBuilder.addToRouteItems(routeArray);
            }
        }
    }

    private void addRoute(KubernetesListBuilder listBuilder, ServiceBuilder serviceBuilder, List<Route> routes) {
        ObjectMeta metadata = serviceBuilder.getMetadata();
        if (metadata != null && isExposedService(serviceBuilder)) {
            String name = metadata.getName();
            if (!hasRoute(listBuilder, name)) {
                RoutePort routePort = createRoutePort(serviceBuilder);
                if (routePort != null) {
                    // TODO one day lets support multiple ports on a Route when the model supports it
                    routes.add(new RouteBuilder().
                            withMetadata(serviceBuilder.getMetadata()).
                            withNewSpec().
                            withPort(routePort).
                            withNewTo().withKind("Service").withName(name).endTo().
                            endSpec().
                            build());
                }
            }
        }
    }

    private RoutePort createRoutePort(ServiceBuilder serviceBuilder) {
        RoutePort routePort = null;
        ServiceSpec spec = serviceBuilder.getSpec();
        if (spec != null) {
            List<ServicePort> ports = spec.getPorts();
            if (ports != null && ports.size() > 0) {
                ServicePort servicePort = ports.get(0);
                if (servicePort != null) {
                    IntOrString targetPort = servicePort.getTargetPort();
                    if (targetPort != null) {
                        routePort = new RoutePort();
                        routePort.setTargetPort(targetPort);
                    }
                }
            }
        }
        return routePort;
    }

    /**
     * Returns true if we already have a route created for the given name
     */
    private boolean hasRoute(final KubernetesListBuilder listBuilder, final String name) {
        final AtomicBoolean answer = new AtomicBoolean(false);
        listBuilder.accept(new TypedVisitor<RouteBuilder>() {

            @Override
            public void visit(RouteBuilder builder) {
                ObjectMeta metadata = builder.getMetadata();
                if (metadata != null && name.equals(metadata.getName())) {
                    answer.set(true);
                }
            }
        });
        return answer.get();
    }

    protected boolean isExposedService(ServiceBuilder serviceBuilder) {
        Service service = serviceBuilder.build();
        return isExposedService(service);
    }

    protected boolean isExposedService(Service service) {
        ObjectMeta metadata = service.getMetadata();
        if (metadata != null) {
            Map<String, String> labels = metadata.getLabels();
            if (labels != null) {
                if ("true".equals(labels.get("expose")) || "true".equals(labels.get(Fabric8Annotations.SERVICE_EXPOSE_URL.value()))) {
                    return true;
                }
            }
        } else {
            log.info("No Metadata for service! " + service);
        }
        return false;
    }
}


/**
 * Copyright 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.maven.enricher.deprecated;

import io.fabric8.kubernetes.api.builder.TypedVisitor;
import io.fabric8.kubernetes.api.model.KubernetesListBuilder;
import io.fabric8.kubernetes.api.model.ReplicationControllerBuilder;
import io.fabric8.kubernetes.api.model.apps.DaemonSetBuilder;
import io.fabric8.kubernetes.api.model.apps.DeploymentBuilder;
import io.fabric8.kubernetes.api.model.apps.ReplicaSetBuilder;
import io.fabric8.kubernetes.api.model.apps.StatefulSetBuilder;
import io.fabric8.kubernetes.api.model.batch.JobBuilder;
import io.fabric8.maven.core.config.PlatformMode;
import io.fabric8.maven.core.util.kubernetes.Fabric8Annotations;
import io.fabric8.maven.enricher.api.MavenEnricherContext;
import java.net.MalformedURLException;
import java.net.URL;
import java.util.Collections;
import java.util.Map;
import java.util.Properties;
import java.util.Set;

import io.fabric8.openshift.api.model.DeploymentConfigBuilder;
import org.apache.commons.lang3.StringUtils;
import org.apache.maven.model.DistributionManagement;
import org.apache.maven.model.Site;
import org.apache.maven.project.MavenProject;

/**
 * Adds a link to the generated documentation for this microservice so we can link to the versioned docs in the
 * annotations
 */
public class DocLinkEnricher extends AbstractLiveEnricher {

    public DocLinkEnricher(MavenEnricherContext buildContext) {
        super(buildContext, "f8-deprecated-link");
    }

    @Override
    public void create(PlatformMode platformMode, KubernetesListBuilder builder) {
        builder.accept(new TypedVisitor<DeploymentBuilder>() {
            @Override
            public void visit(DeploymentBuilder builder) {
                builder.editMetadata().addToAnnotations(getAnnotations()).endMetadata();
            }
        });

        builder.accept(new TypedVisitor<DeploymentConfigBuilder>() {
            @Override
            public void visit(DeploymentConfigBuilder builder) {
                builder.editMetadata().addToAnnotations(getAnnotations()).endMetadata();
            }
        });

        builder.accept(new TypedVisitor<ReplicaSetBuilder>() {
            @Override
            public void visit(ReplicaSetBuilder builder) {
                builder.editMetadata().addToAnnotations(getAnnotations()).endMetadata();
            }
        });

        builder.accept(new TypedVisitor<ReplicationControllerBuilder>() {
            @Override
            public void visit(ReplicationControllerBuilder builder) {
                builder.editMetadata().addToAnnotations(getAnnotations()).endMetadata();
            }
        });

        builder.accept(new TypedVisitor<DaemonSetBuilder>() {
            @Override
            public void visit(DaemonSetBuilder builder) {
                builder.editMetadata().addToAnnotations(getAnnotations()).endMetadata();
            }
        });

        builder.accept(new TypedVisitor<StatefulSetBuilder>() {
            @Override
            public void visit(StatefulSetBuilder builder) {
                builder.editMetadata().addToAnnotations(getAnnotations()).endMetadata();
            }
        });

        builder.accept(new TypedVisitor<JobBuilder>() {
            @Override
            public void visit(JobBuilder builder) {
                builder.editMetadata().addToAnnotations(getAnnotations()).endMetadata();
            }
        });
    }

    public Map<String, String> getAnnotations() {
        String url = findDocumentationUrl();
        return url != null ? Collections.singletonMap(Fabric8Annotations.DOCS_URL.value(), url) : null;
    }

    private String getDocumentationUrl() {
        if (getContext() instanceof MavenEnricherContext) {
            MavenEnricherContext mavenEnricherContext = (MavenEnricherContext) getContext();
            MavenProject currentProject = mavenEnricherContext.getProject();
            while (currentProject != null) {
                DistributionManagement distributionManagement = currentProject.getDistributionManagement();
                if (distributionManagement != null) {
                    Site site = distributionManagement.getSite();
                    if (site != null) {
                        return site.getUrl();
                    }
                }
                currentProject = currentProject.getParent();
            }
        }
        return null;
    }

    protected String findDocumentationUrl() {

        String url = getDocumentationUrl();
        if (StringUtils.isNotBlank(url)) {
            // lets replace any properties...
            url = replaceProperties(url, getContext().getConfiguration().getProperties());

            // lets convert the internal dns name to a public name
            try {
                String urlToParse = url;
                int idx = url.indexOf("://");
                if (idx > 0) {
                    // lets strip any previous schemes such as "dav:"
                    int idx2 = url.substring(0, idx).lastIndexOf(':');
                    if (idx2 >= 0 && idx2 < idx) {
                        urlToParse = url.substring(idx2 + 1);
                    }
                }
                URL u = new URL(urlToParse);
                String serviceName = u.getHost();
                String protocol = u.getProtocol();
                if (isOnline()) {
                    // lets see if the host name is a service name in which case we'll resolve to the public URL
                    String publicUrl = getExternalServiceURL(serviceName, protocol);
                    if (StringUtils.isNotBlank(publicUrl)) {
                        return String.format("%s/%s", publicUrl, u.getPath());
                    }
                }
            } catch (MalformedURLException e) {
                getLog().error("Failed to parse URL: %s. %s", url, e);
            }
            return url;
        }
        return null;
    }

    /**
     * Replaces all text of the form <code>${foo}</code> with the value in the properties object
     */
    protected static String replaceProperties(String text, Properties properties) {
        Set<Map.Entry<Object, Object>> entries = properties.entrySet();
        for (Map.Entry<Object, Object> entry : entries) {
            Object key = entry.getKey();
            Object value = entry.getValue();
            if (key != null && value != null) {
                String pattern = "${" + key + "}";
                text = StringUtils.replace(text, pattern, value.toString());
            }
        }
        return text;
    }
}


/**
 * Copyright 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.maven.core.util;

import java.io.BufferedReader;
import java.io.Closeable;
import java.io.File;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.util.ArrayList;
import java.util.List;
import java.util.Map;

import com.google.common.base.Function;
import io.fabric8.maven.docker.util.Logger;
import org.apache.commons.lang3.StringUtils;

import static io.fabric8.maven.docker.util.EnvUtil.isWindows;

/**
 * A helper class for running external processes
 */
public class ProcessUtil {

    public static int runCommand(final Logger log, File command, List<String> args) throws IOException {
        return runCommand(log, command, args, false);
    }

    public static int runCommand(final Logger log, File command, List<String> args, boolean withShutdownHook) throws IOException {
        return runAsyncCommand(log, command, args, withShutdownHook, true).await();
    }

    public static ProcessExecutionContext runAsyncCommand(final Logger log, File command, List<String> args, boolean withShutdownHook, boolean useStandardLoggingLevel) throws IOException {
        String[] commandWithArgs = prepareCommandArray(command.getAbsolutePath(), args);
        Process process = Runtime.getRuntime().exec(commandWithArgs);
        if (withShutdownHook) {
            addShutdownHook(log, process, command);
        }
        List<Thread> threads = startLoggingThreads(process, log, command.getName() + " " + StringUtils.join(args, " "), useStandardLoggingLevel);
        return new ProcessExecutionContext(process, threads, log);
    }

    private static void joinThreads(List<Thread> threads, Logger log) {
        for (Thread thread : threads) {
            try {
                thread.join();
            } catch (InterruptedException e) {
                log.warn("Caught %s", e);
            }
        }
    }

    public static File findExecutable(Logger log, String name) {
        return findExecutable(log, name, getPathDirectories());
    }

    public static File findExecutable(Logger log, String name, List<File> directories) {
        for (File directory : directories) {
            for (String extension : isWindows() ? new String[]{ ".exe", ".bat", ".cmd", "" } : new String[] { "" }) {
                File file = new File(directory, name + extension);
                if (file.exists() && file.isFile()) {
                    if (!file.canExecute()) {
                        log.warn("Found %s on the PATH but it is not executable. Ignoring ...", file);
                    } else {
                        return file;
                    }
                }
            }
        }
        return null;
    }

    public static boolean folderIsOnPath(Logger logger, File dir) {
        List<File> paths = getPathDirectories();
        if (paths.size() == 0) {
            logger.warn("The $PATH environment variable is empty! Usually you have a PATH defined to find binaries.");
            logger.warn("Please report this to the fabric8 team: https://github.com/fabric8io/fabric8-maven-plugin/issues/new");
            return false;
        }

        for (File path : paths) {
            if (canonicalPath(path).equals(canonicalPath(dir))) {
                return true;
            }
        }
        return false;
    }

    // ==========================================================================================================

    private static void addShutdownHook(final Logger log, final Process process, final File command) {
        Runtime.getRuntime().addShutdownHook(new Thread(command.getName()) {
            @Override
            public void run() {
                if (process != null) {
                    // Trying to determine if the process is alive
                    boolean alive = false;
                    try {
                        process.exitValue();
                    } catch (IllegalThreadStateException e) {
                        alive = true;
                    }

                    if (alive) {
                        log.info("Terminating process %s", command);
                        try {
                            process.destroy();
                        } catch (Exception e) {
                            log.error("Failed to terminate process %s", command);
                        }
                        /* Only available in Java 8: So disabled for now until we switch to Java 8
                        try {
                            if (process != null && process.isAlive()) {
                                process.destroyForcibly();
                            }
                        } catch (Exception e) {
                            log.error("Failed to forcibly terminate process %s", command);
                        }
                        */
                    }
                }
            }
        });
    }

    private static String[] prepareCommandArray(String command, List<String> args) {
        List<String> nArgs = args != null ? args : new ArrayList<String>();
        String[] commandWithArgs = new String[nArgs.size() + 1];
        commandWithArgs[0] = command;
        for (int i = 0; i < nArgs.size(); i++) {
            commandWithArgs[i+1] = nArgs.get(i);
        }
        return commandWithArgs;
    }

    private static void processOutput(InputStream inputStream, Function<String, Void> function) throws IOException {
        try (BufferedReader reader = new BufferedReader(new InputStreamReader(inputStream))) {
            String line = null;
            while ((line = reader.readLine()) != null) {
                function.apply(line);
            }
        }
    }

    private static String canonicalPath(File file) {
        try {
            return file.getCanonicalPath();
        } catch (IOException e) {
            String absolutePath = file.getAbsolutePath();
            return absolutePath;
        }
    }

    private static List<File> getPathDirectories() {
        List<File> pathDirectories = new ArrayList<>();
        String pathText = System.getenv("PATH");
        if( isWindows() && pathText==null ) {
            // On windows, the PATH env var is case insensitive, force to upper case.
            for (Map.Entry<String, String> entry : System.getenv().entrySet()) {
                if( entry.getKey().equalsIgnoreCase("PATH") ) {
                    pathText = entry.getValue();
                    break;
                }
            }
        }
        if (StringUtils.isNotBlank(pathText)) {
            String[] pathTexts = pathText.split(File.pathSeparator);
            for (String text : pathTexts) {
                File dir = new File(text);
                if (dir.exists() && dir.isDirectory()) {
                    pathDirectories.add(dir);
                }
            }
        }
        return pathDirectories;
    }

    private static List<Thread> startLoggingThreads(final Process process, final Logger log, final String commandDesc, boolean useStandardLoggingLevel) {
        List<Thread> threads = new ArrayList<>();
        threads.add(startOutputLoggingThread(process, log, commandDesc, useStandardLoggingLevel));
        threads.add(startErrorLoggingThread(process, log, commandDesc, useStandardLoggingLevel));
        return threads;
    }

    private static Thread startErrorLoggingThread(final Process process, final Logger log, final String commandDesc, final boolean useStandardLoggingLevel) {
        Thread logThread = new Thread("[ERR] " + commandDesc) {
            @Override
            public void run() {
                try {
                    processOutput(process.getErrorStream(), createErrorHandler(log, useStandardLoggingLevel));
                } catch (IOException e) {
                    log.error("Failed to read error stream from %s : %s", commandDesc, e.getMessage());
                }
            }
        };
        logThread.setDaemon(true);
        logThread.start();
        return logThread;
    }

    private static Thread startOutputLoggingThread(final Process process, final Logger log, final String commandDesc, final boolean useStandardLoggingLevel) {
        Thread logThread = new Thread("[OUT] " + commandDesc) {
            @Override
            public void run() {
                try {
                    processOutput(process.getInputStream(), createOutputHandler(log, useStandardLoggingLevel));
                } catch (IOException e) {
                    log.error("Failed to read output stream from %s : %s", commandDesc, e.getMessage());
                }
            }
        };
        logThread.setDaemon(true);
        logThread.start();
        return logThread;
    }

    private static Function<String, Void> createOutputHandler(final Logger log, final boolean useStandardLoggingLevel) {
        return outputLine -> {
            if (useStandardLoggingLevel) {
                log.info("%s", outputLine);
            } else {
                log.debug("%s", outputLine);
            }
            return null;
        };
    }

    private static Function<String, Void> createErrorHandler(final Logger log,  final boolean useStandardLoggingLevel) {
        return outputLine -> {
            if (useStandardLoggingLevel) {
                log.error("%s", outputLine);
            } else {
                log.warn("%s", outputLine);
            }
            return null;
        };
    }

    // =====================================================================================

    /**
     * Closeable class for holding a reference to a subprocess.
     */
    public static class ProcessExecutionContext implements Closeable {

        private Process process;

        private List<Thread> loggingThreads;

        private Logger log;

        public ProcessExecutionContext(Process process, List<Thread> loggingThreads, Logger log) {
            this.process = process;
            this.loggingThreads = loggingThreads;
            this.log = log;
        }

        public int await() {
            try {
                int answer = process.waitFor();
                joinThreads(loggingThreads, log);
                return answer;
            } catch (InterruptedException e) {
                return process.exitValue();
            }
        }

        @Override
        public void close() throws IOException {
            process.destroy();
            await();
        }
    }
}


/**
 * Copyright 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.maven.core.service;

import java.io.Closeable;
import java.io.File;
import java.io.IOException;
import java.util.ArrayList;
import java.util.Collections;
import java.util.LinkedList;
import java.util.List;
import java.util.Objects;
import java.util.concurrent.locks.Condition;
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;

import io.fabric8.kubernetes.api.model.LabelSelector;
import io.fabric8.kubernetes.api.model.Pod;
import io.fabric8.kubernetes.api.model.PodList;
import io.fabric8.kubernetes.client.KubernetesClient;
import io.fabric8.kubernetes.client.KubernetesClientException;
import io.fabric8.kubernetes.client.Watch;
import io.fabric8.kubernetes.client.Watcher;
import io.fabric8.kubernetes.client.dsl.FilterWatchListDeletable;
import io.fabric8.maven.core.util.ProcessUtil;
import io.fabric8.maven.core.util.kubernetes.KubernetesClientUtil;
import io.fabric8.maven.core.util.kubernetes.KubernetesHelper;
import io.fabric8.maven.core.util.kubernetes.KubernetesResourceUtil;
import io.fabric8.maven.core.util.kubernetes.OpenshiftHelper;
import io.fabric8.maven.docker.util.Logger;
import org.apache.commons.lang3.StringUtils;

/**
 * A service for forwarding connections to remote pods.
 *
 * @author nicola
 * @since 28/03/2017
 */
public class PortForwardService {

    private ClientToolsService clientToolsService;

    private Logger log;

    private KubernetesClient kubernetes;

    public PortForwardService(KubernetesClient kubernetes, Logger log) {
        this.clientToolsService = new ClientToolsService(log);
        this.log = Objects.requireNonNull(log, "log");
        this.kubernetes = Objects.requireNonNull(kubernetes, "kubernetes");
    }

    /**
     * Forwards a port to the newest pod matching the given selector.
     * If another pod is created, it forwards connections to the new pod once it's ready.
     */
    public Closeable forwardPortAsync(final Logger externalProcessLogger, final LabelSelector podSelector, final int remotePort, final int localPort) throws Fabric8ServiceException {

        final Lock monitor = new ReentrantLock(true);
        final Condition podChanged = monitor.newCondition();
        final Pod[] nextForwardedPod = new Pod[1];

        final Thread forwarderThread = new Thread() {
            @Override
            public void run() {

                Pod currentPod = null;
                Closeable currentPortForward = null;

                try {
                    monitor.lock();

                    while (true) {
                        if (podEquals(currentPod, nextForwardedPod[0])) {
                            podChanged.await();
                        } else {
                            Pod nextPod = nextForwardedPod[0]; // may be null
                            try {
                                monitor.unlock();
                                // out of critical section

                                if (currentPortForward != null) {
                                    log.info("Closing port-forward from pod %s", KubernetesHelper.getName(currentPod));
                                    currentPortForward.close();
                                    currentPortForward = null;
                                }

                                if (nextPod != null) {
                                    log.info("Starting port-forward to pod %s", KubernetesHelper.getName(nextPod));
                                    currentPortForward = forwardPortAsync(externalProcessLogger, KubernetesHelper.getName(nextPod), remotePort, localPort);
                                } else {
                                    log.info("Waiting for a pod to become ready before starting port-forward");
                                }
                                currentPod = nextPod;
                            } finally {
                                monitor.lock();
                            }
                        }

                    }

                } catch (InterruptedException e) {
                    log.debug("Port-forwarding thread interrupted", e);
                    Thread.currentThread().interrupt();
                } catch (Exception e) {
                    log.warn("Error while port-forwarding to pod", e);
                } finally {
                    monitor.unlock();

                    if (currentPortForward != null) {
                        try {
                            currentPortForward.close();
                        } catch (Exception e) {}
                    }
                }
            }
        };

        // Switching forward to the current pod if present
        Pod newPod = getNewestPod(podSelector);
        nextForwardedPod[0] = newPod;

        final Watch watch = KubernetesClientUtil.withSelector(kubernetes.pods(), podSelector, log).watch(new Watcher<Pod>() {

            @Override
            public void eventReceived(Action action, Pod pod) {
                monitor.lock();
                try {
                    List<Pod> candidatePods;
                    if (nextForwardedPod[0] != null) {
                        candidatePods = new LinkedList<>();
                        candidatePods.add(nextForwardedPod[0]);
                        candidatePods.add(pod);
                    } else {
                        candidatePods = Collections.singletonList(pod);
                    }
                    Pod newPod = getNewestPod(candidatePods); // may be null
                    if (!podEquals(nextForwardedPod[0], newPod)) {
                        nextForwardedPod[0] = newPod;
                        podChanged.signal();
                    }
                } finally {
                    monitor.unlock();
                }
            }

            @Override
            public void onClose(KubernetesClientException e) {
                // don't care
            }
        });

        forwarderThread.start();

        final Closeable handle = () -> {
            try {
                watch.close();
            } catch (Exception e) {}
            try {
                forwarderThread.interrupt();
                forwarderThread.join(15000);
            } catch (Exception e) {}
        };
        Runtime.getRuntime().addShutdownHook(new Thread() {
            @Override
            public void run() {
                try {
                    handle.close();
                } catch (Exception e) {
                    // suppress
                }
            }
        });

        return handle;
    }

    private boolean podEquals(Pod pod1, Pod pod2) {
        if (pod1 == pod2) {
            return true;
        }
        if (pod1 == null || pod2 == null) {
            return false;
        }
        return KubernetesHelper.getName(pod1).equals(KubernetesHelper.getName(pod2));
    }

    private Pod getNewestPod(LabelSelector selector) {
        FilterWatchListDeletable<Pod, PodList, Boolean, Watch, Watcher<Pod>> pods =
                KubernetesClientUtil.withSelector(kubernetes.pods(), selector, log);

        PodList list = pods.list();
        if (list != null) {
            List<Pod> items = list.getItems();
            return getNewestPod(items);
        }
        return null;
    }

    private Pod getNewestPod(List<Pod> items) {
        Pod targetPod = null;
        if (items != null) {
            for (Pod pod : items) {
                if (KubernetesHelper.isPodWaiting(pod) || KubernetesHelper.isPodRunning(pod)) {
                    if (targetPod == null || (KubernetesHelper.isPodReady(pod) && KubernetesResourceUtil.isNewerResource(pod, targetPod))) {
                        targetPod = pod;
                    }
                }
            }
        }
        return targetPod;
    }

    public void forwardPort(Logger externalProcessLogger, String pod, int remotePort, int localPort) throws Fabric8ServiceException {
        forwardPortAsync(externalProcessLogger, pod, remotePort, localPort).await();
    }

    public ProcessUtil.ProcessExecutionContext forwardPortAsync(Logger externalProcessLogger, String pod, int remotePort, int localPort) throws Fabric8ServiceException {
        File command = clientToolsService.getKubeCtlExecutable(OpenshiftHelper.isOpenShiftClient(kubernetes));
        log.info("Port forwarding to port " + remotePort + " on pod " + pod + " using command " + command);

        List<String> args = new ArrayList<>();
        args.add("port-forward");
        args.add(pod);
        args.add(localPort + ":" + remotePort);

        String commandLine = command + " " + StringUtils.join(args, " ");
        log.verbose("Executing command " + commandLine);
        try {
            return ProcessUtil.runAsyncCommand(externalProcessLogger, command, args, true, false);
        } catch (IOException e) {
            throw new Fabric8ServiceException("Error while executing the port-forward command", e);
        }
    }
}


/**
 * Copyright 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.maven.core.util;

import java.io.File;
import java.io.FileInputStream;
import java.io.IOException;
import java.io.InputStream;
import java.net.MalformedURLException;
import java.net.URL;
import java.net.URLClassLoader;
import java.util.ArrayList;
import java.util.Collection;
import java.util.List;
import java.util.Optional;
import java.util.Properties;
import java.util.Set;
import java.util.jar.JarEntry;
import java.util.jar.JarInputStream;

import com.google.common.base.Objects;
import org.apache.commons.lang3.StringUtils;
import org.apache.maven.artifact.Artifact;
import org.apache.maven.artifact.DependencyResolutionRequiredException;
import org.apache.maven.model.Plugin;
import org.apache.maven.plugin.MojoExecutionException;
import org.apache.maven.project.MavenProject;
import org.codehaus.plexus.archiver.tar.TarArchiver;
import org.codehaus.plexus.archiver.tar.TarLongFileMode;
import org.codehaus.plexus.archiver.zip.ZipArchiver;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

/**
 * @author roland
 * @since 31/03/16
 */
public class MavenUtil {
    private static final transient Logger LOG = LoggerFactory.getLogger(MavenUtil.class);

    private static final String DEFAULT_CONFIG_FILE_NAME = "kubernetes.json";

    public static boolean isKubernetesJsonArtifact(String classifier, String type) {
        return "json".equals(type) && "kubernetes".equals(classifier);
    }

    public static boolean hasKubernetesJson(File f) throws IOException {
        try (FileInputStream fis = new FileInputStream(f); JarInputStream jis = new JarInputStream(fis)) {
            for (JarEntry entry = jis.getNextJarEntry(); entry != null; entry = jis.getNextJarEntry()) {
                if (entry.getName().equals(DEFAULT_CONFIG_FILE_NAME)) {
                    return true;
                }
            }
        }
        return false;
    }

    public static URLClassLoader getCompileClassLoader(MavenProject project) {
        try {
            List<String> classpathElements = project.getCompileClasspathElements();
            return createClassLoader(classpathElements, project.getBuild().getOutputDirectory());
        } catch (DependencyResolutionRequiredException e) {
            throw new IllegalArgumentException("Cannot resolve artifact from compile classpath",e);
        }
    }

    public static URLClassLoader getTestClassLoader(MavenProject project) {
        try {
            List<String> classpathElements = project.getTestClasspathElements();
            return createClassLoader(classpathElements, project.getBuild().getTestOutputDirectory());
        } catch (DependencyResolutionRequiredException e) {
            throw new IllegalArgumentException("Cannot resolve artifact from test classpath", e);
        }
    }

    public static String createDefaultResourceName(String artifactId, String ... suffixes) {
        String suffix = StringUtils.join(suffixes, "-");
        String ret = artifactId + (suffix.length() > 0 ? "-" + suffix : "");
        if (ret.length() > 63) {
            ret = ret.substring(0,63);
        }
        return ret.toLowerCase();
    }

    // ====================================================

    private static URLClassLoader createClassLoader(List<String> classpathElements, String... paths) {
        List<URL> urls = new ArrayList<>();
        for (String path : paths) {
            URL url = pathToUrl(path);
            urls.add(url);
        }
        for (Object object : classpathElements) {
            if (object != null) {
                String path = object.toString();
                URL url = pathToUrl(path);
                urls.add(url);
            }
        }
        return createURLClassLoader(urls);
    }

    private static URLClassLoader createURLClassLoader(Collection<URL> jars) {
        return new URLClassLoader(jars.toArray(new URL[jars.size()]));
    }

    private static URL pathToUrl(String path) {
        try {
            File file = new File(path);
            return file.toURI().toURL();
        } catch (MalformedURLException e) {
            throw new IllegalArgumentException(String.format("Cannot convert %s to a an URL: %s",path,e.getMessage()),e);
        }
    }

    /**
     * Returns true if the maven project has a dependency with the given groupId and artifactId (if not null)
     */
    public static boolean hasDependency(MavenProject project, String groupId, String artifactId) {
        return getDependencyVersion(project, groupId, artifactId) != null;
    }

    /**
     * Returns the version associated to the dependency dependency with the given groupId and artifactId (if present)
     */
    public static String getDependencyVersion(MavenProject project, String groupId, String artifactId) {
        Set<Artifact> artifacts = project.getArtifacts();
        if (artifacts != null) {
            for (Artifact artifact : artifacts) {
                String scope = artifact.getScope();
                if (Objects.equal("test", scope)) {
                    continue;
                }
                if (artifactId != null && !Objects.equal(artifactId, artifact.getArtifactId())) {
                    continue;
                }
                if (Objects.equal(groupId, artifact.getGroupId())) {
                    return artifact.getVersion();
                }
            }
        }
        return null;
    }

    public static boolean hasPlugin(MavenProject project, String groupId, String artifactId) {
        return project.getPlugin(groupId + ":" + artifactId) != null;
    }

    public static boolean hasPluginOfAnyGroupId(MavenProject project, String pluginArtifact) {
        return getPluginOfAnyGroupId(project, pluginArtifact) != null;
    }

    public static Plugin getPluginOfAnyGroupId(MavenProject project, String pluginArtifact) {
        return getPlugin(project, null, pluginArtifact);
    }

    /**
     * Returns the plugin with the given groupId (if present) and artifactId.
     */
    public static Plugin getPlugin(MavenProject project, String groupId, String artifactId) {
        if (artifactId == null) {
            throw new IllegalArgumentException("artifactId cannot be null");
        }

        List<Plugin> plugins = project.getBuildPlugins();
        if (plugins != null) {
            for (Plugin plugin : plugins) {
                boolean matchesArtifactId = artifactId.equals(plugin.getArtifactId());
                boolean matchesGroupId = groupId == null || groupId.equals(plugin.getGroupId());

                if (matchesGroupId && matchesArtifactId) {
                    return plugin;
                }
            }
        }
        return null;
    }

    /**
     * Returns true if any of the given resources could be found on the given class loader
     */
    public static boolean hasResource(MavenProject project, String... paths) {
        URLClassLoader compileClassLoader = getCompileClassLoader(project);
        for (String path : paths) {
            try {
                if (compileClassLoader.getResource(path) != null) {
                    return true;
                }
            } catch (Throwable e) {
                // ignore
            }
        }
        return false;
    }

    public static void createArchive(File sourceDir, File destinationFile, TarArchiver archiver) throws MojoExecutionException {
        try {
            archiver.setCompression(TarArchiver.TarCompressionMethod.gzip);
            archiver.setLongfile(TarLongFileMode.posix);
            archiver.addDirectory(sourceDir);
            archiver.setDestFile(destinationFile);
            archiver.createArchive();
        } catch (IOException e) {
            throw new MojoExecutionException("Failed to create archive " + destinationFile + ": " + e, e);
        }
    }

    public static void createArchive(File sourceDir, File destinationFile, ZipArchiver archiver) throws MojoExecutionException {
        try {
            archiver.addDirectory(sourceDir);
            archiver.setDestFile(destinationFile);
            archiver.createArchive();
        } catch (IOException e) {
            throw new MojoExecutionException("Failed to create archive " + destinationFile + ": " + e, e);
        }
    }

    /**
     * Returns the version from the list of pre-configured versions of common groupId/artifact pairs
     */
    public static String getVersion(String groupId, String artifactId) throws IOException {
        String path = "META-INF/maven/" + groupId + "/" + artifactId + "/pom.properties";
        InputStream in = MavenUtil.class.getClassLoader().getResourceAsStream(path);
        if (in == null) {
            throw new IOException("Could not find " + path + " on classath!");
        }
        Properties properties = new Properties();
        try {
            properties.load(in);
        } catch (IOException e) {
            throw new IOException("Failed to load " + path + ". " + e, e);
        }
        String version = properties.getProperty("version");
        if (StringUtils.isBlank(version)) {
            throw new IOException("No version property in " + path);

        }
        return version;
    }

    public static Optional<List<String>> getCompileClasspathElementsIfRequested(MavenProject project, boolean useProjectClasspath) throws MojoExecutionException {
        if (!useProjectClasspath) {
            return Optional.empty();
        }

        try {
            return Optional.of(project.getCompileClasspathElements());
        } catch (DependencyResolutionRequiredException e) {
            throw new MojoExecutionException("Cannot extra compile class path elements", e);
        }
    }
}


/**
 * Copyright 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.maven.core.access;

import io.fabric8.kubernetes.client.Config;
import io.fabric8.kubernetes.client.DefaultKubernetesClient;
import io.fabric8.kubernetes.client.KubernetesClient;
import io.fabric8.kubernetes.client.KubernetesClientException;
import io.fabric8.maven.core.config.PlatformMode;
import io.fabric8.maven.core.config.RuntimeMode;
import io.fabric8.maven.core.util.kubernetes.OpenshiftHelper;
import io.fabric8.maven.docker.util.Logger;
import io.fabric8.openshift.client.DefaultOpenShiftClient;
import io.fabric8.openshift.client.OpenShiftAPIGroups;
import io.fabric8.openshift.client.OpenShiftClient;
import java.net.UnknownHostException;

/**
 * @author roland
 * @since 17/07/16
 */
public class ClusterAccess {

    private ClusterConfiguration clusterConfiguration;

    private KubernetesClient client;

    public ClusterAccess(ClusterConfiguration clusterConfiguration) {
        this.clusterConfiguration = clusterConfiguration;

        if (this.clusterConfiguration == null) {
            this.clusterConfiguration = new ClusterConfiguration.Builder().build();
        }

        this.client = null;
    }

    @Deprecated
    public ClusterAccess(String namespace) {
        ClusterConfiguration.Builder clusterConfigurationBuilder = new ClusterConfiguration.Builder();
        clusterConfigurationBuilder.namespace(namespace);
        this.clusterConfiguration = clusterConfigurationBuilder.build();
        this.client = null;
    }

    public ClusterAccess(ClusterConfiguration clusterConfiguration, KubernetesClient client) {
        this.clusterConfiguration = clusterConfiguration;
        this.client = client;
    }

    public <T extends KubernetesClient> T createDefaultClient(Logger log) {
        if (isOpenShift(log)) {
            return (T) createOpenShiftClient();
        }

        return (T) createKubernetesClient();
    }

    public KubernetesClient createKubernetesClient() {
        return new DefaultKubernetesClient(createDefaultConfig());
    }

    public OpenShiftClient createOpenShiftClient() {
        return new DefaultOpenShiftClient(createDefaultConfig());
    }

    // ============================================================================

    private Config createDefaultConfig() {
        return this.clusterConfiguration.getConfig();
    }

    public String getNamespace() {
        return this.clusterConfiguration.getNamespace();
    }

    /**
     * Returns true if this cluster is a traditional OpenShift cluster with the /oapi REST API
     * or supports the new /apis/image.openshift.io API Group
     */
    public boolean isOpenShiftImageStream(Logger log) {
        if (isOpenShift(log)) {
            OpenShiftClient openShiftClient = null;
            if (this.client == null) {
                openShiftClient = createOpenShiftClient();
            } else if (this.client instanceof OpenShiftClient) {
                openShiftClient = (OpenShiftClient) this.client;
            } else if (this.client.isAdaptable(OpenShiftClient.class)) {
                openShiftClient = client.adapt(OpenShiftClient.class);
            } else {
                return false;
            }
            return openShiftClient.supportsOpenShiftAPIGroup(OpenShiftAPIGroups.IMAGE);
        }
        return false;
    }

    public boolean isOpenShift(Logger log) {
        try {
            return this.client == null ?
                OpenshiftHelper.isOpenShift(createKubernetesClient()) :
                OpenshiftHelper.isOpenShift(this.client);
        } catch (KubernetesClientException exp) {
            Throwable cause = exp.getCause();
            String prefix = cause instanceof UnknownHostException ? "Unknown host " : "";
            log.warn("Cannot access cluster for detecting mode: %s%s",
                     prefix,
                     cause != null ? cause.getMessage() : exp.getMessage());
        }
        return false;
    }

    public RuntimeMode resolveRuntimeMode(RuntimeMode mode, Logger log) {
        RuntimeMode resolvedMode;
        if (mode == null) {
            mode = RuntimeMode.DEFAULT;
        }
        if (mode.isAuto()) {
            resolvedMode = isOpenShiftImageStream(log) ? RuntimeMode.openshift : RuntimeMode.kubernetes;
        } else {
            resolvedMode = mode;
        }
        return resolvedMode;
    }
}