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

import java.util.HashMap;
import java.util.Map;
import java.util.Optional;

import org.osgi.service.component.ComponentContext;
import org.osgi.service.component.annotations.Activate;
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.JsonElement;
import com.google.gson.JsonObject;
import com.google.gson.JsonPrimitive;

import ru.entaxy.esb.platform.runtime.base.connecting.generator.Generated;
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.OutputInfo;
import ru.entaxy.platform.base.objects.factory.exceptions.FactoryNotFoundException;
import ru.entaxy.platform.base.support.CommonUtils;
import ru.entaxy.platform.base.support.JSONUtils;
import ru.entaxy.platform.config.runtime.ObjectConfig;
import ru.entaxy.platform.config.runtime.ObjectConfigHelper;
import ru.entaxy.platform.config.runtime.ObjectConfig.FactoryOutputConfiguration;
import ru.entaxy.platform.core.producer.api.EntaxyProducerService;
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.objectmodel.FactoredObject;
import ru.entaxy.platform.core.producer.executor.objectmodel.FactoredObjectProxy;
import ru.entaxy.platform.core.producer.executor.objectmodel.FactoredObjectRef;
import ru.entaxy.platform.core.producer.executor.objectmodel.ObjectModel;

// @Component(service = ProducingCommandExecutor.class, immediate = true)
// @CommandExecutor(id = "add-config", predecessors = {"enrich"}, descendants = {"validate"})
public class AddConfigCommand2 extends AbstractCommandExecutor implements ProducingCommandExecutor {

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

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

    public AddConfigCommand2() {
        super(null);
    }

