/*-
 * ~~~~~~licensing~~~~~~
 * profile-runtime
 * ==========
 * 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.profiles.runtime.support;

import java.lang.reflect.Type;
import java.util.ArrayList;
import java.util.List;
import java.util.Map.Entry;
import java.util.regex.Pattern;
import java.util.stream.Collectors;

import org.apache.camel.Exchange;
import org.apache.camel.Processor;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import com.google.gson.Gson;
import com.google.gson.JsonElement;
import com.google.gson.JsonObject;
import com.google.gson.reflect.TypeToken;

import ru.entaxy.platform.base.support.CommonUtils;
import ru.entaxy.platform.base.support.JSONUtils;
import ru.entaxy.platform.profiles.runtime.OutConnector;
import ru.entaxy.platform.profiles.runtime.SystemProfile;
import ru.entaxy.platform.services.runtime.exceptions.ConnectorNotFound;

public class ConnectorSelector implements Processor {

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

    public static final String PROP_TARGET_CONNECTOR = "NTX_target_connector";
    public static final String PROP_PREFERRED_CONNECTOR = "NTX_preferred_connector";
    public static final String PROP_CONNECTOR_PREFERRED_AS_MANDATORY = "NTX_connector_preferred_as_mandatory";

    public static final String PROP_SOURCE_PREFIX = "NTX_SOURCE_";

    public static final String PATTERN_ANY = "*";
    public static final String PATTERN_EQUAL = "=";
    public static final String NTX_LOGGING_KEY = "NTX_loggingKey";

    protected SystemProfile profile;

    protected String config;

    protected SelectorConfig selectorConfig;

    @Override
    public void process(Exchange exchange) throws Exception {
        log.debug("--> SELECTING CONNECTOR !!!");
        try {
            exchange.getIn().removeHeader(PROP_TARGET_CONNECTOR);
            SelectionExecutor executor = new SelectionExecutor(selectorConfig, profile);
            OutConnector result = executor.findConnector(exchange);
            if (result != null)
                exchange.getIn().setHeader(PROP_TARGET_CONNECTOR, result.getId());
        } catch (Exception ex) {
            log.error(exchange.getIn().getHeader(NTX_LOGGING_KEY) +". Exception: " + ex.getMessage() + "with params: preferred_connector-" + exchange.getIn().getHeader(PROP_PREFERRED_CONNECTOR)
            + ", connector_preferred_as_mandatory-" +  exchange.getIn().getHeader(PROP_CONNECTOR_PREFERRED_AS_MANDATORY));
            throw ex;
        } finally {
            exchange.getIn().removeHeader(PROP_PREFERRED_CONNECTOR);
            exchange.getIn().removeHeader(PROP_CONNECTOR_PREFERRED_AS_MANDATORY);
        }
    }

    public SystemProfile getProfile() {
        return profile;
    }

    public void setProfile(SystemProfile profile) {
        this.profile = profile;
    }

    public String getConfig() {
        return config;
    }

    public void setConfig(String config) {
        this.config = config;
        processConfig();
    }

    protected void processConfig() {
        if (!CommonUtils.isValid(config)) {
            selectorConfig = new SelectorConfig();
        } else {
            selectorConfig = new SelectorConfig(this.config);
        }
    }

    protected static class SelectionExecutor {

        protected SelectorConfig selectorConfig;
        protected SystemProfile profile;

        public SelectionExecutor(SelectorConfig selectorConfig, SystemProfile profile) {
            this.selectorConfig = selectorConfig;
            this.profile = profile;
        }

        public OutConnector findConnector(Exchange exchange) {
            OutConnector result = null;

            if (selectorConfig.useTheOnlyConnector && (profile.getOutConnectors().size() == 1))
                return profile.getOutConnectors().get(0);

            Object value = exchange.getIn().getHeader(PROP_PREFERRED_CONNECTOR);
            if (value != null) {
                try {
                    SelectorConfig preferredConfig = createConfigForPreferredConnector(value);
                    List<String> resolvedPatters = new ArrayList<>(preferredConfig.patterns);
                    result = findConnectorByPatterns(preferredConfig, resolvedPatters);
                    if (result != null)
                        return result;
                } catch (Exception e) {
                    log.warn("Invalid preferred connector format", e);
                }

                Boolean connectorPreferredAsMandatory = getConnectorPreferredAsMandatory(exchange);
                if (result == null && ((connectorPreferredAsMandatory != null && connectorPreferredAsMandatory)
                        || (selectorConfig.connectorPreferredAsMandatory != null && selectorConfig.connectorPreferredAsMandatory))) {
                    setConnectorPreferredAsNotMandatory(exchange, selectorConfig);
                    throw new ConnectorNotFound(getHeaderLoggingKey(exchange) + " Connector by preferred connector [" + value + "] not found");
                }
            }

//          FIND BY SOURCE
            List<String> resolvedPatterns = resolvePatternsForExchange(selectorConfig, exchange);
            result = findConnectorByPatterns(selectorConfig, resolvedPatterns);

            if (result == null && selectorConfig.useDefaultConnector)
                result = profile.getDefaultOutConnector();

            if (result == null) {
                StringBuilder errorMessage = new StringBuilder();
                errorMessage.append(getHeaderLoggingKey(exchange)).append(" Connector by source")
                        .append(selectorConfig.useDefaultConnector ? " " : " and default connector not found ").append(" not found");
                throw new ConnectorNotFound(errorMessage.toString());
            }

            return result;
        }

        private String getHeaderLoggingKey(Exchange exchange) {
            Object headerLoggingKey = exchange.getIn().getHeader(NTX_LOGGING_KEY);
            return headerLoggingKey == null ? "" : headerLoggingKey.toString();
        }

        protected List<String> resolvePatternsForExchange(SelectorConfig config, Exchange exchange) {
            List<String> result = new ArrayList<>();
            List<String> values = new ArrayList<>();
            for (String property : config.properties) {
                String headerName = PROP_SOURCE_PREFIX + property.replace('.', '_');
                Object value = exchange.getIn().getHeader(headerName);
                if (value == null) {
                    if (config.treatNoPropertyAsAny)
                        values.add(PATTERN_ANY);
                    else
                        values.add("");
                    continue;
                }
                values.add(value.toString());
            }
            for (String pattern : config.patterns)
                result.add(resolvePattern(pattern, values));
            return result;
        }

        protected String resolvePattern(String pattern, List<String> values) {
            String[] splitted = pattern.split(":");
            for (int i = 0; i < splitted.length; i++)
                if (PATTERN_EQUAL.equals(splitted[i]))
                    splitted[i] = values.get(i);
            return String.join(":", splitted);
        }

        protected OutConnector findConnectorByPatterns(SelectorConfig config, List<String> resolvedPatterns) {
            for (String resolvedPattern : resolvedPatterns) {
                if (!config.allowAllStarsPattern && isAllStarsPattern(resolvedPattern))
                    continue;
                String[] pattern = resolvedPattern.split(":");
                for (OutConnector connector : profile.getOutConnectors()) {
                    String[] connectorInfo = getConnectorInfo(config, connector);
                    boolean found = false;
                    for (int i = 0; i < pattern.length; i++) {
                        if ((pattern[i] == null)
                                || (PATTERN_ANY.equals(pattern[i]))
                                || (pattern[i].equals(connectorInfo[i]))
                        ) {
                            found = true;
                            continue;
                        } else {
                            found = false;
                            break;
                        }
                    }
                    if (found)
                        return connector;
                }
            }
            return null;
        }

        protected boolean isAllStarsPattern(String pattern) {
            return Pattern.matches("[*:]+", pattern);
        }

        protected String[] getConnectorInfo(SelectorConfig config, OutConnector connector) {
            List<String> info = new ArrayList<>();
            for (String property : config.properties) {
                info.add(connector.getProperty(property));
            }
            return info.toArray(new String[]{});
        }

        protected SelectorConfig createConfigForPreferredConnector(Object descriptor) throws Exception {

            JsonObject preferredConnectorDescriptor = JSONUtils.getJsonRootObject(descriptor.toString());
            SelectorConfig result = new SelectorConfig();
            List<String> patternItems = new ArrayList<>();
            for (Entry<String, JsonElement> entry : preferredConnectorDescriptor.entrySet()) {
                if (!entry.getValue().isJsonPrimitive())
                    continue;
                result.properties.add(entry.getKey());
                patternItems.add(entry.getValue().getAsJsonPrimitive().getAsString());
            }
            String pattern = patternItems.stream().collect(Collectors.joining(":"));
            if (!CommonUtils.isValid(pattern))
                return null;
            result.patterns.add(pattern);
            result.failBackToLLegacyRouter = false;
            result.useDefaultConnector = false;
            result.useTheOnlyConnector = false;
            result.connectorPreferredAsMandatory = true;
            result.allowAllStarsPattern = false;
            result.treatNoPropertyAsAny = false;

            return result;
        }

        private Boolean getConnectorPreferredAsMandatory(Exchange exchange) {
            Object connectorPreferredAsMandatory = exchange.getIn().getHeader(PROP_CONNECTOR_PREFERRED_AS_MANDATORY);
            return (connectorPreferredAsMandatory == null ? null : (Boolean) connectorPreferredAsMandatory);
        }

        private void setConnectorPreferredAsNotMandatory(Exchange exchange, SelectorConfig selectorConfig) {
            if (exchange.getIn().getHeader(PROP_CONNECTOR_PREFERRED_AS_MANDATORY) != null)
                exchange.getIn().getHeaders().put(PROP_CONNECTOR_PREFERRED_AS_MANDATORY, selectorConfig.connectorPreferredAsMandatory);
        }
    }


    protected static class SelectorConfig {

        public interface FIELDS {
            String PROPERTIES = "properties";
            String PATTERNS = "patterns";
            String OPTIONS = "options";
            String OPTION_FAIL_BACK_TO_LEGACY = "fail-back-to-legacy-router";
            String OPTION_USE_THE_ONLY_CONNECTOR = "use-the-only-connector";
            String CONNECTOR_PREFERRED_AS_MANDATORY = "connector-preferred-as-mandatory";
            String OPTION_USE_DEFAULT_CONNECTOR = "use-default-connector";
            String OPTION_ALLOW_ALL_STARS_PATTERN = "allow-all-stars-pattern";
            String OPTION_TREAT_NO_PROPERTY_AS_ANY = "treat-no-property-as-any";
        }


        List<String> properties;
        List<String> patterns;

        boolean failBackToLLegacyRouter = true;
        boolean useTheOnlyConnector = true;
        Boolean connectorPreferredAsMandatory = null;
        boolean useDefaultConnector = true;
        boolean allowAllStarsPattern = true;
        boolean treatNoPropertyAsAny = true;

        public SelectorConfig() {
            super();
            properties = new ArrayList<>();
            patterns = new ArrayList<>();
        }

        public SelectorConfig(String jsonData) {
            JsonObject config = JSONUtils.getJsonRootObject(jsonData);
            Gson gson = new Gson();
            Type listType = new TypeToken<ArrayList<String>>() {
            }.getType();
            if (config.has(FIELDS.PROPERTIES))
                try {
                    properties = gson.fromJson(config.get(FIELDS.PROPERTIES), listType);
                } catch (Exception e) {
                    // NOOP
                }
            if (config.has(FIELDS.PATTERNS))
                try {
                    patterns = gson.fromJson(config.get(FIELDS.PATTERNS), listType);
                } catch (Exception e) {
                    // NOOP
                }

            if (config.has(FIELDS.OPTIONS)) {

                JsonElement je = config.get(FIELDS.OPTIONS);
                if (je.isJsonObject()) {

                    JsonObject options = je.getAsJsonObject();

                    if (options.has(FIELDS.OPTION_FAIL_BACK_TO_LEGACY))
                        try {
                            failBackToLLegacyRouter = options.get(FIELDS.OPTION_FAIL_BACK_TO_LEGACY).getAsBoolean();
                        } catch (Exception e) {
                            // NOOP
                        }
                    if (options.has(FIELDS.OPTION_USE_DEFAULT_CONNECTOR))
                        try {
                            useDefaultConnector = options.get(FIELDS.OPTION_USE_DEFAULT_CONNECTOR).getAsBoolean();
                        } catch (Exception e) {
                            // NOOP
                        }
                    if (options.has(FIELDS.OPTION_USE_THE_ONLY_CONNECTOR))
                        try {
                            useTheOnlyConnector = options.get(FIELDS.OPTION_USE_THE_ONLY_CONNECTOR).getAsBoolean();
                        } catch (Exception e) {
                            // NOOP
                        }
                    if (options.has(FIELDS.CONNECTOR_PREFERRED_AS_MANDATORY))
                        try {
                            connectorPreferredAsMandatory = options.get(FIELDS.CONNECTOR_PREFERRED_AS_MANDATORY).getAsBoolean();
                        } catch (Exception e) {
                            // NOOP
                        }
                    if (options.has(FIELDS.OPTION_ALLOW_ALL_STARS_PATTERN))
                        try {
                            allowAllStarsPattern = options.get(FIELDS.OPTION_ALLOW_ALL_STARS_PATTERN).getAsBoolean();
                        } catch (Exception e) {
                            // NOOP
                        }
                    if (options.has(FIELDS.OPTION_TREAT_NO_PROPERTY_AS_ANY))
                        try {
                            treatNoPropertyAsAny = options.get(FIELDS.OPTION_TREAT_NO_PROPERTY_AS_ANY).getAsBoolean();
                        } catch (Exception e) {
                            // NOOP
                        }
                }
            }

        }
    }
}
