/*-
 * ~~~~~~licensing~~~~~~
 * schema-impl
 * ==========
 * 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.esb.platform.runtime.core.infrastructure.schema.impl;

import java.io.File;
import java.io.IOException;
import java.io.InputStream;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.HashSet;
import java.util.List;
import java.util.Locale;
import java.util.Map;
import java.util.Map.Entry;
import java.util.Set;
import java.util.UUID;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import java.util.stream.Collectors;

import org.apache.commons.io.FileUtils;
import org.apache.commons.lang3.StringUtils;
import org.jetbrains.annotations.NotNull;
import org.openapitools.codegen.languages.JavaCXFServerCodegen;
import org.openapitools.codegen.utils.CamelizeOption;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import com.google.gson.JsonArray;
import com.google.gson.JsonObject;
import com.ibm.icu.text.Transliterator;

import io.swagger.v3.oas.models.OpenAPI;
import io.swagger.v3.oas.models.Operation;
import io.swagger.v3.oas.models.PathItem;
import io.swagger.v3.oas.models.PathItem.HttpMethod;
import io.swagger.v3.oas.models.parameters.RequestBody;
import ru.entaxy.esb.platform.runtime.base.connecting.generator.Generated;
import ru.entaxy.esb.resources.EntaxyResource;
import ru.entaxy.esb.resources.EntaxyResource.RESOURCE_SCOPE;
import ru.entaxy.esb.resources.EntaxyResourceMetadata;
import ru.entaxy.esb.resources.EntaxyResourceMetadata.MetadataSection;
import ru.entaxy.esb.resources.EntaxyResourceProcessor;
import ru.entaxy.platform.base.objects.EntaxyObject;
import ru.entaxy.platform.base.support.CommonUtils;
import ru.entaxy.platform.base.support.JSONUtils;
import ru.entaxy.platform.base.support.JSONUtils.JsonBuilder.JsonArrayBuilder;
import ru.entaxy.platform.core.producer.api.EntaxyProducerUtils;
import ru.entaxy.platform.core.producer.api.ProducerResult;
import ru.entaxy.platform.core.producer.executor.generationmodel.GeneratedList;
import ru.entaxy.platform.core.support.design.openapi.EntaxyOpenApiGenerator;
import ru.entaxy.platform.core.support.design.openapi.EntaxyOpenApiGenerator.GenerationResult;
import ru.entaxy.platform.core.support.runtime.EntaxyResourceUtils;

public class OpenApiResourceProcessor implements EntaxyResourceProcessor {

	private static final Logger log = LoggerFactory.getLogger(OpenApiResourceProcessor.class);
	
	protected static final String YAML_EXTENSION = "yaml";
	protected static final String JSON_EXTENSION = "json";
	
	public static final String CYRILLIC_TO_LATIN = "Russian-Latin/BGN";

	protected static class OpenApiPostprocessor implements EntaxyOpenApiGenerator.GenerationCallback {

		private static final String GENERATED_RESOURCES = "generatedResources";

		protected EntaxyResource entaxyResource;

		protected EntaxyResourceMetadata metadata;
		protected EntaxyResourceMetadata.MetadataSection metadataSection;
		
		protected String resourcesRootLocation = "";
		protected String resourcesLocation = "";

		protected Set<String> reservedWords;
		
		public OpenApiPostprocessor(EntaxyResource entaxyResource) {
			this.entaxyResource = entaxyResource;
			this.setReservedWordsLowerCase(Arrays.asList("object", "list", "file", "localVarPath", "localVarQueryParams", "localVarCollectionQueryParams", "localVarHeaderParams", "localVarCookieParams", "localVarFormParams", "localVarPostBody", "localVarAccepts", "localVarAccept", "localVarContentTypes", "localVarContentType", "localVarAuthNames", "localReturnType", "ApiClient", "ApiException", "ApiResponse", "Configuration", "StringUtil", "abstract", "continue", "for", "new", "switch", "assert", "default", "if", "package", "synchronized", "boolean", "do", "goto", "private", "this", "break", "double", "implements", "protected", "throw", "byte", "else", "import", "public", "throws", "case", "enum", "instanceof", "return", "transient", "catch", "extends", "int", "short", "try", "char", "final", "interface", "static", "void", "class", "finally", "long", "strictfp", "volatile", "const", "float", "native", "super", "while", "null"));
		}
		
		public void process() {

			EntaxyOpenApiGenerator generator = new EntaxyOpenApiGenerator();
			
			String url = "entaxy-resource://" + entaxyResource.getURL().replace(":", "/");
			resourcesRootLocation = entaxyResource.getURL().replace(':', '/') + "/";

			String rootFolder = UUID.randomUUID().toString();
			String resourceName = url.substring(url.lastIndexOf('/')+1);
			Path target = Paths.get(System.getProperty("karaf.home"), "temp", rootFolder, resourceName);
			File f = null;
			
			try {
				f = EntaxyResourceUtils.downloadFile(target, url);
			} catch (Exception e) {
				log.error(String.format("Error downloading resource: [%s]->[%s]", url, target.toString()), e);
				return;
			}
			if (f != null) {
				url = f.getAbsolutePath();
				generator.setRootFolder(rootFolder);
			}
			
			generator.setUrl(url);
			generator.setCallback(this);
			GenerationResult gr = generator.generate();
			if (gr.isOk()) {
				String classesPath = gr.properties.get("classes.path").toString();
				String urlToUpload = "service-resources:" + this.resourcesLocation + "/classes";
				try {
					EntaxyResourceUtils.uploadFolder(classesPath, urlToUpload);
				} catch (IOException e) {
					log.error(String.format("FAILED upload: [%s]->[%s]", classesPath, urlToUpload), e);
				}
				
				@SuppressWarnings("unchecked")
				List<String> apiClasses = (List<String>)gr.properties.getOrDefault("classes.api", Collections.emptyList());
				@SuppressWarnings("unchecked")
				List<String> modelClasses = (List<String>)gr.properties.getOrDefault("classes.model", Collections.emptyList());
				
				log.debug("=== RESULT ===");
				
				JsonObject classes = new JsonObject();
				
				JsonArray apis = new JsonArray();
				log.debug("Api:");
				for (String s: apiClasses) {
					apis.add(s);
					log.debug("\t" + s);
				}

				JsonArray models = new JsonArray();
				log.debug("Model:");
				for (String s: modelClasses) {
					models.add(s);
					log.debug("\t" + s);
				}
				classes.addProperty("folder", "classes");
				classes.add("apis", apis);
				classes.add("models", models);
				this.metadataSection.getContent().add(OpenApiResourceMetadata.PROPERTIES.CLASSES, classes);
				
				// clean generatedResources
				metadata.getSection(EntaxyResourceMetadata.SECTION_RESOURCE.NAME).getContent().remove(GENERATED_RESOURCES);
				
				this.metadata.update();
				
				generateResources(entaxyResource, metadata);
				
				JsonArrayBuilder<?> builder = JSONUtils.JsonBuilder.create(metadata.getSection(EntaxyResourceMetadata.SECTION_RESOURCE.NAME).getContent())
						.array(GENERATED_RESOURCES, false);
				String prefix = "service-resources:" + this.resourcesLocation + "/classes/";
				for (int i=0; i<apis.size(); i++) {
					String classData = prefix + apis.get(i).getAsString().replace('.', '/') + ".class;runtime";
					builder.string(classData);
				}
				for (int i=0; i<models.size(); i++) {
					String classData = prefix + models.get(i).getAsString().replace('.', '/') + ".class;runtime";
					builder.string(classData);
				}
				
				this.metadata.update();
			}
			generator.clean();
			Path rootFolderPath = Paths.get(System.getProperty("karaf.home"), "temp", rootFolder);
			try {
				FileUtils.deleteDirectory(rootFolderPath.toAbsolutePath().toFile());
			} catch (Exception e) {
				log.error(String.format("Error deleting resource: [%s]->[%s]", url, rootFolderPath), e);
			}
		}
		
		@Override
		public void event(GenerationResult generationResult) {
			switch (generationResult.currentPhase) {
			case CHECK:
				if (!generationResult.isFinished() && generationResult.isCurrentPhaseOk()) {
					OpenAPI api = (OpenAPI)generationResult.properties.get("api");
					generationResult.generator.setApiTitleConverted(
							api.getInfo().getTitle().replace(' ', '_').replace('-', '_')
									.replace('.', '_').replaceAll("\\(|\\)", "_")
									.toLowerCase());
					generationResult.generator.setApiVersionConverted(
							api.getInfo().getVersion().replace('.', '_'));
					generationResult.generator.setApiPackage(translit("entaxy.runtime.service." 
							+ generationResult.generator.getApiTitleConverted() 
							+ ".v_" + generationResult.generator.getApiVersionConverted())
						 );
					processOpenApi(api);
				}
				break;
			default:
			}			
		}
		
		//TODO It makes sense to make a separate common util and put it there
		private String translit(String in) {
			//substitute ё, ъ, ь first
			in = in.replaceAll("ё", "е");
			in = in.replaceAll("ъ", "");
			in = in.replaceAll("ь", "");
			in = in.replaceAll("ы", "");
			Transliterator toLatinTrans = Transliterator.getInstance(CYRILLIC_TO_LATIN);
	        String result = toLatinTrans.transliterate(in);
			return result;
		}
		
		protected void processOpenApi(OpenAPI api) {
			this.metadata = entaxyResource.getMetadata(true);
			this.metadataSection = this.metadata.getSection(OpenApiResourceMetadata.SECTION_NAME);
			this.metadataSection.getContent().remove("error");
			JsonObject info = new JsonObject();
			info.addProperty("title", api.getInfo().getTitle());
			info.addProperty("version", api.getInfo().getVersion());

			JsonObject operationData = getOperationData(api);
			this.metadataSection.getContent().add(OpenApiResourceMetadata.PROPERTIES.INFO, info);
			this.metadataSection.getContent().add(OpenApiResourceMetadata.PROPERTIES.OPERATIONS, operationData);
			
			this.resourcesLocation = (this.resourcesRootLocation
					+ "/" + translit(api.getInfo().getTitle().replace(' ', '_'))
					+ "/" + api.getInfo().getVersion().replace(' ', '_'))
						.replace("//", "/");
			
			this.metadataSection.getContent().addProperty(WSDLResourceMetadata.PROPERTIES.SERVICE_RESOURCES_ROOT, this.resourcesLocation);
		}

		@NotNull
		private JsonObject getOperationData(OpenAPI api) {
			JsonObject operationData = new JsonObject();
			for (Entry<String, PathItem> entry: api.getPaths().entrySet()) {
				JsonObject pathData = new JsonObject();
				Map<HttpMethod, Operation> operationMap = entry.getValue().readOperationsMap();
				for (Entry<HttpMethod, Operation> oper: operationMap.entrySet()) {
					JsonObject operationId = new JsonObject();
					String operation = oper.getValue().getOperationId();
					if (oper.getValue().getOperationId() == null) {
						operation = entry.getKey() + "/" + oper.getKey().name().toLowerCase();
					}
					String path = entry.getKey();
					String parameter;
					boolean pathParameterized = false;
					JsonArray pathParameters = new JsonArray();
					Pattern p = Pattern.compile("^(?<pre>.*)\\{(?<param>[^\\.]+)\\}(?<tail>.*)$");
			        Matcher m = p.matcher(path);
			        while (m.find()) {
			            parameter = "NTX_CLIENT_" + m.group("param");
			            path = m.group("pre") + "${exchangeProperty." + parameter + "}" + m.group("tail");
			            m = p.matcher(path);
			            pathParameterized = true;
			            pathParameters.add(parameter);
			        }
			        operationId.add("pathParameters", pathParameters);
			        operationId.addProperty("pathParameterized", pathParameterized);
			        operationId.addProperty("pathProcessed", path);
					
					RequestBody requestBody = oper.getValue().getRequestBody();
					if (requestBody != null &&
					        requestBody.getContent() != null && requestBody.getContent().size() > 0) {
					    operationId.addProperty("contentType", requestBody.getContent().keySet().stream().findFirst().get());
					}
					operationId.addProperty("original", oper.getValue().getOperationId());
					operationId.addProperty("operationIdCamelCase", toCamelCase(operation));
					pathData.add(oper.getKey().name(), operationId);
				}
				operationData.add(entry.getKey(), pathData);
			}
			return operationData;
		}

		protected String toCamelCase(String operationId) {
			JavaCXFServerCodegen javaCXFServerCodegen = new JavaCXFServerCodegen();
			String operationIdToCamelCase = javaCXFServerCodegen.removeNonNameElementToCamelCase(operationId);
			if (StringUtils.isEmpty(operationIdToCamelCase)) {
				throw new RuntimeException("Empty method/operation name (operationId) not allowed");
			} else {
				operationId = org.openapitools.codegen.utils.StringUtils.camelize(javaCXFServerCodegen.sanitizeName(operationIdToCamelCase), CamelizeOption.LOWERCASE_FIRST_LETTER);
				if (isReservedWord(operationId)) {
					String newOperationId = org.openapitools.codegen.utils.StringUtils.camelize("call_" + operationId, CamelizeOption.LOWERCASE_FIRST_LETTER);
					log.warn("{} (reserved word) cannot be used as method name. Renamed to {}", operationId, newOperationId);
					return newOperationId;
				} else {
					if (operationId.matches("^\\d.*")) {
						log.warn(operationId + " (starting with a number) cannot be used as method name. Renamed to " + org.openapitools.codegen.utils.StringUtils.camelize("call_" + operationId), true);
						operationId = org.openapitools.codegen.utils.StringUtils.camelize("call_" + operationId, CamelizeOption.LOWERCASE_FIRST_LETTER);
					}

					return operationId;
				}
			}
		}

		protected boolean isReservedWord(String word) {
			return word != null && this.reservedWords.contains(word.toLowerCase(Locale.ROOT));
		}

		protected void setReservedWordsLowerCase(List<String> words) {
			reservedWords = new HashSet<>();
			for (String word : words) {
				reservedWords.add(word.toLowerCase(Locale.ROOT));
			}
		}

		protected void generateResources(EntaxyResource resource, EntaxyResourceMetadata meta) {
			// '{"@LIFECYCLE":["general"], "*": {"@SKIP": ["deploy+"]}}'
			String instructions = EntaxyProducerUtils.instructions()
					.lifecycle("general")
					.any().skip("deploy+")
					.getInstructionsString();
			
			MetadataSection section = meta.getSection(OpenApiResourceMetadata.SECTION_NAME);
			if (section == null)
				return;
			
			JsonObject metaContent = section.getContent();

			JsonObject object = new JsonObject();
			object.addProperty(EntaxyObject.FIELDS.FACTORY_ID, "service-resources-openapi");
			
			JsonObject properties = new JsonObject();
			properties.add("service", metaContent.deepCopy());
			properties.addProperty("parentResource", resource.getURL());
			
			object.add(EntaxyObject.FIELDS.PROPERTIES, properties);

			
			ProducerResult result = ServiceHelper.getInstance().getEntaxyProducerService()
				.produce(object, instructions);
			
			Map<String, List<Exception>> exceptions = result.getAllExceptions();
			if (!exceptions.isEmpty()) {
				log.error("ERRORS WHILE PROCESSING OPENAPI [generateResources]:");
				for (String key: exceptions.keySet()) {
					for (Exception e: exceptions.get(key))
						log.error("ERROR in command [" + key + "]", e);
				}
			} else {
				// collect generated resources
				List<Generated> generatedResources = result.findResultObject(GeneratedList.class)
						.stream()
						.filter(g -> (
								( g != null )
								&& g.getProperties().containsKey("provider")
								&& g.getProperties().containsKey("location")
								&& g.getProperties().containsKey("scope")))
						.collect(Collectors.toList());
				List<String> resourcesData = new ArrayList<>();
				for (Generated g: generatedResources) {
					String provider = g.getProperties().getOrDefault("provider", "").toString();
					if (!CommonUtils.isValid(provider))
						continue;
					String location = g.getProperties().getOrDefault("location", "").toString();
					if (!CommonUtils.isValid(location))
						continue;
					EntaxyResource.RESOURCE_SCOPE scope = RESOURCE_SCOPE.DESIGN;
					try {
						scope = RESOURCE_SCOPE.valueOf(g.getProperties().getOrDefault("scope", "design").toString().toUpperCase());
					} catch (Exception e) {
						// use default value
					}
					resourcesData.add(provider+":"+location+";"+scope.name());
				}
				
				if (resourcesData.isEmpty())
					return;
				
				JsonArrayBuilder<?> arrayBuilder = JSONUtils.JsonBuilder.create(meta.getSection(EntaxyResourceMetadata.SECTION_RESOURCE.NAME).getContent())
					.array(GENERATED_RESOURCES, false);
				for (String s: resourcesData)
					arrayBuilder.string(s);
				
				meta.update();
			}
			
		}
		
	}
	
	@Override
	public InputStream preProcess(InputStream arg0, EntaxyResource arg1) {
		return null;
	}

	@Override
	public void postProcess(EntaxyResource resource) {
		if (!resource.exists())
			return;
		if (!resource.getName().endsWith("." + YAML_EXTENSION) && 
				!resource.getName().endsWith("." + JSON_EXTENSION))
			return;
		log.info("OpenApi PROCESSOR FOR: [{}]", resource.getURL());
		
		(new OpenApiPostprocessor(resource)).process();
	}

}