    protected AddConfigCommand2(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 {

        printOutput("\n\n\t== ADD-CONFIG == \n\n");

        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;

            log.debug("OBJECT :: {}/{}", fo.getObjectId(), fo.getObjectType());

            String factoryId = fo.factoryId;

            EntaxyFactory factory = entaxyProducerService.findFactoryById(factoryId);
            if (factory == null)
                throw new FactoryNotFoundException(factoryId);

            String output = fo.getOutputType();
            log.debug("OUTPUT :: {}", output);

            OutputInfo oi = CommonUtils.isValid(output) ? factory.getOutputByType(output) : factory.getDefaultOutput();
            Map<String, Object> config = oi.getConfig();
            if ((config == null) || (config.isEmpty()))
                continue;

            String val = config.getOrDefault(ObjectConfig.CONFIGURABLE_ATTRIBUTE_NAME, "false").toString();
            Boolean configurable = Boolean.valueOf(val);
            if (!configurable)
                continue;

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

            if (objectProperties.has(ObjectConfig.CONFIG_FIELD_NAME))
                continue;

            @SuppressWarnings("serial")
            Generated g = this.entaxyProducerService.findFactoryById("common-config")
                    .generate("config-field", "private", new HashMap<String, Object>() {
                        {
                            put(FIELDS.FACTORY_ID, "common-config");
                            put(ObjectConfig.TARGET_FACTORY_FIELD_NAME, fo.factoryId);
                            put(ObjectConfig.TARGET_TYPE_FIELD_NAME, fo.getObjectType());
                            put(ObjectConfig.TARGET_ID_FIELD_NAME, fo.getObjectId());
                            put(ObjectConfig.TARGET_OUTPUT_FIELD_NAME, fo.getOutputType());
                        }
                    });

            objectProperties.add(ObjectConfig.CONFIG_FIELD_NAME, JSONUtils.getJsonRootObject(g.getObject().toString()));
            objectModel.setDirty();

        }

        for (FactoredObjectRef ref : objectModel.refs) {
            // process external refs and replace ref to object with ref to config if
            // - ref is not marked 'directValueOnly'
            // - the referenced object has config
            // - the referenced field is configurable

            // if (!ref.isExternal())
            // continue;

            log.debug("REF :: " + ref.relativePath);

            if (ObjectConfigHelper.getInstance().isDirectValueOnly(ref))
                continue;

            String objectId = ref.getTargetId();
            String objectType = ref.getTargetType();

            log.debug("TO :: " + objectId + ":" + objectType);
            log.debug("DEFINED IN :: " + ref.definedIn.getObjectId());

            String refField = ref.getOrDefault(FIELDS.REF_FIELD, FIELDS.OBJECT_ID).toString();
            if (FIELDS.OBJECT_ID.equals(refField))
                // objectId can't be configured
                continue;

            // skip if it's already config
            if (ObjectConfig.CONFIG_OBJECT_TYPE.equals(objectType))
                continue;

            String configurationPid = ObjectConfig.getConfigurationPid(objectId, objectType);

            String factoryId = null;

            JsonElement newTargetId = null;

            if (ref.isExternal()) {

                EntaxyObject theObject = ObjectConfigHelper.getInstance().findObject(objectId, objectType);
                if (theObject == null)
                    // referenced object not found
                    continue;

                factoryId = theObject.getFactoryId();

                EntaxyObject configObject = ObjectConfigHelper.getInstance()
                        .findObject(configurationPid, ObjectConfig.CONFIG_OBJECT_TYPE);
                if (configObject == null)
                    // config for referenced object not found
                    continue;

                newTargetId = new JsonPrimitive(configurationPid);

            } else {

                log.debug("EXTERNAL :: false");

                Optional<FactoredObject> theObjectOpt = objectModel.objects.stream()
                        .filter(obj -> objectId.equals(obj.getObjectId())).findFirst();
                if (!theObjectOpt.isPresent())
                    continue;

                FactoredObject theObject = theObjectOpt.get();

                log.debug("TARGET OBJECT :: found");

                factoryId = theObject.factoryId;

                Optional<FactoredObjectRef> configRefOpt = objectModel.refs.stream()
                        .filter(r -> r.definedIn.equals(theObject))
                        .filter(r -> r.relativePath.endsWith(ObjectConfig.CONFIG_FIELD_NAME))
                        .findFirst();

                if (!configRefOpt.isPresent())
                    continue;

                log.debug("REF TO CONFIG :: found; TARGET :: " + configRefOpt.get().getTargetId());

                Optional<FactoredObject> configObjectOpt = objectModel.objects.stream()
                        .filter(obj -> obj.getObjectId().equals(configRefOpt.get().getTargetId()))
                        .findFirst();
                if (!configObjectOpt.isPresent())
                    continue;

                log.debug("CONFIG OBJECT :: found");

                newTargetId = new JsonPrimitive(configObjectOpt.get().getObjectId());
            }


            EntaxyFactory entaxyFactory = entaxyProducerService.findFactoryById(factoryId);
            if (entaxyFactory == null)
                // factory for referenced object not found
                continue;

            FactoryOutputConfiguration conf = FactoryOutputConfiguration.read(entaxyFactory,
                    EntaxyFactory.CONFIGURATION.OUTPUTS.OUTPUT_TYPE_INIT);
            if (!conf.isConfigurable)
                continue;


            if (!ObjectConfig.isConfigurable(entaxyFactory, EntaxyFactory.CONFIGURATION.OUTPUTS.OUTPUT_TYPE_INIT,
                    refField, conf.defaultFieldsConfigurable, conf.configurations))
                // field is not configurable
                continue;

            JSONUtils.replaceValue(ref.origin, FIELDS.OBJECT_TYPE, new JsonPrimitive(ObjectConfig.CONFIG_OBJECT_TYPE));
            JSONUtils.replaceValue(ref.origin, FactoredObjectRef.TARGET_TYPE_FIELD,
                    new JsonPrimitive(ObjectConfig.CONFIG_OBJECT_TYPE));
            JSONUtils.replaceValue(ref.origin, FactoredObjectRef.TARGET_ID_FIELD, newTargetId);

            // and we must generate ref to config to merge to main object
            JSONUtils.replaceValue(ref.origin, FIELDS.IS_REF_BY_VALUE_ONLY, new JsonPrimitive(false));

            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;
    }

}
