/*-
 * ~~~~~~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.generationmodel;

import java.util.Base64;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Map.Entry;
import java.util.stream.Collectors;

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

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

import ru.entaxy.platform.base.objects.EntaxyObject.FIELDS;
import ru.entaxy.platform.base.objects.EntaxyObject.HEADERS;
import ru.entaxy.platform.base.objects.factory.EntaxyFactory;
import ru.entaxy.platform.base.objects.factory.EntaxyFactory.OutputInfo;
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.ProducerResult;
import ru.entaxy.platform.core.producer.api.ProducerResult.CommandResult;
import ru.entaxy.platform.core.producer.executor.objectmodel.FactoredObject;
import ru.entaxy.platform.core.producer.executor.objectmodel.FactoredObjectRef;

public class GenerateTask extends AbstractObjectTask {

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

    public static final String CONTENT_FIELD = "##_CONTENT";

    public static final String INSTRUCTIONS_FIELD = "##instructions";
    public static final String GENERATION_PROPERTIES_FIELD = "##generation";
    public static final String SKIP_OBJECT_PUBLISHING_FIELD = "##SKIP_OBJECT_PUBLISHING";

    // TODO move to EntaxyFactory
    public static final String HEADER_OBJECT_MAP = "@OBJECT-MAP";

    // TODO move to EntaxyFactory
    public static final String DIRECTIVE_SKIP_PUBLISH = "@SKIP_PUBLISH";

    // TODO move to EntaxyFactory
    public static final String DIRECTIVE_PUBLISH_AS = "@PUBLISH_AS";

    // TODO move to EntaxyFactory
    public static final String DIRECTIVE_SKIP_PROVIDE = "@SKIP_PROVIDE";

    public String factoryId;
    public String factoryType;
    // public String objectId;
    public JsonElement properties;

    public String outputType = "init";
    public String scope = "public";

    public Map<String, Object> generationProperties = new HashMap<>();

    public FactoredObjectRef originRef = null;

    public FactoredObject originObject = null;

    public GenerateTask(FactoredObject factoredObject) {
        super(factoredObject);
        fillFromOrigin();
    }

    @Override
    public boolean execute(ProducerResult currentResult, CommandResult currentCommand,
            EntaxyProducerService producerService, Map<String, Object> instructions) throws Exception {
        if (!CommonUtils.isValid(factoryId))
            throw new IllegalArgumentException("Factory ID is null");
        if (!CommonUtils.isValid(factoryType))
            try {
                EntaxyFactory f = producerService.findFactoryById(factoryId);
                if (f != null) {
                    log.debug("Factory found by id [{}] with type [{}]", f.getId(), f.getType());
                    factoryType = f.getType();
                } else
                    throw new IllegalArgumentException("Factory not found: " + factoryId);
            } catch (Exception e) {
                throw e;
            }
        log.debug("Execute for type [{}] with factoryId [{}]/[{}]", factoryType, factoryId, outputType);
        Map<String, Object> effectiveProperties = createEffectiveProperties(instructions);
        EntaxyFactory factory = producerService.getProducerForType(factoryType).getFactoryById(factoryId);

        // check hierarchy
        if (factory == null) {
            factory = producerService.findFactoryById(factoryId);
            boolean typeChecked = false;
            EntaxyFactory current = factory;
            while (!typeChecked && current != null) {
                typeChecked = current.getType().equals(factoryType);
                if (typeChecked)
                    break;
                current = producerService.findFactoryById(current.getParent());
            }
            if (!typeChecked) {
                log.error("FAILED producing for output [{}]: factory [{}] is not of type [{}]", outputType, factoryId,
                        factoryType);
                throw new IllegalArgumentException(
                        String.format("Factory [%s] is not of type [%s]", factoryId, factoryType));
            }
        }

        this.generatedResult = factory.generate(outputType, scope, effectiveProperties);
        // objectId could be changed by pre/post processor, so we update it
        // TODO move string to const
        String objectId = this.generatedResult.getProperties()
                .getOrDefault(FIELDS.OBJECT_ID, origin.getObjectId()).toString();
        if (!origin.getObjectId().equals(objectId)) {
            log.debug("Changing objectId from [{}] to [{}]", origin.getObjectId(), objectId);
            origin.setObjectId(objectId);
        }

        boolean publishObject = !isSkipPublish(factory, this.outputType, this.scope);

        // check for old-style configuration
        // TODO remove in next releases
        if (publishObject)
            if (effectiveProperties.containsKey(SKIP_OBJECT_PUBLISHING_FIELD)) {
                try {
                    publishObject = !((Boolean) effectiveProperties.get(SKIP_OBJECT_PUBLISHING_FIELD));
                } catch (Exception e) {
                    // NOOP
                }
            }

        // we prepare publish data for all objects
        JsonObject publishData = generationModel.publishDataStorage.generatePublishData(origin, factory, outputType);


        GeneratedHeaders headers = GeneratedHeaders.getHeaders(this.generatedResult.getProperties());
        headers.set(HEADERS.ENTAXY_GENERATED, "true");

        if (publishObject) {

            String publishedObjectId = origin.getObjectId();

            String publishAsField = getPublishAs(factory, this.outputType, this.scope);
            if (CommonUtils.isValid(publishAsField)) {
                if (effectiveProperties.containsKey(publishAsField))
                    publishedObjectId = effectiveProperties.getOrDefault(publishAsField, publishedObjectId).toString();
            }

            if (!publishedObjectId.equals(origin.getObjectId()))
                headers.append(HEADER_OBJECT_MAP,
                        origin.getObjectId() + ":" + factoryType + "::" + publishedObjectId + ":" + factoryType);

            // set @SKIP_PROVIDE property
            if (isSkipProvide(factory, this.outputType, this.scope)) {
                this.generatedResult.getProperties().put(DIRECTIVE_SKIP_PROVIDE, true);
            }



            headers.set(HEADERS.MAIN_OBJECT, publishedObjectId + ":" + factoryType + ":" + scope);
            headers.append(HEADERS.GENERATED_OBJECTS, publishedObjectId + ":" + factoryType + ":" + scope);
            // headers.set(GeneratedHeaders.HEADER_MAIN_OBJECT_TYPE, factoryType);
            // headers.set(GeneratedHeaders.HEADER_MAIN_OBJECT_FACTORY_ID, factoryId);
            // headers.append(GeneratedHeaders.HEADER_OBJECT_ID, origin.getObjectId());
            // headers.append(GeneratedHeaders.HEADER_OBJECT_TYPE, factoryType);
            headers.append(HEADERS.GENERATED_OBJECTS_CONFIG, Base64.getEncoder().encodeToString(
                    publishData.toString().getBytes()));
        }
        this.generationProperties.clear();
        this.generationProperties.putAll(effectiveProperties);
        this.generatedResult.getProperties().put(GENERATION_PROPERTIES_FIELD,
                new HashMap<String, Object>(this.generationProperties));
        return true;
    }

    @Deprecated
    protected JsonObject getFilteredObjectData(JsonObject originData, EntaxyFactory factory, String outputType) {
        JsonObject result = getFilteredObject(originData, factory, outputType);
        if (result == null)
            result = new JsonObject();
        return result;
    }

    @Deprecated
    protected JsonObject getFilteredObject(JsonObject originObject, EntaxyFactory factory, String outputType) {
        JsonObject result = new JsonObject();

        Map<String, JsonObject> factoryFields = factory.getOutputByType(outputType).getFields().stream()
                .collect(Collectors.toMap(fi -> fi.getName(), fi -> fi.getJsonOrigin()));

        for (Entry<String, JsonElement> entry : originObject.entrySet()) {
            String name = entry.getKey();
            if (FactoredObject.EMBEDDED_FIELD.equals(name) || FactoredObject.IGNORE_SECTION.equals(name))
                continue;

            boolean skip = false;

            // check @INTERNAL directive via factory data
            JsonObject joField = factoryFields.getOrDefault(name, new JsonObject());
            if (joField.has(EntaxyFactory.CONFIGURATION.DIRECTIVES.INTERNAL)) {
                try {
                    skip = joField.get(EntaxyFactory.CONFIGURATION.DIRECTIVES.INTERNAL).getAsBoolean();
                } catch (Exception ignore) {
                    // NOOP
                }
            }
            if (skip)
                continue;

            JsonElement je = entry.getValue();
            if (je.isJsonObject()) {
                JsonObject jo = je.getAsJsonObject();

                // check @INTERNAL directive via object data

                if (jo.has(EntaxyFactory.CONFIGURATION.DIRECTIVES.INTERNAL)) {
                    try {
                        skip = jo.get(EntaxyFactory.CONFIGURATION.DIRECTIVES.INTERNAL).getAsBoolean();
                    } catch (Exception e) {
                        // NOOP
                    }
                    if (skip)
                        continue;
                }
                JsonObject filtered = getFilteredObject(jo, factory, outputType);
                if (filtered != null)
                    result.add(name, filtered);
            } else {
                result.add(name, je);
            }
        }
        return result;
    }

    protected String getPublishAs(EntaxyFactory factory, String outputType, String scope) {
        OutputInfo info = factory.getOutputByType(outputType);
        if (info == null)
            return null;
        Map<String, Object> config = info.getConfig();
        if (config == null)
            return null;
        if (config.containsKey(DIRECTIVE_PUBLISH_AS)) {
            try {
                // check scopes
                Map<String, Object> data = (Map<String, Object>) config.get(DIRECTIVE_PUBLISH_AS);
                if (data.containsKey(scope)) {
                    return data.get(scope).toString();
                } else {
                    if (data.containsKey("*")) {
                        return data.get("*").toString();
                    }
                }
            } catch (Exception e) {
                return null;
            }
        }
        return null;
    }

    protected boolean isSkipPublish(EntaxyFactory factory, String outputType, String scope) {
        OutputInfo info = factory.getOutputByType(outputType);
        if (info == null)
            return false;
        Map<String, Object> config = info.getConfig();
        if (config == null)
            return false;
        if (config.containsKey(DIRECTIVE_SKIP_PUBLISH)) {
            try {
                // check scopes
                Map<String, Object> data = (Map<String, Object>) config.get(DIRECTIVE_SKIP_PUBLISH);
                if (data.containsKey(EntaxyFactory.CONFIGURATION.OUTPUTS.ATTRIBUTES.SCOPES)) {
                    List<String> scopes =
                            (List<String>) data.get(EntaxyFactory.CONFIGURATION.OUTPUTS.ATTRIBUTES.SCOPES);
                    return scopes.contains(scope);
                } else {
                    return true;
                }
            } catch (Exception e) {
                return true;
            }
        }
        return false;
    }

    protected boolean isSkipProvide(EntaxyFactory factory, String outputType, String scope) {
        OutputInfo info = factory.getOutputByType(outputType);
        if (info == null)
            return false;
        Map<String, Object> config = info.getConfig();
        if (config == null)
            return false;
        if (config.containsKey(DIRECTIVE_SKIP_PROVIDE)) {
            try {
                // check scopes
                Map<String, Object> data = (Map<String, Object>) config.get(DIRECTIVE_SKIP_PROVIDE);
                if (data.containsKey(EntaxyFactory.CONFIGURATION.OUTPUTS.ATTRIBUTES.SCOPES)) {
                    List<String> scopes =
                            (List<String>) data.get(EntaxyFactory.CONFIGURATION.OUTPUTS.ATTRIBUTES.SCOPES);
                    return scopes.contains(scope);
                } else {
                    return true;
                }
            } catch (Exception e) {
                return true;
            }
        }
        return false;
    }

    protected Map<String, Object> createEffectiveProperties(Map<String, Object> instructions) {
        Map<String, Object> result = new HashMap<>();
        result.put(INSTRUCTIONS_FIELD, instructions);
        JsonElement effectiveProperties = this.properties.deepCopy();
        if (effectiveProperties.isJsonObject()) {
            JsonObject jobj = effectiveProperties.getAsJsonObject();
            for (Entry<String, JsonElement> entry : origin.origin.entrySet()) {
                /*
                 * if (entry.getKey().equals(FIELDS.PROPERTIES)) continue;
                 */
                if (jobj.has(entry.getKey()))
                    continue;
                jobj.add(entry.getKey(), entry.getValue());
            }
            result.putAll(JSONUtils.element2map(effectiveProperties));
        }
        return result;
    }

    public void fillFromOrigin() {
        this.factoryId = origin.factoryId;
        // this.objectId = origin.getObjectId();
        this.factoryType = origin.getObjectType();
        this.scope = origin.scope;
        this.properties = origin.origin.get(FIELDS.PROPERTIES);

        if (CommonUtils.isValid(origin.getOutputType()))
            this.outputType = origin.getOutputType();

    }

    public void updateObjectId(String objectId) {
        // this.objectId = objectId;
        this.origin.setObjectId(objectId);
    }

    @Override
    public String getInfo() {
        return "GENERATE " + "[" + origin.getObjectId() + "] :: " + factoryId + "/" + outputType;
    }
}
