/*
 * Copyright 2018-2019 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
 *
 *      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 org.springframework.cloud.deployer.spi.kubernetes;

import java.util.ArrayList;
import java.util.Collections;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.regex.Pattern;
import java.util.stream.Collectors;

import io.fabric8.kubernetes.api.model.LocalObjectReference;
import io.fabric8.kubernetes.api.model.PodSpec;
import io.fabric8.kubernetes.api.model.StatusCause;
import io.fabric8.kubernetes.api.model.batch.CronJob;
import io.fabric8.kubernetes.api.model.batch.CronJobBuilder;
import io.fabric8.kubernetes.api.model.batch.CronJobList;
import io.fabric8.kubernetes.client.KubernetesClient;
import io.fabric8.kubernetes.client.KubernetesClientException;

import org.springframework.cloud.deployer.spi.scheduler.CreateScheduleException;
import org.springframework.cloud.deployer.spi.scheduler.ScheduleInfo;
import org.springframework.cloud.deployer.spi.scheduler.ScheduleRequest;
import org.springframework.cloud.deployer.spi.scheduler.Scheduler;
import org.springframework.cloud.deployer.spi.scheduler.SchedulerException;
import org.springframework.cloud.deployer.spi.scheduler.SchedulerPropertyKeys;
import org.springframework.util.Assert;
import org.springframework.util.CollectionUtils;
import org.springframework.util.StringUtils;

/**
 * Kubernetes implementation of the {@link Scheduler} SPI.
 *
 * @author Chris Schaefer
 * @author Ilayaperumal Gopinathan
 */
public class KubernetesScheduler extends AbstractKubernetesDeployer implements Scheduler {
	private static final String SPRING_CRONJOB_ID_KEY = "spring-cronjob-id";

	private static final String SCHEDULE_EXPRESSION_FIELD_NAME = "spec.schedule";

	public KubernetesScheduler(KubernetesClient client,
			KubernetesSchedulerProperties properties) {
		Assert.notNull(client, "KubernetesClient must not be null");
		Assert.notNull(properties, "KubernetesSchedulerProperties must not be null");

		this.client = client;
		this.properties = properties;
		this.containerFactory = new DefaultContainerFactory(properties);
		this.deploymentPropertiesResolver = new DeploymentPropertiesResolver(
				KubernetesSchedulerProperties.KUBERNETES_SCHEDULER_PROPERTIES_PREFIX, properties);
	}

	@Override
	public void schedule(ScheduleRequest scheduleRequest) {
		scheduleRequest.setSchedulerProperties(mergeSchedulerProperties(scheduleRequest));
		if(scheduleRequest != null) {
			validateScheduleName(scheduleRequest);
		}
		try {
			createCronJob(scheduleRequest);
		}
		catch (KubernetesClientException e) {
			String invalidCronExceptionMessage = getExceptionMessageForField(e, SCHEDULE_EXPRESSION_FIELD_NAME);

			if (StringUtils.hasText(invalidCronExceptionMessage)) {
				throw new CreateScheduleException(invalidCronExceptionMessage, e);
			}

			throw new CreateScheduleException("Failed to create schedule " + scheduleRequest.getScheduleName(), e);
		}
	}

	/**
	 * Merge the Deployment properties into Scheduler properties.
	 * This way, the CronJob's scheduler properties are updated with the deployer properties if set any.
	 * @param scheduleRequest the {@link ScheduleRequest}
	 * @return the merged schedule properties
	 */
	static Map<String, String> mergeSchedulerProperties(ScheduleRequest scheduleRequest) {
		Map<String, String> deploymentProperties = scheduleRequest.getDeploymentProperties();
		Map<String, String> schedulerProperties = new HashMap<>();
		schedulerProperties.putAll(scheduleRequest.getSchedulerProperties());
		if (deploymentProperties != null) {
			for (Map.Entry<String, String> deploymentProperty : deploymentProperties.entrySet()) {
				String deploymentPropertyKey = deploymentProperty.getKey();
				if (StringUtils.hasText(deploymentPropertyKey) && deploymentPropertyKey.startsWith(KubernetesDeployerProperties.KUBERNETES_DEPLOYER_PROPERTIES_PREFIX)) {
					String schedulerPropertyKey = KubernetesSchedulerProperties.KUBERNETES_SCHEDULER_PROPERTIES_PREFIX +
							deploymentPropertyKey.substring(KubernetesDeployerProperties.KUBERNETES_DEPLOYER_PROPERTIES_PREFIX.length());
					if (!schedulerProperties.containsKey(schedulerPropertyKey)) {
						schedulerProperties.put(schedulerPropertyKey, deploymentProperty.getValue());
					}
				}
			}
		}
		return schedulerProperties;
	}

