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

import java.util.ArrayList;
import java.util.Arrays;
import java.util.Base64;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Optional;
import java.util.regex.Pattern;

import org.apache.felix.utils.properties.TypedProperties;
import org.osgi.framework.ServiceReference;
import org.osgi.service.cm.Configuration;
import org.osgi.service.cm.ConfigurationAdmin;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

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

import ru.entaxy.platform.base.objects.EntaxyObject;
import ru.entaxy.platform.base.objects.factory.EntaxyFactory;
import ru.entaxy.platform.base.objects.factory.EntaxyFactory.FieldInfo;
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.EntaxyProducerUtils;

public class ObjectConfig {

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

    public static final String CONFIG_OBJECT_TYPE = "entaxy.runtime.config";

    public static final String TARGET_FACTORY_FIELD_NAME = "targetObjectFactoryId";
    public static final String TARGET_TYPE_FIELD_NAME = "targetObjectType";
    public static final String TARGET_ID_FIELD_NAME = "targetObjectId";
    public static final String TARGET_OUTPUT_FIELD_NAME = "targetObjectOutput";

    public static final String CONFIG_PROPERTIES_FIELD_NAME = "configuredProperties";
    public static final String CONFIG_FACTORY_FIELD_NAME = "__entaxy_configuredFactoryId";
    public static final String CONFIG_TYPE_FIELD_NAME = "__entaxy_configuredType";
    public static final String CONFIG_ID_FIELD_NAME = "__entaxy_configuredId";
    public static final String CONFIG_OUTPUT_FIELD_NAME = "__entaxy_configuredOutput";
    public static final String CONFIG_HINT_FIELD_NAME = "__entaxy_config_hint";

    public static final List<String> CONFIG_INTERNAL_FIELDS = Arrays.asList(new String[] {CONFIG_FACTORY_FIELD_NAME,
        CONFIG_TYPE_FIELD_NAME, CONFIG_ID_FIELD_NAME, CONFIG_OUTPUT_FIELD_NAME, CONFIG_HINT_FIELD_NAME});

    public static final String CONFIG_PID_FIELD_NAME = "configurationPid";
    public static final String CONFIG_PLACEHOLDER_PREFIX_FIELD_NAME = "placeholderPrefix";

    @Deprecated
    public static final String CONFIG_FIELD_NAME = "##config";
    public static final String CONFIG_DEFAULTS_FIELD_NAME = "##config";
    public static final String CONFIG_LINK_FIELD_NAME = "##config-link";
    public static final String CONFIGURABLE_ATTRIBUTE_NAME = "configurable";
    public static final String DEFAULT_FIELDS_CONFIGURABLE_ATTRIBUTE_NAME = "fieldsConfigurableByDefault";
    public static final String CONFIGURABLE_FIELDS_ATTRIBUTE_NAME = "configurableFields";
    public static final String PROPERTIES_BACKUP_FIELD = EntaxyObject.FIELDS.PROPERTIES + "_backup";

    public static final String DIRECT_VALUE_ONLY_ATTR_NAME = "directValueOnly";

    public static final String CONFIGURABLES_FIELD_NAME = "configurables";
    public static final String IMMUTABLES_FIELD_NAME = "immutables";

    public static final String HEADER_OBJECT_CONFIG_PID = "Entaxy-Generated-Object-Config-Pid";

    protected static Gson GSON = new GsonBuilder().create();

    public static String getConfigurationPid(String objectId, String type) {
        return reduceType(type) + "." + objectId.replace("-", "_");
    }

    public static String reduceType(String type) {
        return type
                // .replace(EntaxyObject.OBJECT_TYPES.ENTAXY_RUNTIME_TYPE_PREFIX, "e_r_")
                .replace("-", "_");
    }

    public static String getConfigurationPrefix(String objectId, String type) {
        return "$" + getConfigurationPid(objectId, type) + "{";
    }

