/*
 * Copyright 2015-2020 the original author or authors.
 *
 * 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
 *
 *      https://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 org.springframework.cloud.deployer.spi.kubernetes;

import java.util.HashMap;
import java.util.Map;
import java.util.Optional;
import java.util.stream.Collectors;

import io.fabric8.kubernetes.api.model.Affinity;
import io.fabric8.kubernetes.api.model.Container;
import io.fabric8.kubernetes.api.model.ContainerStatus;
import io.fabric8.kubernetes.api.model.Pod;
import io.fabric8.kubernetes.api.model.PodList;
import io.fabric8.kubernetes.api.model.PodSecurityContext;
import io.fabric8.kubernetes.api.model.PodSpec;
import io.fabric8.kubernetes.api.model.PodSpecBuilder;
import io.fabric8.kubernetes.api.model.ResourceRequirements;
import io.fabric8.kubernetes.api.model.Secret;
import io.fabric8.kubernetes.api.model.Service;
import io.fabric8.kubernetes.api.model.ServiceList;
import io.fabric8.kubernetes.client.KubernetesClient;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;

import org.springframework.cloud.deployer.spi.app.AppDeployer;
import org.springframework.cloud.deployer.spi.app.AppStatus;
import org.springframework.cloud.deployer.spi.core.AppDeploymentRequest;
import org.springframework.cloud.deployer.spi.core.RuntimeEnvironmentInfo;
import org.springframework.cloud.deployer.spi.kubernetes.support.PropertyParserUtils;
import org.springframework.cloud.deployer.spi.scheduler.ScheduleRequest;
import org.springframework.cloud.deployer.spi.util.RuntimeVersionUtils;
import org.springframework.core.io.Resource;
import org.springframework.util.StringUtils;

/**
 * Abstract base class for a deployer that targets Kubernetes.
 *
 * @author Florian Rosenberg
 * @author Thomas Risberg
 * @author Mark Fisher
 * @author Donovan Muller
 * @author David Turanski
 * @author Chris Schaefer
 * @author Enrique Medina Montenegro
 * @author Ilayaperumal Gopinathan
 */
public class AbstractKubernetesDeployer {

	protected static final String SPRING_DEPLOYMENT_KEY = "spring-deployment-id";
	protected static final String SPRING_GROUP_KEY = "spring-group-id";
	protected static final String SPRING_APP_KEY = "spring-app-id";
	protected static final String SPRING_MARKER_KEY = "role";
	protected static final String SPRING_MARKER_VALUE = "spring-app";

	private static final String SERVER_PORT_KEY = "server.port";

	protected final Log logger = LogFactory.getLog(getClass().getName());

	protected ContainerFactory containerFactory;

	protected KubernetesClient client;

	protected KubernetesDeployerProperties properties;

	protected DeploymentPropertiesResolver deploymentPropertiesResolver;

	/**
	 * Create the RuntimeEnvironmentInfo.
	 *
	 * @param spiClass the SPI interface class
	 * @param implementationClass the SPI implementation class
	 * @return the Kubernetes runtime environment info
	 */
	protected RuntimeEnvironmentInfo createRuntimeEnvironmentInfo(Class spiClass, Class implementationClass) {
		return new RuntimeEnvironmentInfo.Builder()
				.spiClass(spiClass)
				.implementationName(implementationClass.getSimpleName())
				.implementationVersion(RuntimeVersionUtils.getVersion(implementationClass))
				.platformType("Kubernetes")
				.platformApiVersion(client.getApiVersion())
				.platformClientVersion(RuntimeVersionUtils.getVersion(client.getClass()))
				.platformHostVersion("unknown")
				.addPlatformSpecificInfo("master-url", String.valueOf(client.getMasterUrl()))
				.addPlatformSpecificInfo("namespace", client.getNamespace())
				.build();
	}

	/**
	 * Creates a map of labels for a given application ID.
	 *
	 * @param appId the application id
	 * @param request The {@link AppDeploymentRequest}
	 * @return the built id map of labels
	 */
	Map<String, String> createIdMap(String appId, AppDeploymentRequest request) {
		Map<String, String> map = new HashMap<>();
		map.put(SPRING_APP_KEY, appId);
		String groupId = request.getDeploymentProperties().get(AppDeployer.GROUP_PROPERTY_KEY);
		if (groupId != null) {
			map.put(SPRING_GROUP_KEY, groupId);
		}
		map.put(SPRING_DEPLOYMENT_KEY, appId);
		return map;
	}

	protected AppStatus buildAppStatus(String id, PodList podList, ServiceList services) {
		AppStatus.Builder statusBuilder = AppStatus.of(id);
		Service service = null;
		if (podList != null && podList.getItems() != null) {
			for (Pod pod : podList.getItems()) {
				String deploymentKey = pod.getMetadata().getLabels().get(SPRING_DEPLOYMENT_KEY);
				for (Service svc : services.getItems()) {
					if (svc.getMetadata().getName().equals(deploymentKey)) {
						service = svc;
						break;
					}
				}
				//find the container with the correct env var
				for(Container container : pod.getSpec().getContainers()) {
					if(container.getEnv().stream().anyMatch(envVar -> "SPRING_CLOUD_APPLICATION_GUID".equals(envVar.getName()))) {
						//find container status for this container
						Optional<ContainerStatus> containerStatusOptional =
							pod.getStatus().getContainerStatuses()
							   .stream().filter(containerStatus -> container.getName().equals(containerStatus.getName()))
							   .findFirst();

						statusBuilder.with(new KubernetesAppInstanceStatus(pod, service, properties, containerStatusOptional.orElse(null)));

						break;
					}
				}
			}
		}
		return statusBuilder.build();
	}

