/*-
 * ~~~~~~licensing~~~~~~
 * test-producers
 * ==========
 * Copyright (C) 2020 - 2025 EmDev LLC
 * ==========
 * You may not use this file except in accordance with the License Terms of the Copyright
 * Holder located at: https://entaxy.ru/eula . All copyrights, all intellectual property
 * rights to the Software and any copies are the property of the Copyright Holder. Unless
 * it is explicitly allowed the Copyright Holder, the User is prohibited from using the
 * Software for commercial purposes to provide services to third parties.
 * 
 * The Copyright Holder hereby declares that the Software is provided on an "AS IS".
 * Under no circumstances does the Copyright Holder guarantee or promise that the
 * Software provided by him will be suitable or not suitable for the specific purposes
 * of the User, that the Software will meet all commercial and personal subjective
 * expectations of the User, that the Software will work properly, without technical
 * errors, quickly and uninterruptedly.
 * 
 * Under no circumstances shall the Copyright Holder or its Affiliates is not liable
 * to the User for any direct or indirect losses of the User, his expenses or actual
 * damage, including, downtime; loss of bussines; lost profit; lost earnings; loss
 * or damage to data, property, etc.
 * ~~~~~~/licensing~~~~~~
 */
package ru.entaxy.platform.core.producer.executor;

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

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

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

import ru.entaxy.esb.platform.runtime.base.extensions.api.EntaxyExtendable;
import ru.entaxy.esb.platform.runtime.base.extensions.api.EntaxyExtension;
import ru.entaxy.esb.platform.runtime.base.extensions.api.EntaxyExtensionsProvider;
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.EntaxyProducerService.INSTRUCTIONS;
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);
	
	public static final String DIRECTIVE_SKIP = EntaxyProducerService.DIRECTIVES.SKIP;
	
	public static final String DIRECTIVE_CONTINUE_ON_EXCEPTION = EntaxyProducerService.DIRECTIVES.CONTINUE_ON_EXCEPTION;
	
	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;
	
	protected final List<EntaxyExtension> extensions = new ArrayList<>();
	
	public static class EntaxyExtensionsProviderImpl implements EntaxyExtensionsProvider {
		
		List<EntaxyExtension> entaxyExtensions;
		
		public EntaxyExtensionsProviderImpl(List<EntaxyExtension> entaxyExtensions) {
			if (entaxyExtensions == null)
				this.entaxyExtensions = new ArrayList<>();
			else
				this.entaxyExtensions = entaxyExtensions;
		}
		
		@Override
		public List<EntaxyExtension> getExtensions() {
			return this.entaxyExtensions;
		}
		
	}
	
	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<>();
		JsonObject directives = new JsonObject();
		if (executionPlanJSON.has(ANY_COMMAND)) {
			JsonElement anyCommandElement = executionPlanJSON.get(ANY_COMMAND).deepCopy();
			if (anyCommandElement.isJsonObject()) {
				JsonObject anyCommandObject = anyCommandElement.getAsJsonObject();
				List<String> toRemove = new ArrayList<>();
				for (Entry<String, JsonElement> entry: anyCommandObject.entrySet())
					if (entry.getKey().startsWith("@")) {
						directives.add(entry.getKey(), entry.getValue().deepCopy());
						toRemove.add(entry.getKey());
					}
				// remove directives
				for (String s: toRemove)
					anyCommandObject.remove(s);
			}
			anyCommandInstructions = JSONUtils.element2map(anyCommandElement);
		}
		
		
		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, new HashMap<>(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;
		applyDirectives(directives);
		
	}
	
	protected void applyDirectives(JsonObject directives) {
		// apply @SKIP
		if (directives.has(DIRECTIVE_SKIP)) {
			JsonElement je = directives.get(DIRECTIVE_SKIP);
			applySimpleDirective(je, DIRECTIVE_SKIP, INSTRUCTIONS.SKIP);
		}
		if (directives.has(DIRECTIVE_CONTINUE_ON_EXCEPTION)) {
			JsonElement je = directives.get(DIRECTIVE_CONTINUE_ON_EXCEPTION);
			applySimpleDirective(je, DIRECTIVE_CONTINUE_ON_EXCEPTION, INSTRUCTIONS.CONTINUE_ON_EXCEPTION);
		}
	}
	
	protected void applySimpleDirective(JsonElement directiveData, String directiveName, String targetInstruction) {
		List<String> toAfffect = new ArrayList<>(); 
		if (directiveData.isJsonPrimitive())
			toAfffect.add(directiveData.getAsString());
		if (directiveData.isJsonArray()) {
			JsonArray ja = directiveData.getAsJsonArray();
			for (int i=0; i<ja.size(); i++)
				toAfffect.add(ja.get(i).getAsString());
		}
		for (String s: toAfffect) {
			boolean affectBefore = s.endsWith("-");
			boolean affectAfter = s.endsWith("+");
			if (affectBefore || affectAfter)
				s = s.substring(0, s.length()-1);
			if (affectBefore) {
				for (ExecutionTask et: executionPlan.tasks) {
					if (et.commandId.equals(s))
						break;
					et.instructions.put(targetInstruction, true);
				}
			} else if (affectAfter) {
				boolean found = false;
				for (ExecutionTask et: executionPlan.tasks) {
					if (et.commandId.equals(s)) {
						found = true;
						continue;
					}
					if (found)
						et.instructions.put(targetInstruction, true);
				}
				
			} else {
				for (ExecutionTask et: executionPlan.tasks) {
					if (et.commandId.equals(s)) {
						et.instructions.put(targetInstruction, true);
						break;
					}
				}
			}
		}		
	}
	
	public ProducerResult execute() {
		
		ProducerResult producerResult = new ProducerResult();
		
		producerResult.commandResult("#plan").result(true).resultObject(executionPlan);
		producerResult.commandResult("#init").result(true).resultObject(configuration);
		
		// add all available extensions to task executors
		if (producerService instanceof EntaxyExtendable) {
			producerResult.commandResult("#extensions").result(true).resultObject(
				(EntaxyExtensionsProvider)new EntaxyExtensionsProviderImpl(((EntaxyExtendable)producerService).getExtensions())	
					);
		} else {
			producerResult.commandResult("#extensions").result(true).resultObject(
					(EntaxyExtensionsProvider)new EntaxyExtensionsProviderImpl(null)	
						);			
		}

		
		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.result)
				if ((cr.exceptions != null) && !cr.exceptions.isEmpty()) {
					boolean continueOnException = (Boolean)task.instructions.getOrDefault(INSTRUCTIONS.CONTINUE_ON_EXCEPTION, false);
					if (!continueOnException)
						break;
				}
			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;
	}

}