    public static boolean isConfigurable(EntaxyFactory factory, String outputType, String fieldName,
            boolean defaultFieldsConfigurable, List<ConfigurableFieldsConfiguration> configurations) {
        Optional<FieldInfo> fi = factory.getOutputByType(outputType).getFields().stream()
                .filter(f -> f.getName().equals(fieldName)).findFirst();
        if (!fi.isPresent())
            return false;

        boolean fieldIsConfigurable = defaultFieldsConfigurable;

        try {
            fieldIsConfigurable = fi.get().getJsonOrigin().get(CONFIGURABLE_ATTRIBUTE_NAME).getAsBoolean();
        } catch (Exception e) {
            // usinf default value
        }

        if (!fieldIsConfigurable)
            return false;

        for (ConfigurableFieldsConfiguration cfc : configurations)
            if (cfc.isConfigurable(factory, outputType, fieldName))
                return true;

        return false;
    }

    public static void backupProperties(Map<String, Object> parameters) {
        backupProperties(parameters, EntaxyObject.FIELDS.PROPERTIES);
    }

    @SuppressWarnings("unchecked")
    public static void backupProperties(Map<String, Object> parameters, String fieldName) {
        if (!parameters.containsKey(fieldName))
            return;
        Map<String, Object> backup = new HashMap<>();
        backup.putAll((Map<String, Object>) parameters.get(fieldName));
        parameters.put(ObjectConfig.PROPERTIES_BACKUP_FIELD, backup);
    }

    public static void restoreBackupProperties(Map<String, Object> parameters) {
        restoreBackupProperties(parameters, EntaxyObject.FIELDS.PROPERTIES);
    }

    public static void restoreBackupProperties(Map<String, Object> parameters, String fieldName) {
        if (!parameters.containsKey(ObjectConfig.PROPERTIES_BACKUP_FIELD))
            return;
        parameters.put(fieldName, parameters.get(ObjectConfig.PROPERTIES_BACKUP_FIELD));
    }

    public static void removeConfigData(Map<String, Object> parameters) {
        removeConfigData(parameters, EntaxyObject.FIELDS.PROPERTIES);
    }

    @SuppressWarnings("unchecked")
    public static void removeConfigData(Map<String, Object> parameters, String fieldName) {
        if (!parameters.containsKey(fieldName))
            return;
        ((Map<String, Object>) parameters.get(fieldName)).remove(CONFIG_FIELD_NAME);
    }

    public static Map<String, Object> getTargetConfigMap(EntaxyFactory factory, String outputType, String fieldName,
            Map<String, Object> configurables, Map<String, Object> immutables) {
/*
 		Optional<FieldInfo> fi = factory.getOutputByType(outputType).getFields().stream().filter(f -> f.getName().equals(fieldName)).findFirst();
 
		if (!fi.isPresent()) {
			log.debug("FIELD NOT FOUND: " + fieldName);
			return null;
		}
		FieldInfo field = fi.get();
		boolean isConfigurable = true;
		log.debug("\n FIELDNAME:: " + fieldName + " -> " + field.getJsonOrigin());
		if (field.getJsonOrigin().has(CONFIGURABLE_ATTRIBUTE_NAME))
			try {
				isConfigurable =  field.getJsonOrigin().get(CONFIGURABLE_ATTRIBUTE_NAME).getAsBoolean();
			} catch (Exception e) {
				isConfigurable = true; 
			}
		if (!isConfigurable) {
			log.debug("FIELD NOT CONFIGURABLE: " + fieldName);
			return null;
		}
*/
        Optional<FieldInfo> fi = factory.getOutputByType(outputType).getFields().stream()
                .filter(f -> f.getName().equals(fieldName)).findFirst();

        if (!fi.isPresent()) {
            log.debug("FIELD NOT FOUND: " + fieldName);
            return null;
        }

        FactoryOutputConfiguration conf = FactoryOutputConfiguration.read(factory, outputType);

        if (!isConfigurable(factory, outputType, fieldName, conf.defaultFieldsConfigurable, conf.configurations))
            return null;

        FieldInfo field = fi.get();

        if (field.isImmutable())
            return immutables;
        return configurables;
    }