	public void validateScheduleName(ScheduleRequest request) {
		if(request.getScheduleName() == null) {
			throw new CreateScheduleException("The name for the schedule request is null", null);
		}
		if(request.getScheduleName().length() > 52) {
			throw new CreateScheduleException(String.format("because Schedule Name: '%s' has too many characters.  Schedule name length must be 52 characters or less", request.getScheduleName()), null);
		}
		if(!Pattern.matches("^[a-z0-9]([-a-z0-9]*[a-z0-9])?$", request.getScheduleName())) {
			throw new CreateScheduleException("Invalid Format for Schedule Name. Schedule name can only contain lowercase letters, numbers 0-9 and hyphens.", null);
		}

	}

	@Override
	public void unschedule(String scheduleName) {
		boolean unscheduled = this.client.batch().cronjobs().withName(scheduleName).delete();

		if (!unscheduled) {
			throw new SchedulerException("Failed to unschedule schedule " + scheduleName + " does not exist.");
		}
	}

	@Override
	public List<ScheduleInfo> list(String taskDefinitionName) {
		return list()
				.stream()
				.filter(scheduleInfo -> taskDefinitionName.equals(scheduleInfo.getTaskDefinitionName()))
				.collect(Collectors.toList());
	}

	@Override
	public List<ScheduleInfo> list() {
		CronJobList cronJobList = this.client.batch().cronjobs().list();

		List<CronJob> cronJobs = cronJobList.getItems();
		List<ScheduleInfo> scheduleInfos = new ArrayList<>();

		for (CronJob cronJob : cronJobs) {
			if (cronJob.getMetadata() != null && cronJob.getMetadata().getLabels() != null &&
					StringUtils.hasText(cronJob.getMetadata().getLabels().get(SPRING_CRONJOB_ID_KEY))) {
				Map<String, String> properties = new HashMap<>();
				properties.put(SchedulerPropertyKeys.CRON_EXPRESSION, cronJob.getSpec().getSchedule());

				ScheduleInfo scheduleInfo = new ScheduleInfo();
				scheduleInfo.setScheduleName(cronJob.getMetadata().getName());
				scheduleInfo.setTaskDefinitionName(cronJob.getMetadata().getLabels().get(SPRING_CRONJOB_ID_KEY));
				scheduleInfo.setScheduleProperties(properties);

				scheduleInfos.add(scheduleInfo);
			}
		}

		return scheduleInfos;
	}

	protected CronJob createCronJob(ScheduleRequest scheduleRequest) {
		Map<String, String> labels = Collections.singletonMap(SPRING_CRONJOB_ID_KEY,
				scheduleRequest.getDefinition().getName());

		Map<String, String> schedulerProperties = scheduleRequest.getSchedulerProperties();
		String schedule = schedulerProperties.get(SchedulerPropertyKeys.CRON_EXPRESSION);
		Assert.hasText(schedule, "The property: " + SchedulerPropertyKeys.CRON_EXPRESSION + " must be defined");

		PodSpec podSpec = createPodSpec(scheduleRequest);
		String taskServiceAccountName = this.deploymentPropertiesResolver.getTaskServiceAccountName(scheduleRequest.getSchedulerProperties());
		if (StringUtils.hasText(taskServiceAccountName)) {
			podSpec.setServiceAccountName(taskServiceAccountName);
		}

		CronJob cronJob = new CronJobBuilder().withNewMetadata().withName(scheduleRequest.getScheduleName())
				.withLabels(labels).endMetadata().withNewSpec().withSchedule(schedule).withNewJobTemplate()
				.withNewSpec().withNewTemplate().withSpec(podSpec).endTemplate().endSpec()
				.endJobTemplate().endSpec().build();

		setImagePullSecret(scheduleRequest, cronJob);

		return this.client.batch().cronjobs().create(cronJob);
	}

	protected String getExceptionMessageForField(KubernetesClientException clientException,
			String fieldName) {
		List<StatusCause> statusCauses = clientException.getStatus().getDetails().getCauses();

		if (!CollectionUtils.isEmpty(statusCauses)) {
			for (StatusCause statusCause : statusCauses) {
				if (fieldName.equals(statusCause.getField())) {
					return clientException.getStatus().getMessage();
				}
			}
		}

		return null;
	}

	private void setImagePullSecret(ScheduleRequest scheduleRequest, CronJob cronJob) {

		String imagePullSecret = this.deploymentPropertiesResolver.getImagePullSecret(scheduleRequest.getSchedulerProperties());

		if (StringUtils.hasText(imagePullSecret)) {
			LocalObjectReference localObjectReference = new LocalObjectReference();
			localObjectReference.setName(imagePullSecret);

			cronJob.getSpec().getJobTemplate().getSpec().getTemplate().getSpec().getImagePullSecrets()
					.add(localObjectReference);
		}
	}
}