/*
 * Copyright 2017 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.admin.shell.command;

import java.io.File;
import java.io.FileInputStream;
import java.io.IOException;
import java.util.Collections;
import java.util.LinkedHashMap;
import java.util.Map;
import java.util.Properties;

import org.apache.commons.io.FilenameUtils;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.config.YamlPropertiesFactoryBean;
import org.springframework.cloud.deployer.admin.rest.client.ApplicationOperations;
import org.springframework.cloud.deployer.admin.rest.client.DataFlowOperations;
import org.springframework.cloud.deployer.admin.rest.resource.ApplicationDefinitionResource;
import org.springframework.cloud.deployer.admin.rest.util.DeploymentPropertiesUtils;
import org.springframework.cloud.deployer.admin.shell.config.DataFlowShell;
import org.springframework.core.io.FileSystemResource;
import org.springframework.hateoas.PagedResources;
import org.springframework.shell.core.CommandMarker;
import org.springframework.shell.core.annotation.CliAvailabilityIndicator;
import org.springframework.shell.core.annotation.CliCommand;
import org.springframework.shell.core.annotation.CliOption;
import org.springframework.shell.table.BeanListTableModel;
import org.springframework.shell.table.Table;
import org.springframework.shell.table.TableBuilder;
import org.springframework.stereotype.Component;

/**
 * Application commands sharing same space for registry commands
 * but handling individual application deployments.
 *
 * @author Janne Valkealahti
 */
@Component
public class ApplicationCommands implements CommandMarker {

	private static final String LIST_APPLICATION = "app instances";
	private static final String CREATE_APPLICATION = "app create";
	private static final String DEPLOY_APPLICATION = "app deploy";
	private static final String UNDEPLOY_APPLICATION = "app undeploy";
	private static final String DESTROY_APPLICATION = "app destroy";
	private static final String UNDEPLOY_APPLICATION_ALL = "app all undeploy";
	private static final String DESTROY_APPLICATION_ALL = "app all destroy";
	private static final String PROPERTIES_OPTION = "properties";
	private static final String PROPERTIES_FILE_OPTION = "propertiesFile";

	@Autowired
	private DataFlowShell dataFlowShell;

	@Autowired
	private UserInput userInput;

	@CliAvailabilityIndicator({ LIST_APPLICATION, CREATE_APPLICATION, DEPLOY_APPLICATION, UNDEPLOY_APPLICATION,
			DESTROY_APPLICATION, UNDEPLOY_APPLICATION_ALL, DESTROY_APPLICATION_ALL })
	public boolean available() {
		DataFlowOperations dataFlowOperations = dataFlowShell.getDataFlowOperations();
		return dataFlowOperations != null && dataFlowOperations.applicationOperations() != null;
	}

	@CliCommand(value = LIST_APPLICATION, help = "List created applications")
	public Table listApplications() {
		final PagedResources<ApplicationDefinitionResource> applications = applicationOperations().list();
		LinkedHashMap<String, Object> headers = new LinkedHashMap<>();
		headers.put("name", "Application Name");
		headers.put("dslText", "Application Definition");
		headers.put("status", "Status");
		BeanListTableModel<ApplicationDefinitionResource> model = new BeanListTableModel<>(applications, headers);
		return DataFlowTables.applyStyle(new TableBuilder(model))
				.build();
	}

	@CliCommand(value = CREATE_APPLICATION, help = "Create a new application definition")
	public String createApplication(
			@CliOption(mandatory = true, key = { "", "name" }, help = "the name to give to the application") String name,
			@CliOption(mandatory = true, key = { "definition" }, help = "a application definition, using the DSL") String dsl,
			@CliOption(key = "deploy", help = "whether to deploy the application immediately", unspecifiedDefaultValue = "false", specifiedDefaultValue = "true") boolean deploy) {
		applicationOperations().createApplication(name, dsl, deploy);
		String message = String.format("Created new application '%s'", name);
		if (deploy) {
			message += "\nDeployment request has been sent";
		}
		return message;
	}