    public static String getConfiguredObjectId(String pid) {
        ServiceReference<ConfigurationAdmin> ref = null;
        try {
            ref =
                    ObjectConfigHelper.getInstance().getReference(ConfigurationAdmin.class);
            ConfigurationAdmin configurationAdmin = ObjectConfigHelper.getInstance().getService(ref);

            Configuration configuration = configurationAdmin.getConfiguration(pid, "?");
            if (configuration == null)
                return null;

            String configuredId = configuration.getProperties().get(CONFIG_ID_FIELD_NAME).toString();
            String configuredType = configuration.getProperties().get(CONFIG_TYPE_FIELD_NAME).toString();

            if (!CommonUtils.isValid(configuredId) || !CommonUtils.isValid(configuredType))
                return null;

            return configuredId + ":" + configuredType;

        } catch (Exception e) {
            return null;
        } finally {
            ObjectConfigHelper.getInstance().ungetService(ref);
        }
    }

    public static class FactoryOutputConfiguration {

        public static FactoryOutputConfiguration read(EntaxyFactory factory, String outputType) {

            FactoryOutputConfiguration result = new FactoryOutputConfiguration();

            OutputInfo oi = factory.getOutputByType(outputType);
            if (oi == null)
                oi = factory.getDefaultOutput();

            Map<String, Object> outputConfig = oi.getConfig();

            result.isConfigurable = false;
            try {
                result.isConfigurable = (Boolean) outputConfig.get(ObjectConfig.CONFIGURABLE_ATTRIBUTE_NAME);
            } catch (Exception e) {
                log.trace("Parameter [{}] not found in [{}]:[{}]", ObjectConfig.CONFIGURABLE_ATTRIBUTE_NAME,
                        factory.getId(), outputType);
            }
            if (!result.isConfigurable)
                return result;

            result.defaultFieldsConfigurable = false;
            try {
                result.defaultFieldsConfigurable =
                        (Boolean) outputConfig.get(ObjectConfig.DEFAULT_FIELDS_CONFIGURABLE_ATTRIBUTE_NAME);
            } catch (Exception e) {
                log.trace("Parameter [{}] not found in [{}]:[{}]",
                        ObjectConfig.DEFAULT_FIELDS_CONFIGURABLE_ATTRIBUTE_NAME, factory.getId(), outputType);
            }

            result.configurations = new ArrayList<>();

            Object configuration = outputConfig.get(ObjectConfig.CONFIGURABLE_FIELDS_ATTRIBUTE_NAME);
            if (configuration != null) {
                if (configuration instanceof List) {
                    List configList = (List) configuration;
                    for (Object configItem : configList) {
                        if (configItem instanceof Map) {
                            ObjectConfig.ConfigurableFieldsConfiguration cnf =
                                    ObjectConfig.ConfigurableFieldsConfiguration.read((Map) configItem);
                            result.configurations.add(cnf);
                        }
                    }
                } else if (configuration instanceof Map) {
                    ObjectConfig.ConfigurableFieldsConfiguration cnf =
                            ObjectConfig.ConfigurableFieldsConfiguration.read((Map) configuration);
                    result.configurations.add(cnf);
                }
            } else {
                result.configurations.add(new ObjectConfig.ConfigurableFieldsConfiguration());
            }

            return result;
        }

        public boolean isConfigurable;
        public boolean defaultFieldsConfigurable;
        public List<ConfigurableFieldsConfiguration> configurations;

    }

    public static class ConfigurableFieldsConfiguration {

        public static ConfigurableFieldsConfiguration read(Map parameters) {
            JsonElement obj = GSON.toJsonTree(parameters);
            if (obj.isJsonObject()) {
                ConfigurableFieldsConfiguration result = GSON.fromJson(obj, ConfigurableFieldsConfiguration.class);
                return result;
            }
            return null;
        }


        List<String> includeNames = new ArrayList<>();
        List<String> includePatterns = new ArrayList<>();
        List<String> excludeNames = new ArrayList<>();
        List<String> excludePatterns = new ArrayList<>();