	protected void logPossibleDownloadResourceMessage(Resource resource) {
		if (logger.isInfoEnabled()) {
			logger.info("Preparing to run a container from  " + resource
					+ ". This may take some time if the image must be downloaded from a remote container registry.");
		}
	}

	/**
	 * Create PodSpec for the given {@link AppDeploymentRequest}

	 * @param appDeploymentRequest the app deployment request to use to create the PodSpec
	 * @return the PodSpec
	 */
	PodSpec createPodSpec(AppDeploymentRequest appDeploymentRequest) {

		String appId = createDeploymentId(appDeploymentRequest);

		Map<String, String>  deploymentProperties = (appDeploymentRequest instanceof ScheduleRequest) ?
				((ScheduleRequest) appDeploymentRequest).getSchedulerProperties() : appDeploymentRequest.getDeploymentProperties();

		PodSpecBuilder podSpec = new PodSpecBuilder();

		String imagePullSecret = this.deploymentPropertiesResolver.getImagePullSecret(deploymentProperties);

		if (imagePullSecret != null) {
			podSpec.addNewImagePullSecret(imagePullSecret);
		}

		boolean hostNetwork = this.deploymentPropertiesResolver.getHostNetwork(deploymentProperties);

		ContainerConfiguration containerConfiguration = new ContainerConfiguration(appId, appDeploymentRequest)
				.withProbeCredentialsSecret(getProbeCredentialsSecret(deploymentProperties))
				.withHostNetwork(hostNetwork);

		if (KubernetesAppDeployer.class.isAssignableFrom(this.getClass())) {
			containerConfiguration.withExternalPort(getExternalPort(appDeploymentRequest));
		}

		Container container = containerFactory.create(containerConfiguration);

		// add memory and cpu resource limits
		ResourceRequirements req = new ResourceRequirements();
		req.setLimits(this.deploymentPropertiesResolver.deduceResourceLimits(deploymentProperties));
		req.setRequests(this.deploymentPropertiesResolver.deduceResourceRequests(deploymentProperties));
		container.setResources(req);
		ImagePullPolicy pullPolicy = this.deploymentPropertiesResolver.deduceImagePullPolicy(deploymentProperties);
		container.setImagePullPolicy(pullPolicy.name());

		Map<String, String> nodeSelectors = this.deploymentPropertiesResolver.getNodeSelectors(deploymentProperties);
		if (nodeSelectors.size() > 0) {
			podSpec.withNodeSelector(nodeSelectors);
		}

		podSpec.withTolerations(this.deploymentPropertiesResolver.getTolerations(deploymentProperties));

		// only add volumes with corresponding volume mounts
		podSpec.withVolumes(this.deploymentPropertiesResolver.getVolumes(deploymentProperties).stream()
				.filter(volume -> container.getVolumeMounts().stream()
						.anyMatch(volumeMount -> volumeMount.getName().equals(volume.getName())))
				.collect(Collectors.toList()));

		if (hostNetwork) {
			podSpec.withHostNetwork(true);
		}
		podSpec.addToContainers(container);

		podSpec.withRestartPolicy(this.deploymentPropertiesResolver.getRestartPolicy(deploymentProperties).name());

		String deploymentServiceAcccountName = this.deploymentPropertiesResolver.getDeploymentServiceAccountName(deploymentProperties);

		if (deploymentServiceAcccountName != null) {
			podSpec.withServiceAccountName(deploymentServiceAcccountName);
		}

		PodSecurityContext podSecurityContext = this.deploymentPropertiesResolver.getPodSecurityContext(deploymentProperties);
		if (podSecurityContext != null) {
			podSpec.withSecurityContext(podSecurityContext);
		}

		Affinity affinity = this.deploymentPropertiesResolver.getAffinityRules(deploymentProperties);
		// Make sure there is at least some rule.
		if (affinity.getNodeAffinity() != null
				|| affinity.getPodAffinity() != null
				|| affinity.getPodAntiAffinity() != null) {
			podSpec.withAffinity(affinity);
		}

		Container initContainer = this.deploymentPropertiesResolver.getInitContainer(deploymentProperties);
		if (initContainer != null) {
			podSpec.addToInitContainers(initContainer);
		}

		return podSpec.build();
	}

	int getExternalPort(final AppDeploymentRequest request) {
		int externalPort = 8080;
		Map<String, String> parameters = request.getDefinition().getProperties();
		if (parameters.containsKey(SERVER_PORT_KEY)) {
			externalPort = Integer.valueOf(parameters.get(SERVER_PORT_KEY));
		}

		return externalPort;
	}

	String createDeploymentId(AppDeploymentRequest request) {
		String groupId = request.getDeploymentProperties().get(AppDeployer.GROUP_PROPERTY_KEY);
		String deploymentId;
		if (groupId == null) {
			deploymentId = String.format("%s", request.getDefinition().getName());
		}
		else {
			deploymentId = String.format("%s-%s", groupId, request.getDefinition().getName());
		}
		// Kubernetes does not allow . in the name and does not allow uppercase in the name
		return deploymentId.replace('.', '-').toLowerCase();
	}

	/**
	 * Return the Secret corresponds to the name of ProbeCredentialSecret
	 * @param kubernetesDeployerProperties the kubernetes deployer properties
	 * @return the Secret Object
	 */
	Secret getProbeCredentialsSecret(Map<String, String> kubernetesDeployerProperties) {
		String secretName = PropertyParserUtils.getDeploymentPropertyValue(kubernetesDeployerProperties,
				this.deploymentPropertiesResolver.getPropertyPrefix() + ".probeCredentialsSecret");

		if (!StringUtils.isEmpty(secretName)) {
			return this.client.secrets().withName(secretName).get();
		}

		return null;
	}

}