/*
 * Copyright 2018 ThoughtWorks, Inc.
 *
 * Licensed 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 cd.go.contrib.elasticagent;

import cd.go.contrib.elasticagent.requests.CreateAgentRequest;
import cd.go.contrib.elasticagent.utils.Size;
import com.fasterxml.jackson.core.JsonFactory;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.dataformat.yaml.YAMLFactory;
import com.github.mustachejava.DefaultMustacheFactory;
import com.github.mustachejava.Mustache;
import com.github.mustachejava.MustacheFactory;
import io.fabric8.kubernetes.api.model.*;
import io.fabric8.kubernetes.client.KubernetesClient;
import org.apache.commons.io.FileUtils;
import org.apache.commons.lang3.StringUtils;
import org.joda.time.DateTime;
import org.joda.time.DateTimeZone;

import java.io.File;
import java.io.IOException;
import java.io.StringReader;
import java.io.StringWriter;
import java.net.URL;
import java.text.ParseException;
import java.util.*;

import static cd.go.contrib.elasticagent.Constants.*;
import static cd.go.contrib.elasticagent.KubernetesPlugin.LOG;
import static cd.go.contrib.elasticagent.executors.GetProfileMetadataExecutor.*;
import static cd.go.contrib.elasticagent.utils.Util.getSimpleDateFormat;
import static java.lang.String.valueOf;
import static java.nio.charset.StandardCharsets.UTF_8;
import static java.text.MessageFormat.format;
import static org.apache.commons.lang3.StringUtils.isBlank;

public class KubernetesInstanceFactory {
    public KubernetesInstance create(CreateAgentRequest request, PluginSettings settings, KubernetesClient client, PluginRequest pluginRequest) {
        String podSpecType = request.properties().get(POD_SPEC_TYPE.getKey());
        if (podSpecType != null) {
            switch (podSpecType) {
                case "properties":
                    return createUsingProperties(request, settings, client, pluginRequest);
                case "remote":
                    return createUsingRemoteFile(request, settings, client, pluginRequest);
                case "yaml":
                    return createUsingPodYaml(request, settings, client, pluginRequest);
                default:
                    throw new IllegalArgumentException(String.format("Unsupported value for `PodSpecType`: %s", podSpecType));
            }
        }
        else {
            if (Boolean.valueOf(request.properties().get(SPECIFIED_USING_POD_CONFIGURATION.getKey()))) {
                return createUsingPodYaml(request, settings, client, pluginRequest);
            } else {
                return createUsingProperties(request, settings, client, pluginRequest);
            }
        }
    }

    private KubernetesInstance createUsingProperties(CreateAgentRequest request, PluginSettings settings, KubernetesClient client, PluginRequest pluginRequest) {
        String containerName = format("{0}-{1}", KUBERNETES_POD_NAME_PREFIX, UUID.randomUUID().toString());

        Container container = new Container();
        container.setName(containerName);
        container.setImage(image(request.properties()));
        container.setImagePullPolicy("IfNotPresent");
        container.setSecurityContext(new SecurityContextBuilder().withPrivileged(privileged(request)).build());

        container.setResources(getPodResources(request));

        ObjectMeta podMetadata = new ObjectMeta();
        podMetadata.setName(containerName);

        PodSpec podSpec = new PodSpec();
        podSpec.setContainers(Arrays.asList(container));

        Pod elasticAgentPod = new Pod("v1", "Pod", podMetadata, podSpec, new PodStatus());

        setGoCDMetadata(request, settings, pluginRequest, elasticAgentPod);

        return createKubernetesPod(client, elasticAgentPod);
    }

    private Boolean privileged(CreateAgentRequest request) {
        final String privilegedMode = request.properties().get(PRIVILEGED.getKey());
        if (StringUtils.isBlank(privilegedMode)) {
            return false;
        }
        return Boolean.valueOf(privilegedMode);
    }

    private void setGoCDMetadata(CreateAgentRequest request, PluginSettings settings, PluginRequest pluginRequest, Pod elasticAgentPod) {
        elasticAgentPod.getMetadata().setCreationTimestamp(getSimpleDateFormat().format(new Date()));

        setContainerEnvVariables(elasticAgentPod, request, settings, pluginRequest);
        setAnnotations(elasticAgentPod, request);
        setLabels(elasticAgentPod, request);
    }

    private ResourceRequirements getPodResources(CreateAgentRequest request) {
        ResourceRequirements resources = new ResourceRequirements();
        HashMap<String, Quantity> limits = new HashMap<>();

        String maxMemory = request.properties().get("MaxMemory");
        if (StringUtils.isNotBlank(maxMemory)) {
            Size mem = Size.parse(maxMemory);
            LOG.debug(format("[Create Agent] Setting memory resource limit on k8s pod: {0}.", new Quantity(valueOf((long) mem.toMegabytes()), "M")));
            long memory = (long) mem.toBytes();
            limits.put("memory", new Quantity(valueOf(memory)));
        }

        String maxCPU = request.properties().get("MaxCPU");
        if (StringUtils.isNotBlank(maxCPU)) {
            LOG.debug(format("[Create Agent] Setting cpu resource limit on k8s pod: {0}.", new Quantity(maxCPU)));
            limits.put("cpu", new Quantity(maxCPU));
        }

        resources.setLimits(limits);

        return resources;
    }

    private static void setLabels(Pod pod, CreateAgentRequest request) {
        Map<String, String> existingLabels = (pod.getMetadata().getLabels() != null) ? pod.getMetadata().getLabels() : new HashMap<>();
        existingLabels.putAll(labelsFrom(request));
        pod.getMetadata().setLabels(existingLabels);
    }

    private static void setAnnotations(Pod pod, CreateAgentRequest request) {
        Map<String, String> existingAnnotations = (pod.getMetadata().getAnnotations() != null) ? pod.getMetadata().getAnnotations() : new HashMap<>();
        existingAnnotations.putAll(request.properties());
        existingAnnotations.put(JOB_IDENTIFIER_LABEL_KEY, request.jobIdentifier().toJson());
        pod.getMetadata().setAnnotations(existingAnnotations);
    }

    private KubernetesInstance createKubernetesPod(KubernetesClient client, Pod elasticAgentPod) {
        LOG.info(format("[Create Agent] Creating K8s pod with spec: {0}.", elasticAgentPod.toString()));
        Pod pod = client.pods().create(elasticAgentPod);
        return fromKubernetesPod(pod);
    }

    KubernetesInstance fromKubernetesPod(Pod elasticAgentPod) {
        KubernetesInstance kubernetesInstance;
        try {
            ObjectMeta metadata = elasticAgentPod.getMetadata();
            DateTime createdAt = DateTime.now().withZone(DateTimeZone.UTC);
            if (StringUtils.isNotBlank(metadata.getCreationTimestamp())) {
                createdAt = new DateTime(getSimpleDateFormat().parse(metadata.getCreationTimestamp())).withZone(DateTimeZone.UTC);
            }
            String environment = metadata.getLabels().get(ENVIRONMENT_LABEL_KEY);
            Long jobId = Long.valueOf(metadata.getLabels().get(JOB_ID_LABEL_KEY));
            kubernetesInstance = new KubernetesInstance(createdAt, environment, metadata.getName(), metadata.getAnnotations(), jobId, PodState.fromPod(elasticAgentPod));
        } catch (ParseException e) {
            throw new RuntimeException(e);
        }
        return kubernetesInstance;
    }

    private static List<EnvVar> environmentFrom(CreateAgentRequest request, PluginSettings settings, String podName, PluginRequest pluginRequest) {
        ArrayList<EnvVar> env = new ArrayList<>();
        String goServerUrl = StringUtils.isBlank(settings.getGoServerUrl()) ? pluginRequest.getSeverInfo().getSecureSiteUrl() : settings.getGoServerUrl();
        env.add(new EnvVar("GO_EA_SERVER_URL", goServerUrl, null));
        String environment = request.properties().get("Environment");
        if (StringUtils.isNotBlank(environment)) {
            env.addAll(parseEnvironments(environment));
        }
        env.addAll(request.autoregisterPropertiesAsEnvironmentVars(podName));

        return new ArrayList<>(env);
    }

    private static void setContainerEnvVariables(Pod pod, CreateAgentRequest request, PluginSettings settings, PluginRequest pluginRequest) {
        for (Container container : pod.getSpec().getContainers()) {
            List<EnvVar> existingEnv = (container.getEnv() != null) ? container.getEnv() : new ArrayList<>();
            existingEnv.addAll(environmentFrom(request, settings, pod.getMetadata().getName(), pluginRequest));
            container.setEnv(existingEnv);
        }
    }

    private static Collection<? extends EnvVar> parseEnvironments(String environment) {
        ArrayList<EnvVar> envVars = new ArrayList<>();
        for (String env : environment.split("\n")) {
            String[] parts = env.split("=");
            envVars.add(new EnvVar(parts[0], parts[1], null));
        }

        return envVars;
    }

    private static HashMap<String, String> labelsFrom(CreateAgentRequest request) {
        HashMap<String, String> labels = new HashMap<>();

        labels.put(CREATED_BY_LABEL_KEY, PLUGIN_ID);
        labels.put(JOB_ID_LABEL_KEY, valueOf(request.jobIdentifier().getJobId()));

        if (StringUtils.isNotBlank(request.environment())) {
            labels.put(ENVIRONMENT_LABEL_KEY, request.environment());
        }

        labels.put(KUBERNETES_POD_KIND_LABEL_KEY, KUBERNETES_POD_KIND_LABEL_VALUE);

        return labels;
    }

    private static String image(Map<String, String> properties) {
        String image = properties.get("Image");

        if (isBlank(image)) {
            throw new IllegalArgumentException("Must provide `Image` attribute.");
        }

        if (!image.contains(":")) {
            return image + ":latest";
        }
        return image;
    }

    private KubernetesInstance createUsingPodYaml(CreateAgentRequest request, PluginSettings settings, KubernetesClient client, PluginRequest pluginRequest) {
        ObjectMapper mapper = new ObjectMapper(new YAMLFactory());
        String podYaml = request.properties().get(POD_CONFIGURATION.getKey());
        String templatizedPodYaml = getTemplatizedPodSpec(podYaml);

        Pod elasticAgentPod = new Pod();
        try {
            elasticAgentPod = mapper.readValue(templatizedPodYaml, Pod.class);
            setPodNameIfNecessary(elasticAgentPod, podYaml);
        } catch (IOException e) {
            //ignore error here, handle this inside validate profile!
            LOG.error(e.getMessage());
        }

        setGoCDMetadata(request, settings, pluginRequest, elasticAgentPod);
        return createKubernetesPod(client, elasticAgentPod);
    }

    private KubernetesInstance createUsingRemoteFile(CreateAgentRequest request, PluginSettings settings, KubernetesClient client, PluginRequest pluginRequest) {
        String fileToDownload = request.properties().get(REMOTE_FILE.getKey());
        String fileType = request.properties().get(REMOTE_FILE_TYPE.getKey());

        Pod elasticAgentPod = new Pod();
        ObjectMapper mapper;
        if ("json".equalsIgnoreCase(fileType)) {
            mapper = new ObjectMapper(new JsonFactory());
        }
        else if ("yaml".equalsIgnoreCase(fileType)) {
            mapper = new ObjectMapper(new YAMLFactory());
        }
        else {
            throw new IllegalArgumentException("RemoteFileType value should be one of `json` or `yaml`.");
        }

        File podSpecFile = new File(String.format("pod_spec_%s", UUID.randomUUID().toString()));
        try {
            FileUtils.copyURLToFile(new URL(fileToDownload), podSpecFile);
            LOG.debug(format("Finished downloading %s to %s", fileToDownload, podSpecFile));
            String spec = FileUtils.readFileToString(podSpecFile, UTF_8);
            String templatizedPodSpec = getTemplatizedPodSpec(spec);
            elasticAgentPod = mapper.readValue(templatizedPodSpec, Pod.class);
            setPodNameIfNecessary(elasticAgentPod, spec);
            FileUtils.deleteQuietly(podSpecFile);
            LOG.debug(format("Deleted %s", podSpecFile));

        } catch (IOException e) {
            //ignore error here, handle this inside validate profile!
            LOG.error(e.getMessage());
        }
        setGoCDMetadata(request, settings, pluginRequest, elasticAgentPod);
        return createKubernetesPod(client, elasticAgentPod);
    }

    private void setPodNameIfNecessary(Pod elasticAgentPod, String spec) {
        if (!spec.contains(POD_POSTFIX)) {
            String newPodName = elasticAgentPod.getMetadata().getName().concat(String.format("-%s", UUID.randomUUID().toString()));
            elasticAgentPod.getMetadata().setName(newPodName);
        }
    }


    public static String getTemplatizedPodSpec(String podSpec) {
        StringWriter writer = new StringWriter();
        MustacheFactory mf = new DefaultMustacheFactory();
        Mustache mustache = mf.compile(new StringReader(podSpec), "templatePod");
        mustache.execute(writer, KubernetesInstanceFactory.getJinJavaContext());
        return writer.toString();
    }

    private static Map<String, String> getJinJavaContext() {
        HashMap<String, String> context = new HashMap<>();
        context.put(POD_POSTFIX, UUID.randomUUID().toString());
        context.put(CONTAINER_POSTFIX, UUID.randomUUID().toString());
        context.put(GOCD_AGENT_IMAGE, "gocd/gocd-agent-alpine-3.9");
        context.put(LATEST_VERSION, "v20.3.0");
        return context;
    }
}