/*-
 * ~~~~~~licensing~~~~~~
 * object-producing-resources-support
 * ==========
 * 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.resources;

import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Map.Entry;
import java.util.UUID;

import org.osgi.service.component.ComponentContext;
import org.osgi.service.component.annotations.Activate;
import org.osgi.service.component.annotations.Component;
import org.osgi.service.component.annotations.Reference;
import org.osgi.service.component.annotations.ReferenceCardinality;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import com.google.gson.Gson;
import com.google.gson.GsonBuilder;
import com.google.gson.JsonElement;
import com.google.gson.JsonObject;
import com.google.gson.JsonPrimitive;

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.EntaxyResourceService;
import ru.entaxy.platform.base.objects.EntaxyObject;
import ru.entaxy.platform.base.objects.EntaxyObject.FIELDS;
import ru.entaxy.platform.base.objects.factory.EntaxyFactory;
import ru.entaxy.platform.base.objects.factory.EntaxyFactory.CONFIGURATION.FACTORY;
import ru.entaxy.platform.base.objects.factory.EntaxyFactory.CONFIGURATION.OUTPUTS;
import ru.entaxy.platform.base.objects.factory.EntaxyFactory.FieldInfo;
import ru.entaxy.platform.base.support.CommonUtils;
import ru.entaxy.platform.base.support.JSONUtils;
import ru.entaxy.platform.core.producer.api.EntaxyProducerService;
import ru.entaxy.platform.core.producer.api.EntaxyProducerUtils;
import ru.entaxy.platform.core.producer.api.ExecutionPlan.ExecutionPlanUpdate;
import ru.entaxy.platform.core.producer.api.ProducerResult;
import ru.entaxy.platform.core.producer.api.ProducerResult.CommandResult;
import ru.entaxy.platform.core.producer.api.ProducingCommandExecutor;
import ru.entaxy.platform.core.producer.executor.AbstractCommandExecutor;
import ru.entaxy.platform.core.producer.executor.CommandExecutor;
import ru.entaxy.platform.core.producer.executor.objectmodel.Calculation;
import ru.entaxy.platform.core.producer.executor.objectmodel.FactoredObject;
import ru.entaxy.platform.core.producer.executor.objectmodel.FactoredObjectProxy;
import ru.entaxy.platform.core.producer.executor.objectmodel.ObjectModel;
import ru.entaxy.platform.core.support.runtime.objects.resources.ObjectResourcesProvider;

@Component(service = ProducingCommandExecutor.class, immediate = true)
@CommandExecutor(id = "process-resources", predecessors = {"enrich"}, descendants = {"validate"})
public class ProcessResourcesCommand extends AbstractCommandExecutor implements ProducingCommandExecutor {

    private static final Logger log = LoggerFactory.getLogger(ProcessResourcesCommand.class);

    private static final Gson GSON = (new GsonBuilder()).setPrettyPrinting().create();

    @Reference(cardinality = ReferenceCardinality.MANDATORY)
    EntaxyProducerService entaxyProducerServiceLocal;

    @Reference(cardinality = ReferenceCardinality.MANDATORY)
    EntaxyResourceService entaxyResourceService;

    @Reference(cardinality = ReferenceCardinality.MANDATORY)
    ResourceDataProcessor resourceDataProcessor;

    public ProcessResourcesCommand() {
        super(null);
    }

    protected ProcessResourcesCommand(EntaxyProducerService entaxyProducerService) {
        super(entaxyProducerService);
    }

    @Activate
    public void activate(ComponentContext componentContext) {
        this.entaxyProducerService = this.entaxyProducerServiceLocal;
        this.entaxyProducerService.registerCommand(this);
    }

    @Override
    protected boolean doExecute(ProducerResult currentResult, CommandResult commandResult,
            Map<String, Object> instructions) throws Exception {

        ObjectModel objectModel = currentResult.findResultObject(ObjectModel.class);
        objectModel.startTracking();

        JsonObject incomingJson = objectModel.getJsonCurrent().deepCopy();

        for (FactoredObject fo : objectModel.objects) {

            // skip proxies
            if (fo instanceof FactoredObjectProxy)
                continue;

            String factoryId = fo.factoryId;

            EntaxyFactory factory = entaxyProducerService.findFactoryById(factoryId);

            JsonObject objectOrigin = fo.origin;


            if (JSONUtils.setValue(objectOrigin, FIELDS.PROPERTIES, new JsonObject(), true)) {
                printOutput("[DIRTY] CREATED 'properties' for [" + fo.getObjectId() + "/" + fo.getObjectType() + "]");
                objectModel.setDirty();
            }

            JsonObject objectProperties = objectOrigin.get(FIELDS.PROPERTIES).getAsJsonObject();

            String outputType = CommonUtils.isValid(fo.getOutputType())
                    ? fo.getOutputType()
                    : factory.getDefaultOutput() != null
                            ? factory.getDefaultOutput().getType()
                            : OUTPUTS.OUTPUT_TYPE_INIT;

            List<FieldInfo> factoryFields = factory.getFields(outputType);

            log.debug("\n FIELDS FOR " + factory.getId() + "/" + outputType + ":" + factoryFields.size());

            // if the value is JsonObject
            // and represents some resource or resource metadata
            // we first resolve it to actual value
            JsonObject newProperties = new JsonObject();
            for (Entry<String, JsonElement> entry : objectProperties.entrySet()) {

                if (entry.getValue().isJsonObject())
                    if (entry.getValue().getAsJsonObject().has(ResourceProducer.DIRECTIVES.RESOURCE)) {
                        JsonElement jsonDescriptor =
                                entry.getValue().getAsJsonObject().get(ResourceProducer.DIRECTIVES.RESOURCE);
                        ResourceDescriptor resourceDescriptor = ResourceDescriptor.fromJson(jsonDescriptor);
                        if (resourceDescriptor == null) {
                            log.warn("ResourceDescriptor is null for field [{}]:\n {}", entry.getKey(),
                                    jsonDescriptor.toString());
                            continue;
                        }
                        Object newData = resourceDataProcessor.getResourceDataFromDescriptor(resourceDescriptor);
                        if (newData == null) {
                            // check if the descriptor it to be calculated
                            if (jsonDescriptor.isJsonObject()) {
                                JsonElement calc = JSONUtils.findElementExt(jsonDescriptor.getAsJsonObject(),
                                        "?." + EntaxyFactory.CONFIGURATION.DIRECTIVES.CALCULATED);
                                if (calc == null)
                                    // try to find old-style merker
                                    calc = JSONUtils.findElementExt(jsonDescriptor.getAsJsonObject(), "?.isCalculated");
                                if (calc != null) {
                                    log.warn("ResourceDescriptor for field [{}] is to be calculated; continue.",
                                            entry.getKey());
                                    continue;
                                }
                            }
                            log.warn("Resource data is null for field [{}]: [{}]", entry.getKey(),
                                    resourceDescriptor.getResourceUrl());
                        }
                        JsonElement newValue = GSON.toJsonTree(newData);

                        newProperties.add(entry.getKey(), newValue);
                    } else if (entry.getValue().getAsJsonObject().has(ResourceProducer.DIRECTIVES.RESOURCE_META)) {
                        JsonElement jsonDescriptor =
                                entry.getValue().getAsJsonObject().get(ResourceProducer.DIRECTIVES.RESOURCE_META);
                        ResourceDescriptor resourceDescriptor = ResourceDescriptor.fromJson(jsonDescriptor);
                        if (resourceDescriptor == null)
                            continue;
                        resourceDescriptor.returnResource();
                        EntaxyResource resource = (EntaxyResource) resourceDataProcessor
                                .getResourceDataFromDescriptor(resourceDescriptor);
                        JsonElement newValue;
                        if ((resource == null) || !resource.hasMetadata()) {
                            newValue = new JsonObject();
                        } else {
                            EntaxyResourceMetadata metadata = resource.getMetadata();
                            JsonObject metaJson = metadata.getAsJson();
                            String path = "";
                            if (jsonDescriptor.isJsonObject()) {
                                JsonObject obj = jsonDescriptor.getAsJsonObject();
                                if (obj.has("path")) {
                                    JsonElement je = obj.get("path");
                                    if ((je != null) && je.isJsonPrimitive())
                                        path = je.getAsString();
                                }
                            }
                            if (CommonUtils.isValid(path)) {
                                newValue = JSONUtils.findElementExt(metaJson, path);
                            } else {
                                newValue = metaJson;
                            }
                        }
                        newProperties.add(entry.getKey(), newValue);
                    }
            }
            for (Entry<String, JsonElement> entry : newProperties.entrySet()) {
                if (JSONUtils.setValue(objectProperties, entry.getKey(), entry.getValue().deepCopy(), false)) {
                    printOutput("[DIRTY] UPDATED " + entry.getKey() + " for [" + fo.getObjectId() + "/"
                            + fo.getObjectType() + "]");
                    objectModel.setDirty();
                }

            }

            Map<String, Object> resourceContext = null;

            // for the field defined as a resource
            // we must make an object
            for (FieldInfo fi : factoryFields) {
                log.debug("\n--> FIELD :: " + fi.getName());
                log.debug("\n" + objectProperties.toString());
                JsonObject jsonFi = fi.getJsonOrigin();
                if (!objectProperties.has(fi.getName()))
                    continue;
                log.debug("\nORIGIN:\n" + fi.getJsonOrigin().toString());
                if (jsonFi.has(ResourceProducer.DIRECTIVES.RESOURCE)) {
                    log.debug("\n--> FIELD :: " + fi.getName() + " IS A RESOURCE");
                    ResourceDescriptor descriptor =
                            ResourceDescriptor.fromJson(jsonFi.get(ResourceProducer.DIRECTIVES.RESOURCE));
                    if (descriptor == null)
                        continue;
                    JsonElement currentValue = objectProperties.get(fi.getName());
                    boolean isRef = false;
                    if (currentValue.isJsonObject()) {
                        JsonObject currentObject = currentValue.getAsJsonObject();
                        if (currentObject.has(ResourceDataProcessor.PROP_RESOURCE_REF)) {
                            continue;
                        }
                        if (currentObject.has("isRef")) {
                            isRef = true;
                            // continue;
                        }
                    }
                    JsonElement newValueElement = jsonFi.get(ResourceProducer.DIRECTIVES.RESOURCE);
                    if (!newValueElement.isJsonObject())
                        continue;
                    JsonObject properties = newValueElement.getAsJsonObject().deepCopy();

                    // add default provider if not set
                    if (!properties.has(ResourceDataProcessor.PROP_PROVIDER)) {
                        properties.addProperty(ResourceDataProcessor.PROP_PROVIDER,
                                ObjectResourcesProvider.PROVIDER_NAME);
                    }

                    // add default location if not set
                    if (!properties.has(ResourceDataProcessor.PROP_LOCATION)) {
                        JsonObject locationData = new JsonObject();
                        locationData.addProperty(Calculation.FIELD_IS_CALCULATED, true);
                        locationData.addProperty("lazy", true);
                        locationData.addProperty("expression", "${#OWNER#.#FACTORY#.factory.type}.${#OWNER#.objectId}/"
                                + fi.getName() + "." + fi.getType().split(":")[0]);
                        properties.add(ResourceDataProcessor.PROP_LOCATION, locationData);
                    }

                    if (isRef) {
                        // properties.add("contentRef", currentValue);
                        JSONUtils.replaceValue(currentValue.getAsJsonObject(), EntaxyObject.FIELDS.IS_REF_BY_VALUE_ONLY,
                                new JsonPrimitive(true));
                        JSONUtils.replaceValue(currentValue.getAsJsonObject(), EntaxyObject.FIELDS.REF_FIELD,
                                new JsonPrimitive("##_CONTENT"));
                        properties.add("content", currentValue);

                    } else {
                        properties.add("content", currentValue);
                    }
                    properties.addProperty("type", fi.getType());

                    JsonObject metadata = new JsonObject();
                    if (properties.has(ResourceDataProcessor.PROP_META)) {
                        JsonElement je = properties.get(ResourceDataProcessor.PROP_META);
                        if (je.isJsonObject())
                            metadata = je.getAsJsonObject();
                    }

                    // consider all resources made via factory as DESIGN-scoped
                    RESOURCE_SCOPE resourceScope = RESOURCE_SCOPE.DESIGN;
                    if (properties.has(ResourceDataProcessor.PROP_SCOPE)) {
                        try {
                            resourceScope = RESOURCE_SCOPE.valueOf(
                                    properties.get(ResourceDataProcessor.PROP_SCOPE).getAsString().toUpperCase());
                        } catch (Exception e) {
                            // will use default
                        }
                    }

                    // add context
                    if (resourceContext == null) {
                        resourceContext = createResourceContextOfObject(fo, factory);

                        printOutput("\t CREATED CONTEXT::");
                        printOutput(resourceContext.toString());
                        printOutput("\t /CREATED CONTEXT");

                    }
                    Map<String, Object> fieldContext = new HashMap<>(resourceContext);
                    fieldContext.put("context.fieldName", fi.getName());
                    properties.remove(ResourceProducer.PROP_RESOURCE_CONTEXT);
                    properties.add(ResourceProducer.PROP_RESOURCE_CONTEXT,
                            GSON.toJsonTree(fieldContext));


                    // JSONUtils.setValue(metadata, "resource/owned",new JsonPrimitive(true),
                    // false);
                    JSONUtils.JsonBuilder.create(metadata)
                            .object(EntaxyResourceMetadata.SECTION_RESOURCE.NAME, false)
                            .bool("owned", true)
                            .object("owner")
                            .object(EntaxyFactory.CONFIGURATION.DIRECTIVES.CALCULATED)
                            .bool("lazy", false)
                            .string("expression", "${#OWNER#.objectId}:${#OWNER#.#FACTORY#.factory.type}")
                            .up()
                            .up()
                            .string("scope", resourceScope.name());

                    properties.remove(ResourceDataProcessor.PROP_META);
                    properties.add(ResourceDataProcessor.PROP_META, metadata);

                    String newId = "GEN-" + UUID.randomUUID().toString();

                    JsonObject newValue = new JsonObject();
                    newValue.addProperty(EntaxyObject.FIELDS.OBJECT_ID, newId);
                    newValue.addProperty(FACTORY.TYPE, "entaxy.resource");
                    newValue.addProperty(EntaxyObject.FIELDS.FACTORY_ID, "entaxy-resource");
                    // we must explicitly set scope to 'public' for not to be inherited from owner
                    newValue.addProperty(EntaxyObject.FIELDS.SCOPE, "public");

                    JsonObject refConfig = new JsonObject();

                    if (properties.has("resourceRefConfig")) {
                        JsonElement je = properties.get("resourceRefConfig");
                        if (je.isJsonObject()) {
                            refConfig = je.getAsJsonObject().deepCopy();
                        }
                    }

                    if (isRef && currentValue.getAsJsonObject().has(EntaxyObject.FIELDS.IS_REF_BY_VALUE_ONLY)) {
                        refConfig.add(EntaxyObject.FIELDS.IS_REF_BY_VALUE_ONLY,
                                currentValue.getAsJsonObject().get(EntaxyObject.FIELDS.IS_REF_BY_VALUE_ONLY));
                    }
                    if (!refConfig.has(EntaxyObject.FIELDS.REF_FIELD))
                        refConfig.addProperty(EntaxyObject.FIELDS.REF_FIELD, ResourceProducer.PROP_RESOURCE_REFERENCE);
                    if (!refConfig.has("generated.targetType"))
                        refConfig.addProperty("generated.targetType", Generated.GENERATED_TYPE_BLUEPRINT_FRAGMENT);
                    refConfig.addProperty(ResourceDataProcessor.PROP_RESOURCE_REF, true);
                    newValue.add("refConfig", refConfig);

                    JSONUtils.JsonBuilder.create(properties)
                            .object("__publishResourceValue")
                            .object(EntaxyFactory.CONFIGURATION.DIRECTIVES.CALCULATED)
                            .bool("lazy", false)
                            .bool("allowObjects", false)
                            .string("resultType", "string")
                            .string("expression",
                                    "${properties.provider}:${properties.location};" + resourceScope.name());

                    // newValue.addProperty("__skip", true);
                    newValue.add(FIELDS.PROPERTIES, properties);
                    if (jsonFi.has("config") && jsonFi.get("config").isJsonObject()) {
                        JsonObject configObject = jsonFi.get("config").getAsJsonObject();
                        for (Entry<String, JsonElement> entry : configObject.entrySet()) {
                            if (!newValue.has(entry.getKey()))
                                newValue.add(entry.getKey(), entry.getValue().deepCopy());
                        }
                    }

                    JSONUtils.setValue(objectProperties, fi.getName(), newValue, false);

                    // add resource to published
                    JSONUtils.JsonBuilder.create(objectProperties)
                            .object(EntaxyObject.FIELDS.FIELDS_TO_PUBLISH, false)
                            .array("resources", false)
                            .object()
                            .bool(EntaxyObject.FIELDS.IS_REF, true)
                            .bool(EntaxyObject.FIELDS.IS_REF_BY_VALUE_ONLY, true)
                            .string(EntaxyObject.FIELDS.REF_FIELD, "__publishResourceValue")
                            .string("targetId", newId);
                    /*
                    	.object(EntaxyFactory.CONFIGURATION.DIRECTIVES.CALCULATED)
                    		.bool("lazy", false)
                    		.string("expression"
                    			, properties.get(ResourceDataProcessor.PROP_PROVIDER).getAsString()
                    			+ ":${#FACTORY#.factory.type}.${objectId}/" + fi.getName() + "." + fi.getType().split(":")[0]
                    			+ ";" + resourceScope.name());
                    */
                    objectModel.setDirty();
                }
            }

        }

        if (objectModel.stopTracking()) {

            // remove ##embedded
            for (FactoredObject fo : objectModel.objects)
                fo.origin.remove(FactoredObject.EMBEDDED_FIELD);


            commandResult.planUpdate = ExecutionPlanUpdate.create()
                    // .updateInstructions().target("enrich").value("skip", true).complete()
                    .reset().target("analyze").complete();
        }

        JsonObject outgoingJson = objectModel.getJsonCurrent();

        printOutput("\n== INCOMING JSON ==\n");
        printOutput(incomingJson.toString());
        printOutput("\n== OUTGOING JSON ==\n");
        printOutput(outgoingJson.toString());

        commandResult.resultObject(outgoingJson);

        return true;
    }

    protected Map<String, Object> createResourceContextOfObject(FactoredObject fo, EntaxyFactory factory) {
        Map<String, Object> result = new HashMap<>();
        addDataToContext(result, fo, "context.");
        addDataToContext(result, EntaxyProducerUtils.getFactoryEssence(factory), "context.factory.");
        Object properties = fo.get(FIELDS.PROPERTIES);
        if ((properties != null) && (properties instanceof Map))
            addDataToContext(result, (Map<String, Object>) properties, "context.properties.");
        return result;
    }

    protected void addDataToContext(Map<String, Object> target, Map<String, Object> data, String prefix) {
        for (Map.Entry<String, Object> entry : data.entrySet()) {
            if ((entry.getValue() instanceof Map) || (entry.getValue() instanceof List))
                continue;
            target.put(prefix + entry.getKey(), entry.getValue());
        }
    }

}
