/*-
 * ~~~~~~licensing~~~~~~
 * object-producer-core
 * ==========
 * 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.objectmodel;

import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.regex.Matcher;
import java.util.regex.Pattern;

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

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

import ru.entaxy.platform.base.objects.factory.EntaxyFactory;
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 Calculation extends AbstractItem {

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

    // TODO move const to EntaxyObject
    public static final String FIELD_IS_CALCULATED = "isCalculated";

    public static final String OWNER_PREFIX = "#OWNER";
    public static final String OWNER_OF_TYPE_PREFIX = "#OWNER_OF_TYPE";
    public static final String FACTORY_PREFIX = "#FACTORY";
    public static final String TYPE_DATA_PREFIX = "#TYPEINFO";

    public static final String OWNER_TOKEN = "#OWNER#";
    public static final String FACTORY_TOKEN = "#FACTORY#";
    public static final String TYPE_DATA_TOKEN = "#TYPEINFO#";

    public static final Object NOT_ENOUGH_DATA = new Object();

    public static boolean isCalculated(JsonObject jsonObject) {
        boolean result = false;

        // old-style calculated
        if (jsonObject.has(FIELD_IS_CALCULATED))
            try {
                result = jsonObject.get(FIELD_IS_CALCULATED).getAsBoolean();
            } catch (Exception e) {
                // return false;
            }

        // new-style calculated
        if (jsonObject.has(EntaxyFactory.CONFIGURATION.DIRECTIVES.CALCULATED)) {
            result = jsonObject.get(EntaxyFactory.CONFIGURATION.DIRECTIVES.CALCULATED).isJsonObject();
        }
        return result;
    }

    public static boolean isValidResult(Object result) {
        return !(ExpressionCalculation.TOKEN_NOT_FOUND.equals(result)
                || ExpressionCalculation.RESULT_NOT_ALLOWED.equals(result)
                || NOT_ENOUGH_DATA.equals(result));
    }

    protected ExpressionCalculation expressionCalculation;

    protected String targetPath;

    protected boolean removeResolved = false;
    protected boolean mergeToTarget = false;

    protected boolean lazy = true;

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

    public Calculation(ObjectModel objectModel) {
        super(objectModel);
    }

    @Override
    public void wrap(JsonObject object, Map<String, Object> context, String path) {

        JsonObject content = object;
        if (object.has(EntaxyFactory.CONFIGURATION.DIRECTIVES.CALCULATED))
            content = object.get(EntaxyFactory.CONFIGURATION.DIRECTIVES.CALCULATED).getAsJsonObject();

        if (!ExpressionCalculation.isExpressionCalculation(content))
            return;

        super.wrap(object, context, path);


        this.expressionCalculation = new ExpressionCalculation();
        this.expressionCalculation.init(content);

        AbstractFactored contextOwner = AbstractFactored.getContextOwner(context);
        if (contextOwner instanceof FactoredObject)
            this.definedIn = (FactoredObject) contextOwner;

        contextOwner.addInternal(this);

        computeRelativePath(contextOwner.jsonPath);

        if (content.has("targetPath")) {
            JsonElement tp = content.get("targetPath");
            if (tp.isJsonNull()) {
                this.targetPath = this.relativePath;
            } else {
                this.targetPath = tp.getAsString();
            }
        } else {
            this.targetPath = this.relativePath;
        }

        if (content.has("removeResolved"))
            try {
                this.removeResolved = content.get("removeResolved").getAsBoolean();
            } catch (Exception e) {
                // NOOP
            }

        if (content.has("mergeToTarget"))
            try {
                this.mergeToTarget = content.get("mergeToTarget").getAsBoolean();
            } catch (Exception e) {
                // NOOP
            }

        if (content.has("lazy"))
            try {
                this.lazy = content.get("lazy").getAsBoolean();
            } catch (Exception e) {
                // NOOP
            }

        if (content.has("process")) {
            this.process = JSONUtils.element2map(content.get("process"));
        }

        // System.out.println("\n\n===>>>> [CALCULATION] CREATED [" + contextOwner.factoryId + "/" +
        // this.relativePath + "]");

    }

    public Object calculate(ObjectModel objectModel, EntaxyProducerService entaxyProducerService) {
        if (this.definedIn == null)
            return null;
        return calculate(objectModel, this.definedIn, entaxyProducerService);
    }

    public Object calculate(ObjectModel objectModel, FactoredObject factoredObject,
            EntaxyProducerService entaxyProducerService) {

        Object result = this.expressionCalculation.calculate(objectModel, factoredObject, entaxyProducerService);
        if (!process.isEmpty())
            result = processResult(result);
        return result;

    }

    @SuppressWarnings("unchecked")
    protected Object processResult(Object current) {
        Object result = current;
        if (current instanceof List) {
            if (this.process.containsKey("filter")) {
                String filter = this.process.getOrDefault("filter", "").toString();
                if (CommonUtils.isValid(filter)) {
                    String[] filters = filter.split(",");
                    final List<Object> filtered = new ArrayList<>();
                    for (int i = 0; i < filters.length; i++) {
                        String pattern = EntaxyProducerUtils.toRegexPattern(filters[i]);
                        ((List) current).stream()
                                .filter(obj -> obj != null)
                                .filter(obj -> Pattern.matches(pattern, obj.toString()))
                                .forEach(obj -> filtered.add(obj));
                    }
                    result = filtered;
                }
            }
        }
        return result;
    }

    public String getTargetPath() {
        return targetPath;
    }

    public boolean isRemoveResolved() {
        return removeResolved;
    }

    public boolean isMergeToTarget() {
        return mergeToTarget;
    }

    public boolean isLazy() {
        return lazy;
    }

    public boolean hasFallbackObject() {
        return this.expressionCalculation.hasFallbackObject;
    }

    public Object getFallbackObject() {
        return this.expressionCalculation.fallbackObject;
    }

    public static class ExpressionCalculation {

        // TODO move const to EntaxyObject
        public static final String ATTR_EXPRESSION = "expression";

        public static final String ATTR_RESULT_TYPE = "resultType";

        public static final String ATTR_ALLOW_OBJECTS = "allowObjects";
        public static final String ATTR_ALLOW_ARRAYS = "allowArrays";
        public static final String ATTR_ALLOW_PRIMITIVES = "allowPrimitives";
        public static final String ATTR_ALLOW_NULLS = "allowNulls";

        public static final String ATTR_BLACKLIST_CONTENT = "blacklistContent";

        public static final String ATTR_FALLBACK_OBJECT = "fallbackObject";

        protected static final Object TOKEN_NOT_FOUND = new Object();
        protected static final Object RESULT_NOT_ALLOWED = new Object();

        protected static final RESULT_TYPE DEFAULT_RESULT_TYPE = RESULT_TYPE.STRING;

        public enum RESULT_TYPE {
            STRING,
            BOOLEAN,
            NUMBER,
            LIST,
            LIST_OR_SINGLE,
            MAP;
        }

        JsonObject jsonObject;

        String expression;

        String resultTypeValue = "String";

        boolean allowObjects = true;
        boolean allowArrays = true;
        boolean allowPrimitives = true;
        boolean allowNulls = false;

        boolean hasFallbackObject = false;
        Object fallbackObject = null;

        List<String> blacklistContent = new ArrayList<>();

        List<String> toResolve = new ArrayList<>();
        Map<String, Object> resolved = new HashMap<>();

        protected RESULT_TYPE resultType = DEFAULT_RESULT_TYPE;

        public static boolean isExpressionCalculation(JsonObject jsonObject) {
            return jsonObject.has(ATTR_EXPRESSION);
        }

        public void init(JsonObject jsonObject) {
            this.jsonObject = jsonObject;
            this.expression = jsonObject.get(ATTR_EXPRESSION).getAsString();
            if (jsonObject.has(ATTR_RESULT_TYPE)) {
                this.resultTypeValue = jsonObject.get(ATTR_RESULT_TYPE).getAsString();
                if (CommonUtils.isValid(resultTypeValue)) {
                    if (RESULT_TYPE.valueOf(this.resultTypeValue.toUpperCase()) != null)
                        this.resultType = RESULT_TYPE.valueOf(this.resultTypeValue.toUpperCase());
                }
            }
            hasFallbackObject = jsonObject.has(ATTR_FALLBACK_OBJECT);
            if (hasFallbackObject) {
                try {
                    fallbackObject = JSONUtils.element2object(jsonObject.get(ATTR_FALLBACK_OBJECT));
                } catch (Exception ignore) {
                    // NOOP
                }
            }
            try {
                allowObjects = jsonObject.get(ATTR_ALLOW_OBJECTS).getAsBoolean();
            } catch (Exception e) {
                // NOOP
            }
            try {
                allowArrays = jsonObject.get(ATTR_ALLOW_ARRAYS).getAsBoolean();
            } catch (Exception e) {
                // NOOP
            }
            try {
                allowPrimitives = jsonObject.get(ATTR_ALLOW_PRIMITIVES).getAsBoolean();
            } catch (Exception e) {
                // NOOP
            }
            try {
                allowNulls = jsonObject.get(ATTR_ALLOW_NULLS).getAsBoolean();
            } catch (Exception e) {
                // NOOP
            }
            if (jsonObject.has(ATTR_BLACKLIST_CONTENT)) {
                JsonElement blc = jsonObject.get(ATTR_BLACKLIST_CONTENT);
                if (blc.isJsonArray()) {
                    JsonArray ja = blc.getAsJsonArray();
                    for (int i = 0; i < ja.size(); i++) {
                        this.blacklistContent.add(ja.get(i).getAsString());
                    }
                }
            }
            Pattern pattern = Pattern.compile("\\$\\{.+?\\}");
            Matcher matcher = pattern.matcher(this.expression);
            while (matcher.find()) {
                String value = this.expression.substring(matcher.start(), matcher.end());
                // System.out.println("\\nFOUND :: " + value);
                this.toResolve.add(value.substring(2, value.length() - 1));
            }
        }

        public Object calculate(ObjectModel objectModel, FactoredObject factoredObject,
                EntaxyProducerService entaxyProducerService) {
            for (String expr : toResolve) {
                if (!resolved.containsKey(expr)) {
                    Object result = resolve(expr, objectModel, factoredObject, entaxyProducerService);
                    if (!TOKEN_NOT_FOUND.equals(result) && !RESULT_NOT_ALLOWED.equals(result)) {
                        resolved.put(expr, result);
                    }
                }
            }
            toResolve.removeAll(resolved.keySet());
            if (toResolve.isEmpty()) {
                return doCalculate();
            }
            return NOT_ENOUGH_DATA;
        }

        public Object doCalculate() {
            String expr = expression;

            Object result = null;
            List<Object> list = new ArrayList<>();

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

            switch (this.resultType) {
                case LIST:
                    list.addAll(resolved.values());
                    result = list;
                    break;
                case LIST_OR_SINGLE:
                    list.addAll(resolved.values());
                    result = list;
                    if (list.size() == 1)
                        result = list.get(0);
                    break;
                case MAP:
                    for (Map.Entry<String, Object> entry : resolved.entrySet()) {
                        if (entry.getValue() instanceof Map) {
                            for (Object key : ((Map) entry.getValue()).keySet())
                                map.put(key.toString(), ((Map) entry.getValue()).get(key));
                        } else {
                            map.put(entry.getKey(), entry.getValue());
                        }
                    }
                    result = map;
                    break;
                default:
                    for (Entry<String, Object> entry : resolved.entrySet()) {
                        Object value = entry.getValue();
                        String val = value == null ? "" : value.toString();
                        expr = expr.replace("${" + entry.getKey() + "}", val);
                    }
                    switch (this.resultType) {
                        case BOOLEAN:
                            try {
                                result = Boolean.parseBoolean(expr);
                            } catch (Exception e) {
                                result = CommonUtils.isValid(expr);
                            }
                            break;
                        case NUMBER:
                            try {
                                result = Double.parseDouble(expr);
                            } catch (Exception e) {
                                result = CommonUtils.isValid(expr) ? 1 : 0;
                            }
                            break;
                        default:
                            result = expr;
                            break;
                    }
                    break;
            }

            return result;
        }

        protected boolean startsWithOwnerPrefix(String token) {
            if (!CommonUtils.isValid(token))
                return false;
            return token.startsWith(OWNER_PREFIX)
                    || token.startsWith(OWNER_OF_TYPE_PREFIX);
        }

        public Object resolve(String token, ObjectModel objectModel, FactoredObject factoredObject,
                EntaxyProducerService entaxyProducerService) {

            FactoredObject currentObject = factoredObject;
            String currentToken = token;

            while (startsWithOwnerPrefix(currentToken) && currentObject != null) {
                int ind = currentToken.substring(1).indexOf("#.");
                String subtoken;
                if (ind < 0) {
                    subtoken = currentToken;
                    currentToken = "";
                } else {
                    subtoken = currentToken.substring(0, ind + 2);
                    currentToken = currentToken.substring(ind + 3);
                }
                if (Calculation.OWNER_TOKEN.equals(subtoken)) {
                    currentObject = currentObject.getOwnerObject();
                } else if (subtoken.startsWith(Calculation.OWNER_OF_TYPE_PREFIX)) {
                    int i1 = subtoken.indexOf(':');
                    if (i1 < 0) {
                        log.warn("Incorrect expression: [{}]", subtoken);
                        break;
                    }
                    String typeToFind = subtoken.substring(i1 + 1, subtoken.length() - 1);
                    boolean checkTypeHierarchy = false;

                    if (typeToFind.startsWith("+")) {
                        checkTypeHierarchy = true;
                        typeToFind = typeToFind.substring(1);
                    }

                    while (currentObject != null) {
                        currentObject = currentObject.getOwnerObject();
                        if (currentObject != null) {
                            if (typeToFind.equals(currentObject.getObjectType()))
                                break;
                            if (checkTypeHierarchy) {
                                if (entaxyProducerService.isFactoryOfType(currentObject.factoryId, typeToFind))
                                    break;
                            }
                        }
                    }

                } else
                    break;
            }
            if (currentObject == null)
                return TOKEN_NOT_FOUND;
            if (!CommonUtils.isValid(currentToken))
                return TOKEN_NOT_FOUND;
            JsonObject jsonObject = currentObject.origin;
            if (currentToken.startsWith(FACTORY_TOKEN)) {
                String factoryId = currentObject.factoryId;
                try {
                    EntaxyFactory f = entaxyProducerService.findFactoryById(factoryId);
                    jsonObject = JSONUtils.getJsonRootObject(f.getJsonConfiguration());
                    currentToken = currentToken.substring(Calculation.FACTORY_TOKEN.length());
                    if (currentToken.startsWith("."))
                        currentToken = currentToken.substring(1);

                    if (currentToken.startsWith(TYPE_DATA_TOKEN)) {
                        JsonElement je = jsonObject.get(f.getType());
                        if ((je != null) && je.isJsonObject())
                            jsonObject = je.getAsJsonObject();
                        else
                            jsonObject = null;

                        currentToken = currentToken.substring(Calculation.TYPE_DATA_TOKEN.length());
                        if (currentToken.startsWith("."))
                            currentToken = currentToken.substring(1);
                    }

                } catch (Exception e) {
                    return TOKEN_NOT_FOUND;
                }
            }
            JsonElement result = null;
            if (jsonObject != null)
                if (CommonUtils.isValid(currentToken))
                    result = findElement(jsonObject, currentToken);
                else
                    result = jsonObject;

            if (result == null)
                return TOKEN_NOT_FOUND;
            if (!valueIsAllowed(result))
                return RESULT_NOT_ALLOWED;


            return JSONUtils.element2object(result);
        }

        protected boolean valueIsAllowed(JsonElement value) {
            if (value.isJsonNull() && !allowNulls)
                return false;
            if (value.isJsonObject())
                if (!allowObjects)
                    return false;
                else {
                    if (value.getAsJsonObject().has(EntaxyFactory.CONFIGURATION.DIRECTIVES.CALCULATED))
                        return false;
                    JsonObject jo = value.getAsJsonObject();
                    for (String s : this.blacklistContent)
                        if (jo.has(s))
                            return false;
                }
            if (value.isJsonArray() && !allowArrays)
                return false;
            if (value.isJsonPrimitive()) {
                if (!allowPrimitives)
                    return false;
                else {
                    if (!this.blacklistContent.isEmpty()) {
                        if (value.getAsJsonPrimitive().isString()) {
                            String stringValue = value.getAsString();
                            for (String s : this.blacklistContent) {
                                String pattern = EntaxyProducerUtils.toRegexPattern(s);
                                if (Pattern.matches(pattern, stringValue))
                                    return false;
                            }
                        }
                    }
                }
            }
            return true;
        }

        public JsonElement findElement(JsonObject jsonObject, String pathFragment) {
            JsonElement result = null;
            if (pathFragment.startsWith("?.")) {
                result = findElement(jsonObject, pathFragment.substring(2), "");
            } else {
                result = getElement(jsonObject, pathFragment);
            }

            return result;
        }

        public JsonElement getElement(JsonObject jsonObject, String pathFragment) {
            int ind = pathFragment.indexOf(".");
            String currentFragment;
            String newFragment;
            if (ind > 0) {
                currentFragment = pathFragment.substring(0, ind);
                newFragment = pathFragment.substring(ind + 1);
            } else {
                currentFragment = pathFragment;
                newFragment = null;
            }
            if (!jsonObject.has(currentFragment))
                return null;
            JsonElement je = jsonObject.get(currentFragment);
            if (newFragment == null)
                return je;
            if (!je.isJsonObject())
                return null;
            return getElement(je.getAsJsonObject(), newFragment);
        }

        public JsonElement findElement(JsonObject jsonObject, String pathFragment, String parentPath) {

            for (Entry<String, JsonElement> entry : jsonObject.entrySet()) {

                String currentPath = parentPath + "." + entry.getKey();
                if (currentPath.endsWith(pathFragment))
                    return entry.getValue();

            }

            for (Entry<String, JsonElement> entry : jsonObject.entrySet()) {

                if (!entry.getValue().isJsonObject())
                    continue;

                String currentPath = parentPath + "." + entry.getKey();

                JsonElement currentResult = findElement(entry.getValue().getAsJsonObject(), pathFragment, currentPath);

                if (currentResult != null)
                    return currentResult;
            }


            return null;
        }

    }

}