        List<String> includeTypes = new ArrayList<>();
        List<String> excludeTypes = new ArrayList<>();

        public ConfigurableFieldsConfiguration() {
            includeTypes.add("string");
            includeTypes.add("boolean");
            includeTypes.add("number");
            includePatterns.add("*");
        }


        public boolean isConfigurable(EntaxyFactory factory, String outputType, String fieldName) {
            Optional<FieldInfo> fi = factory.getOutputByType(outputType).getFields().stream()
                    .filter(f -> f.getName().equals(fieldName)).findFirst();
            if (!fi.isPresent())
                return false;

            String fieldType = fi.get().getType().toLowerCase();


            boolean fieldIsExcluded = false;
            for (String name : excludeNames)
                if (fieldName.equals(name)) {
                    fieldIsExcluded = true;
                    break;
                }

            if (!fieldIsExcluded) {
                for (String pattern : excludePatterns) {
                    if (Pattern.matches(EntaxyProducerUtils.toRegexPattern(pattern), fieldName)) {
                        fieldIsExcluded = true;
                        break;
                    }
                }
            }


            if (!fieldIsExcluded) {
                for (String type : excludeTypes)
                    if (type.equals(fieldType)) {
                        fieldIsExcluded = true;
                        break;
                    }
            }

            if (fieldIsExcluded)
                return false;

            boolean fieldIsIncluded = false;

            // first we check types
            if (!fieldIsIncluded) {
                for (String type : includeTypes)
                    if (type.equals(fieldType)) {
                        fieldIsIncluded = true;
                        break;
                    }
            }
            if (!fieldIsIncluded)
                return false;

            if (!includeNames.isEmpty() || !includePatterns.isEmpty())
                fieldIsIncluded = false;

            for (String name : includeNames)
                if (fieldName.equals(name)) {
                    fieldIsIncluded = true;
                    break;
                }

            if (!fieldIsIncluded) {
                for (String pattern : includePatterns) {
                    if (Pattern.matches(EntaxyProducerUtils.toRegexPattern(pattern), fieldName)) {
                        fieldIsIncluded = true;
                        break;
                    }
                }
            }

            return fieldIsIncluded;
        }
    }

    public static class ConfigHint {

        public static final String CONFIG_HINT_SECTION_FIELDS = "fields";

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

        public Map<String, Object> getAsMap() {
            return configHint;
        }

        public JsonElement getAsJson() {
            return JSONUtils.GSON.toJsonTree(configHint);
        }

        public String getAsJsonString() {
            return getAsJson().toString();
        }

        public void generate(Map<String, Object> configProperties, String factoryId, String outputType) {

            Map<String, Object> fieldsHint = generateFieldsHint(configProperties, factoryId, outputType);
            if (fieldsHint != null)
                configHint.put(CONFIG_HINT_SECTION_FIELDS, fieldsHint);

        }

        public void readFrom(TypedProperties typedProperties) {
            if (typedProperties.containsKey(CONFIG_HINT_FIELD_NAME)) {
                String encoded = typedProperties.get(CONFIG_HINT_FIELD_NAME).toString();
                String hintString = new String(Base64.getDecoder().decode(encoded));
                read(hintString);
            }
        }

        public void read(String hintString) {
            this.configHint = JSONUtils.element2map(JSONUtils.getJsonRootObject(hintString));
        }