	@CliCommand(value = DEPLOY_APPLICATION, help = "Deploy a previously created application")
	public String deployApplication(
			@CliOption(key = { "", "name" }, help = "the name of the application to deploy", mandatory = true) String name,
			@CliOption(key = { PROPERTIES_OPTION }, help = "the properties for this deployment", mandatory = false) String properties,
			@CliOption(key = { PROPERTIES_FILE_OPTION }, help = "the properties for this deployment (as a File)", mandatory = false) File propertiesFile
			) throws IOException {
		int which = Assertions.atMostOneOf(PROPERTIES_OPTION, properties, PROPERTIES_FILE_OPTION, propertiesFile);
		Map<String, String> propertiesToUse;
		switch (which) {
			case 0:
				propertiesToUse = DeploymentPropertiesUtils.parse(properties);
				break;
			case 1:
				String extension = FilenameUtils.getExtension(propertiesFile.getName());
				Properties props = null;
				if (extension.equals("yaml") || extension.equals("yml")) {
					YamlPropertiesFactoryBean yamlPropertiesFactoryBean = new YamlPropertiesFactoryBean();
					yamlPropertiesFactoryBean.setResources(new FileSystemResource(propertiesFile));
					yamlPropertiesFactoryBean.afterPropertiesSet();
					props = yamlPropertiesFactoryBean.getObject();
				}
				else {
					props = new Properties();
					try (FileInputStream fis = new FileInputStream(propertiesFile)) {
						props.load(fis);
					}
				}
				propertiesToUse = DeploymentPropertiesUtils.convert(props);
				break;
			case -1: // Neither option specified
				propertiesToUse = Collections.<String, String> emptyMap();
				break;
			default:
				throw new AssertionError();
		}
		applicationOperations().deploy(name, propertiesToUse);
		return String.format("Deployment request has been sent for application '%s'", name);
	}

	@CliCommand(value = UNDEPLOY_APPLICATION, help = "Un-deploy a previously deployed application")
	public String undeployApplication(
			@CliOption(key = { "", "name" }, help = "the name of the application to un-deploy", mandatory = true) String name
			) {
		applicationOperations().undeploy(name);
		return String.format("Un-deployed application '%s'", name);
	}

	@CliCommand(value = DESTROY_APPLICATION, help = "Destroy an existing application")
	public String destroyApplication(
			@CliOption(key = { "", "name" }, help = "the name of the application to destroy", mandatory = true) String name) {
		applicationOperations().destroy(name);
		return String.format("Destroyed application '%s'", name);
	}

	@CliCommand(value = UNDEPLOY_APPLICATION_ALL, help = "Un-deploy all previously deployed applications")
	public String undeployAllApplications(
			@CliOption(key = "force", help = "bypass confirmation prompt", unspecifiedDefaultValue = "false", specifiedDefaultValue = "true") boolean force
			) {
		if (force || "y".equalsIgnoreCase(userInput.promptWithOptions("Really undeploy all applications?", "n", "y", "n"))) {
			applicationOperations().undeployAll();
			return String.format("Un-deployed all the applications");
		}
		else {
			return "";
		}
	}

	@CliCommand(value = DESTROY_APPLICATION_ALL, help = "Destroy all existing applications")
	public String destroyAllApplications(
			@CliOption(key = "force", help = "bypass confirmation prompt", unspecifiedDefaultValue = "false", specifiedDefaultValue = "true") boolean force) {
		if (force || "y".equalsIgnoreCase(userInput.promptWithOptions("Really destroy all applications?", "n", "y", "n"))) {
			applicationOperations().destroyAll();
			return "Destroyed all applications";
		}
		else {
			return "";
		}
	}

	ApplicationOperations applicationOperations() {
		return dataFlowShell.getDataFlowOperations().applicationOperations();
	}
}