/*-
 * ~~~~~~licensing~~~~~~
 * test-producers
 * ==========
 * Copyright (C) 2020 - 2022 EmDev LLC
 * ==========
 * 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.
 * ~~~~~~/licensing~~~~~~
 */
package ru.entaxy.platform.core.producer.executor;

import java.util.ArrayList;
import java.util.Collections;
import java.util.HashMap;
import java.util.Iterator;
import java.util.LinkedList;
import java.util.List;
import java.util.Map;

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import com.google.gson.JsonElement;
import com.google.gson.JsonObject;

import ru.entaxy.platform.base.support.CommonUtils;
import ru.entaxy.platform.base.support.DependencySorter;
import ru.entaxy.platform.base.support.JSONUtils;
import ru.entaxy.platform.core.producer.api.EntaxyProducerService;
import ru.entaxy.platform.core.producer.api.ExecutionPlan;
import ru.entaxy.platform.core.producer.api.ExecutionPlan.ExecutionPlanUpdate;
import ru.entaxy.platform.core.producer.api.ExecutionPlan.ExecutionPlanUpdate.OPERATIONS;
import ru.entaxy.platform.core.producer.api.ExecutionTask;
import ru.entaxy.platform.core.producer.api.ProducerResult;
import ru.entaxy.platform.core.producer.api.ProducerResult.CommandResult;

public class ProducingExecutor {

	private static final Logger log = LoggerFactory.getLogger(ProducingExecutor.class);
	
	protected static final String DEFAULT_EXECUTION_PLAN = "{'validate':{}}";
	
	protected static final String ANY_COMMAND = "*";
	
	protected JsonObject configuration;
	protected EntaxyProducerService producerService;
	
	protected JsonObject executionPlanJSON = JSONUtils.getJsonRootObject(DEFAULT_EXECUTION_PLAN); 
	
	protected ExecutionPlan executionPlan = null;
	
	public ProducingExecutor(JsonObject configuration, String buildInstructions
			, EntaxyProducerService producerService) throws Exception {
		this.configuration = configuration;
		this.producerService = producerService;
		if (CommonUtils.isValid(buildInstructions)) {
			executionPlanJSON = JSONUtils.getJsonRootObject(buildInstructions);
			if (executionPlanJSON.entrySet().isEmpty())
				executionPlanJSON = JSONUtils.getJsonRootObject(DEFAULT_EXECUTION_PLAN);
		}
		buildPlan();
	}
	
	protected ExecutionTask createExecutionTask(String id, Map<String, Object> parameters) {
		ExecutionTask result = new ExecutionTask();
		
		result.commandId = id;
		result.instructions = parameters;
		result.executor = producerService.getCommandById(result.commandId);
		
		return result;
	}
	
	protected ExecutionTask createExecutionTask(Map<String, ExecutionTask> taskMap, String id, Map<String, Object> commandInstructions) {
		if (!taskMap.containsKey(id))
			taskMap.put(id, new ExecutionTask(id));
		ExecutionTask result = taskMap.get(id);
		result.instructions = commandInstructions;
		result.executor = producerService.getCommandById(result.commandId);
		
		return result;
		
	}
	
	protected void createDependency(Map<String, ExecutionTask> taskMap, String masterId, String slaveId) {
		taskMap.get(slaveId).dependsOn.add(taskMap.get(masterId));
	}
	
	protected void buildPlan() throws Exception {
		log.debug("building plan for execution on: \n" + configuration.toString());
		log.debug("execution plan: \n" + executionPlanJSON.toString());
		
		Map<String, ExecutionTask> taskMap = new HashMap<>();
		
		Map<String, Object> anyCommandInstructions = new HashMap<>();
		if (executionPlanJSON.has(ANY_COMMAND))
			anyCommandInstructions = JSONUtils.element2map(executionPlanJSON.get(ANY_COMMAND));
		
		List<String> neededCommands = new ArrayList<>();
		for (String id: executionPlanJSON.keySet()) {
			if (ANY_COMMAND.equals(id))
				continue;
			JsonElement jsonElement = executionPlanJSON.get(id);
			Map<String, Object> commandInstructions = new HashMap<>(anyCommandInstructions);
			commandInstructions.putAll(JSONUtils.element2map(jsonElement));
			neededCommands.add(id);
			if (createExecutionTask(taskMap, id, commandInstructions).executor == null) {
				log.error("Command [{}] not found", id);
				throw new Exception("Command not found: " + id);
			}
		}
		
		List<String> commandsToCheck = List.copyOf(neededCommands);
		List<String> newCommandsToCheck = new ArrayList<>();
		
		// add required commands
		
		while (!commandsToCheck.isEmpty()) {
			newCommandsToCheck.clear();
			for (String command: commandsToCheck) {
				log.debug("Checking command [{}]", command);
				for (String s: taskMap.get(command).executor.getRequiredPredecessors()) {
					if (!neededCommands.contains(s)) {
						log.debug("Adding command [{}]", s);
						newCommandsToCheck.add(s);
						neededCommands.add(s);
						if (createExecutionTask(taskMap, s, anyCommandInstructions).executor == null) {
							log.error("Command [{}] not found", s);
							throw new Exception("Command not found: " + s);
						}
					}
					createDependency(taskMap, s, command);
				}
			}
			commandsToCheck = List.copyOf(newCommandsToCheck);
		}
		
		// create backward dependencies by descendants
		for (String s: taskMap.keySet()) {
			for (String descendant: taskMap.get(s).executor.getDescendants()) {
				if (taskMap.containsKey(descendant))
					createDependency(taskMap, s, descendant);
			}
		}
		
		
		// arrange commands
		
		executionPlan = new ExecutionPlan(new LinkedList<>(taskMap.values()));
		List<ExecutionTask> sorted = DependencySorter.getSortedList(
				executionPlan.tasks, 
				new DependencySorter.DependencyProvider<ExecutionTask>() {

					@Override
					public List<ExecutionTask> getDependencies(ExecutionTask inspectedObject) {
						return inspectedObject.dependsOn;
					}
					
				});
		executionPlan.tasks = sorted;

	}
	
	public ProducerResult execute() {
		
		ProducerResult producerResult = new ProducerResult();
		
		producerResult.commandResult("#plan").result(true).resultObject(executionPlan);
		producerResult.commandResult("#init").result(true).resultObject(configuration);
		
		return execute(producerResult);
		
	}
	
	protected ProducerResult execute(ProducerResult producerResult) {
		
		Iterator<ExecutionTask> taskIterator = executionPlan.iterator();
		
		while (taskIterator.hasNext()) {
			ExecutionTask task = taskIterator.next(); 
			task.executor.execute(producerResult, task.instructions);
			CommandResult cr = producerResult.getLastCommandResult();
			if (cr.planUpdate != null) {
				try {
					log.debug("Applying plan update");
					List<ExecutionPlanUpdate.OPERATIONS> result = executionPlan.applyUpdate(cr.planUpdate);
					producerResult.commandResult("#planUpdate").result(true).resultObject(cr.planUpdate);
					if (result.contains(OPERATIONS.RESET))
						producerResult.commandResult("#reset").result(true).resultObject(cr.planUpdate);
				} catch (Exception e) {
					log.error("Error applying plan update", e);
					producerResult.commandResult("#planUpdate").result(false).resultObject(cr.planUpdate).exception(e);
				}
			}
		}
		
		return producerResult;
	}
}