        protected Map<String, Object> generateFieldsHint(Map<String, Object> configProperties, String factoryId,
                String outputType) {

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

            try {

                ServiceReference<EntaxyProducerService> ref =
                        ObjectConfigHelper.getInstance().getReference(EntaxyProducerService.class);
                if (ref != null) {
                    EntaxyProducerService entaxyProducerService = ObjectConfigHelper.getInstance().getService(ref);

                    EntaxyFactory factory = entaxyProducerService.findFactoryById(factoryId);
                    if (factory == null) {
                        log.error("Factory not found: [{}]", factoryId);
                        return null;
                    }

                    OutputInfo outputInfo = null;

                    if (CommonUtils.isValid(outputType)) {
                        outputInfo = factory.getOutputByType(outputType);
                    } else {
                        outputInfo = factory.getDefaultOutput();
                    }
                    if (outputInfo == null) {
                        log.error("Output [{}] not found in factory [{}]", outputType, factoryId);
                        return null;
                    }

                    for (FieldInfo fi : outputInfo.getFields()) {
                        if (configProperties.containsKey(fi.getName())) {
                            ConfigFieldHint fieldHint = ConfigFieldHint.create(fi);
                            result.put(fieldHint.getName(), (Map<String, Object>) fieldHint);
                        }
                    }

                    ObjectConfigHelper.getInstance().ungetService(ref);

                    for (String name : ObjectConfig.CONFIG_INTERNAL_FIELDS) {
                        ConfigFieldHint fieldHint = new ConfigFieldHint(name);
                        fieldHint.setInternal(true);
                        result.put(name, (Map<String, Object>) fieldHint);
                    }

                }

                return result;

            } catch (Exception e) {
                return null;
            }
        }

        public Map<String, ConfigFieldHint> getFieldsHint() {
            if (configHint.containsKey(CONFIG_HINT_SECTION_FIELDS)) {
                Object obj = configHint.get(CONFIG_HINT_SECTION_FIELDS);
                if (obj instanceof Map) {
                    Map<String, Object> fieldHints = (Map<String, Object>) obj;
                    Map<String, ConfigFieldHint> result = new HashMap<>();

                    for (String name : fieldHints.keySet()) {
                        ConfigFieldHint fieldHint = new ConfigFieldHint(name);
                        fieldHint.load((Map<String, Object>) fieldHints.get(name));
                        result.put(name, fieldHint);
                    }

                    return result;
                }
            }
            return new HashMap<>();
        }

    }

    public static class ConfigFieldHint extends HashMap<String, Object> {

        private static final long serialVersionUID = 1L;

        public static final String CONFIG_FIELD_IS_INTERNAL = "isInternal";

        public static ConfigFieldHint create(FieldInfo fieldInfo) {
            ConfigFieldHint result = new ConfigFieldHint(fieldInfo.getName());

            result.setType(fieldInfo.getType());
            result.setDisplayName(fieldInfo.getDisplayName());
            result.setDescription(fieldInfo.getDescription());
            result.setInternal(false);

            return result;

        }

        public ConfigFieldHint(String name) {
            super();
            setName(name);
        }

        String name;

        public void load(Map<String, Object> data) {
            this.putAll(data);
        }

        public String getName() {
            return name;
        }

        public void setName(String name) {
            this.name = name;
        }

        public String getDisplayName() {
            Object displayName = getOrDefault(EntaxyFactory.CONFIGURATION.FIELDS.ATTRIBUTES.FIELD_DISPLAY_NAME, "");
            return displayName != null ? displayName.toString() : "";
        }

        public void setDisplayName(String displayName) {
            put(EntaxyFactory.CONFIGURATION.FIELDS.ATTRIBUTES.FIELD_DISPLAY_NAME, displayName);
        }

        public String getType() {
            return getOrDefault(EntaxyFactory.CONFIGURATION.FIELDS.ATTRIBUTES.FIELD_TYPE, "String").toString();
        }

        public void setType(String type) {
            put(EntaxyFactory.CONFIGURATION.FIELDS.ATTRIBUTES.FIELD_TYPE, type);
        }

        public String getDescription() {
            return getOrDefault(EntaxyFactory.CONFIGURATION.FIELDS.ATTRIBUTES.FIELD_DESCRIPTION, "").toString();
        }

        public void setDescription(String description) {
            put(EntaxyFactory.CONFIGURATION.FIELDS.ATTRIBUTES.FIELD_DESCRIPTION, description);
        }

        public boolean isInternal() {
            return (Boolean) getOrDefault(CONFIG_FIELD_IS_INTERNAL, false);
        }

        public void setInternal(boolean isInternal) {
            put(CONFIG_FIELD_IS_INTERNAL, isInternal);
        }

    }

}
