/*-
 * ~~~~~~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.base.objects.factory.impl;

import java.lang.reflect.InvocationTargetException;
import java.util.Collections;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Map.Entry;
import java.util.Set;
import java.util.stream.Collectors;

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

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

import ru.entaxy.esb.platform.runtime.base.connecting.generator.Generated;
import ru.entaxy.platform.base.objects.factory.EntaxyFactory;
import ru.entaxy.platform.base.objects.factory.EntaxyFactoryException;
import ru.entaxy.platform.base.objects.factory.EntaxyFactoryUtils;
import ru.entaxy.platform.base.objects.factory.configuration.AbstractElement;
import ru.entaxy.platform.base.objects.factory.configuration.FactoryElement;
import ru.entaxy.platform.base.objects.factory.configuration.FieldsElement;
import ru.entaxy.platform.base.objects.factory.configuration.OutputElement;
import ru.entaxy.platform.base.objects.factory.configuration.OutputsElement;
import ru.entaxy.platform.base.objects.factory.exceptions.OutputNotDefinedException;
import ru.entaxy.platform.base.objects.factory.exceptions.ScopeNotSupportedException;
import ru.entaxy.platform.base.support.CommonUtils;
import ru.entaxy.platform.base.support.JSONUtils;
import ru.entaxy.platform.base.support.osgi.OSGIUtils;

public class DefaultFactory implements EntaxyFactory {

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

    protected Map<String, Class<? extends AbstractElement<?>>> elementClasses = new HashMap<>();

    protected String factoryId = "";

    protected String factoryType = "";

    protected String description;

    protected String shortName;

    protected String displayName;

    protected String label;

    protected String category;

    protected String parent;

    protected boolean isAbstract;

    protected boolean isInternal;

    protected boolean isDeprecated;

    protected FieldsElement fields = null;

    protected OutputsElement outputs = new OutputsElement();

    protected Map<String, Object> factoryData = new HashMap<>();

    protected GenerationHelper helper = null;

    protected JsonObject originJson;

    public DefaultFactory() {
        super();
        elementClasses.put(FactoryElement.ELEMENT_NAME, FactoryElement.class);
        elementClasses.put(OutputsElement.ELEMENT_NAME, OutputsElement.class);
        elementClasses.put(FieldsElement.ELEMENT_NAME, FieldsElement.class);
    }

    protected GenerationHelper getHelper() {
        if (this.helper == null) {
            try {
                helper = OSGIUtils.services().ofClass(GenerationHelper.class).waitService(5000).get();
            } catch (Exception e) {
                // TODO Auto-generated catch block
                log.error("Error getting GenerationHelper", e);
            }
        }
        return this.helper;
    }

    public void configure(String configuration) {

        log.debug("CONFIGURE WITH: " + configuration);

        JsonElement je = (new JsonParser()).parse(configuration);
        JsonObject root = je.getAsJsonObject();

        this.configure(root);
    }

    public void configure(JsonObject root) {
        this.originJson = root.deepCopy();

        log.debug("CONFIGURE WITH JSON: " + root.toString());

        Set<Entry<String, JsonElement>> elementSet = root.entrySet();
        for (Entry<String, JsonElement> entry : elementSet) {
            String name = entry.getKey();
            Object result = null;
            if (elementClasses.containsKey(name)) {
                log.debug("FOUND element class  for [" + name + "] :: " + elementClasses.get(name));
                try {
                    result = elementClasses.get(name).getConstructor().newInstance().fromJson(entry.getValue());
                } catch (InstantiationException | IllegalAccessException | IllegalArgumentException
                        | InvocationTargetException | NoSuchMethodException | SecurityException e) {
                    log.error("Error deserializing [" + name + "]", e);
                    continue;
                }
                // gson.fromJson(entry.getValue(), elementClasses.get(name));
            } else
                result = JSONUtils.element2object(entry.getValue());
            factoryData.put(name, result);
            postprocess(name, result);
        }

    }

    protected void postprocess(String name, Object object) {
        if (object instanceof FactoryElement) {
            FactoryElement fe = (FactoryElement) object;
            log.debug("POSTPROCESS :: FactoryElement");
            if (CommonUtils.isValid(fe.getId()))
                this.factoryId = fe.getId();
            this.factoryType = fe.getType();
            this.category = fe.getCategory();
            this.label = fe.getLabel();
            this.shortName = fe.getShortName();
            this.displayName = fe.getDisplayName();
            this.description = fe.getDescription();
            this.parent = fe.getParent();
            this.isAbstract = fe.isAbstract();
            this.isInternal = fe.isInternal();
            this.isDeprecated = fe.isDeprecated();
        } else if (object instanceof OutputsElement) {
            log.debug("POSTPROCESS :: OutputsElement");
            this.outputs = (OutputsElement) object;
            log.debug("POSTPROCESS :: Default output config size -> " +
                    this.outputs.getOutput(this.outputs.getDefaultOutputType()).getConfig().size());
            if (this.fields != null) {
                this.outputs.setCommonFields(this.fields.getFieldsMap());
                log.debug("COMMON FIELDS :: " + this.fields.getFieldsMap().size());
            } else {
                log.debug("fields :: null");
            }
        } else if (object instanceof FieldsElement) {
            log.debug("POSTPROCESS :: FieldsElement");
            this.fields = (FieldsElement) object;
            if (this.outputs != null) {
                this.outputs.setCommonFields(this.fields.getFieldsMap());
                log.debug("COMMON FIELDS :: " + this.fields.getFieldsMap().size());
            } else {
                log.debug("outputs :: null");
            }
        }
    }

    public boolean isValid() {
        return CommonUtils.isValid(this.factoryId) && CommonUtils.isValid(this.factoryType);
    }

    @Override
    public String getId() {
        return this.factoryId;
    }

    public void setFactoryId(String factoryId) {
        this.factoryId = factoryId;
    }

    @Override
    public String getType() {
        return this.factoryType;
    }

    @Override
    public String getShortName() {
        return this.shortName;
    }

    @Override
    public String getDisplayName() {
        return displayName;
    }

    @Override
    public String getDescription() {
        return description;
    }

    @Override
    public String getLabel() {
        return label;
    }

    @Override
    public String getCategory() {
        return category;
    }

    @Override
    public String getParent() {
        return parent;
    }

    @Override
    public boolean isAbstract() {
        return isAbstract;
    }

    @Override
    public boolean isInternal() {
        return isInternal;
    }

    @Override
    public boolean isDeprecated() {
        return isDeprecated;
    }

    @Override
    public Map<String, Object> getTypeInfo() {
        if (factoryData.containsKey(getType()))
            return (Map<String, Object>) factoryData.get(getType());
        return Collections.emptyMap();
    }

    @Override
    public String getJsonConfiguration() {
        // JsonElement je = (new Gson()).toJsonTree(this.factoryData);
        return EntaxyFactoryUtils.getEffectiveJson(this);
    }

    @Override
    public String getJsonOrigin() {
        return this.originJson.toString();
    }

    @Override
    public Generated generate(Map<String, Object> parameters) throws EntaxyFactoryException {
        return generate(this.outputs.getDefaultOutputType(), parameters);
    }

    @Override
    public Generated generate(String outputType, Map<String, Object> parameters) throws EntaxyFactoryException {
        if (!this.outputs.hasOutput(outputType))
            throw new OutputNotDefinedException(this, outputType);
        return this.generate(outputType, EntaxyFactory.SCOPE.PUBLIC.label, parameters);
    }

    @Override
    public Generated generate(String outputType, String scope, Map<String, Object> parameters)
            throws EntaxyFactoryException {
        log.debug("Generating for: output type [{}], scope [{}]", outputType, scope);

        if (!this.outputs.hasOutput(outputType)) {
            log.debug("Factory: {}. Unknown output: {}", this.getId(), outputType);
            throw new OutputNotDefinedException(this, outputType);
        }

        OutputElement oe = this.outputs.getOutput(outputType);
        if (!oe.isScopeSupported(scope)) {
            log.error("Factory: {}, output: {}. Scope not supported: {}; supported scopes: [{}]", this.getId(),
                    oe.getType(), scope,
                    oe.getSupportedScopes().stream().map(s -> s.label).collect(Collectors.joining(",")));
            throw new ScopeNotSupportedException(this, outputType, scope,
                    oe.getSupportedScopes().stream().map(s -> s.label).collect(Collectors.toList()));
        }

        if (this.getHelper() != null)
            try {
                return this.getHelper().generateForFactory(this, outputType, SCOPE.valueOfLabel(scope), parameters);
            } catch (Exception e) {
                log.error("Generate failed", e);
                // TODO fill the exception
                throw new EntaxyFactoryException("Generation failed", e);
            }
        return null;
    }

    @Override
    public List<OutputInfo> getOutputs() {
        return this.outputs.getOutputs().stream().map(el -> (OutputInfo) el).collect(Collectors.toList());
    }

    @Override
    public OutputInfo getOutputByType(String outputType) {
        return this.outputs.getOutput(outputType);
    }

    @Override
    public List<FieldInfo> getFields() {
        return this.fields.getFields().stream().map(f -> (FieldInfo) f).collect(Collectors.toList());
    }

    @Override
    public List<FieldInfo> getFields(String outputType) {
        return this.outputs.getOutput(outputType).getFields();
    }

    @Override
    public OutputInfo getDefaultOutput() {
        return (OutputInfo) outputs.getOutput(outputs.getDefaultOutputType());
    }
}
