/*-
 * ~~~~~~licensing~~~~~~
 * object-factory
 * ==========
 * 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.tracker;

import java.util.ArrayList;
import java.util.Dictionary;
import java.util.HashMap;
import java.util.Hashtable;
import java.util.List;
import java.util.Map;
import java.util.stream.Collectors;

import org.osgi.framework.BundleContext;
import org.osgi.service.component.ComponentContext;
import org.osgi.service.component.annotations.Activate;
import org.osgi.service.component.annotations.Component;
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.objects.factory.EntaxyFactoryUtils;
import ru.entaxy.platform.base.objects.factory.EntaxyFactoryUtils.FactoryConfigurationStorage;
import ru.entaxy.platform.base.objects.factory.impl.DefaultFactory;
import ru.entaxy.platform.base.support.CommonUtils;
import ru.entaxy.platform.base.support.JSONUtils;
import ru.entaxy.platform.base.support.osgi.tracker.BundleTrackerCustomizerListener;

@Component(service = TrackedFactoryManager.class, immediate = true)
public class TrackedFactoryManager
        implements BundleTrackerCustomizerListener<TrackedFactoryContainer>, FactoryConfigurationStorage {

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

    public static final String DEFAULT_PARENT = "base-object";

    protected BundleContext bundleContext;

    protected Map<String, TrackedManagedFactory> managedFactories = new HashMap<>();

    protected Object managedFactoriesLock = new Object();

    @Activate
    public void activate(ComponentContext componentContext) {
        this.bundleContext = componentContext.getBundleContext();
    }

    @Override
    public void added(TrackedFactoryContainer managedObject) {
        if (managedObject == null) {
            log.debug("managedObject is null");
            return;
        }

        // List<TrackedManagedFactory> processedFactories = new ArrayList<>();

        synchronized (managedFactoriesLock) {

            FactoryProcessor factoryProcessor = new FactoryProcessor();

            for (TrackedFactory tf : managedObject.getFactoryList()) {
                log.info("Added factory: " + tf.getId());

                TrackedManagedFactory tmf;

                try {
                    tmf = new TrackedManagedFactory(tf);
                } catch (Exception e) {
                    log.error("Error creating tracked factory: [" + tf.getId() + "]", e);
                    continue;
                }

                // if we already have factory with the same id
                if (managedFactories.containsKey(tmf.factoryId)) {
                    tmf = managedFactories.get(tmf.factoryId);

                    // remove old service
                    tmf.deactivate();

                    try {
                        tmf.reload(tf);
                    } catch (Exception e) {
                        log.error("Error processing tracked factory: [" + tf.getId() + "]", e);
                        continue;
                    }

                    tmf.detachParent();

                    tmf.detachRequirements();

                } else {
                    this.managedFactories.put(tmf.factoryId, tmf);
                }

                if (!CommonUtils.isValid(tmf.parent) && !DEFAULT_PARENT.equalsIgnoreCase(tmf.factoryId)) {
                    tmf.setParent(DEFAULT_PARENT);
                }

                if (CommonUtils.isValid(tmf.parent)) {
                    TrackedManagedFactory parentF = managedFactories.get(tmf.parent);
                    if ((parentF != null) && parentF.isActive) {
                        tmf.attachParent(parentF);
                    } else {
                        tmf.addWaiting(tmf.parent);
                    }

                }

                for (String requirement : tmf.requirements) {
                    TrackedManagedFactory req = managedFactories.get(requirement);
                    if (req != null)
                        tmf.attachRequirement(req);
                    else
                        tmf.addWaiting(requirement);
                }

                if (tmf.isConsistent())
                    factoryProcessor.add(tmf);



                // processedFactories.add(tmf);

            }

            factoryProcessor.process();

        }

    }

    @Override
    public void modified(TrackedFactoryContainer managedObject) {
        if (managedObject == null)
            return;
        removed(managedObject);
        managedObject.reload();
        added(managedObject);
    }

    @Override
    public void removed(TrackedFactoryContainer managedObject) {
        if (managedObject == null)
            return;

        synchronized (managedFactoriesLock) {
            for (TrackedFactory tf : managedObject.getFactoryList()) {
                try {
                    tf.getServiceRegistration().unregister();
                } catch (Exception e) {
                    // do nothing
                }
                TrackedFactory.trackedFactoriesMap.remove(tf.getId());
            }
        }

    }

    // implement FactoryConfigurationStorage
    public JsonObject getConfiguration(String factoryId) {
        TrackedManagedFactory tmf = managedFactories.get(factoryId);
        if (tmf == null)
            return null;
        if (!tmf.isConsistent() || !tmf.isUpToDate)
            return null;
        return tmf.jsonEffective;
    };

    public List<TrackedManagedFactory> getManagedFactories() {
        return new ArrayList<>(this.managedFactories.values());
    }



    protected class FactoryProcessor {

        List<TrackedManagedFactory> factories = new ArrayList<>();

        public void add(TrackedManagedFactory tmf) {
            synchronized (factories) {
                if (!factories.contains(tmf))
                    factories.add(tmf);
            }
        }

        public void process() {
            while (!this.factories.isEmpty()) {

                List<TrackedManagedFactory> processed = new ArrayList<>();
                List<TrackedManagedFactory> toProcess = new ArrayList<>();

                for (TrackedManagedFactory currentFactory : factories) {
                    processFactory(currentFactory);
                    if (currentFactory.isActive) {
                        processed.add(currentFactory);

                        for (TrackedManagedFactory otherFactory : managedFactories.values()) {

                            // process waiting factories
                            if (otherFactory.isWaiting(currentFactory.factoryId)) {
                                otherFactory.stopWaiting(currentFactory.factoryId);
                                if (otherFactory.isConsistent())
                                    toProcess.add(otherFactory);
                            } else {
                                // cascade process consistent child factories
                                if (otherFactory.parentFactory == currentFactory) {
                                    if (otherFactory.isConsistent())
                                        toProcess.add(otherFactory);
                                }
                            }


                        }
                    }
                }

                if (processed.isEmpty())
                    break;

                for (TrackedManagedFactory tmf : toProcess)
                    if (!this.factories.contains(tmf))
                        this.factories.add(tmf);

                for (TrackedManagedFactory tmf : processed)
                    this.factories.remove(tmf);

            }
        }

        protected void processFactory(TrackedManagedFactory factory) {
            if (factory.isConsistent()) {

                if (CommonUtils.isValid(factory.parent)) {
                    TrackedManagedFactory parentF = managedFactories.get(factory.parent);
                    factory.attachParent(parentF);
                }

                for (String requirement : factory.requirements) {
                    TrackedManagedFactory req = managedFactories.get(requirement);
                    factory.attachRequirement(req);
                }


                JsonObject effective =
                        EntaxyFactoryUtils.calculateEffectiveJson(factory.jsonOrigin, TrackedFactoryManager.this);
                if (!effective.has("#CALC_ERROR")) {

                    factory.jsonEffective = effective.deepCopy();
                    JsonObject jsonFinal = EntaxyFactoryUtils.resolveVariants(effective);

                    // create factory
                    factory.updateConfiguration(jsonFinal);
                    if (createFactory(factory)) {
                        factory.activate();
                    }

                } else {
                    JsonObject jo = effective.get("#CALC_ERROR").getAsJsonObject();
                    if (jo.has("#REQS")) {
                        JsonArray ja = jo.get("#REQS").getAsJsonArray();

                        final TrackedManagedFactory tmfF = factory;
                        ja.forEach(item -> tmfF.addRequirement(item.getAsString()));
                        ja.forEach(item -> tmfF.addWaiting(item.getAsString()));
                        log.info("Factory {} is inconsistent, waiting for: {}", factory.factoryId,
                                factory.waitingFor.stream().collect(Collectors.joining(",")));
                    }
                }
            } else {
                log.info("Factory {} is inconsistent, waiting for: {}", factory.factoryId,
                        factory.waitingFor.stream().collect(Collectors.joining(",")));
            }

        }

        public boolean createFactory(TrackedManagedFactory factory) {
            DefaultFactory defaultFactory = new DefaultFactory();
            defaultFactory.setFactoryId(factory.factoryId);
            defaultFactory.configure(EntaxyFactoryUtils.cleanJson(factory.jsonFinal));
            if (defaultFactory.isValid()) {

                // to ensure id is correct
                factory.trackedFactory.setId(defaultFactory.getId());

                Dictionary<String, String> props = new Hashtable<String, String>();
                props.put(EntaxyFactory.SERVICE.PROP_ID, defaultFactory.getId());
                props.put(EntaxyFactory.SERVICE.PROP_TYPE, defaultFactory.getType());
                props.put(EntaxyFactory.SERVICE.PROP_ORIGIN_BUNDLE,
                        factory.trackedFactory.getBundle().getBundleId() + "");

                factory.trackedFactory.setServiceRegistration(
                        TrackedFactoryManager.this.bundleContext.registerService(EntaxyFactory.class, defaultFactory,
                                props));

                return true;
            }
            return false;
        }


    }

    public static class TrackedManagedFactory {

        TrackedFactory trackedFactory;

        // original JSON description
        JsonObject jsonOrigin;
        // JSON with inheritance & imports resolved
        JsonObject jsonEffective;
        // JSON with VARIANTS resolved
        public JsonObject jsonFinal;

        JsonObject jsonFactorySection = null;

        public String factoryId = null;
        public String parent = null;

        public List<String> requirements = new ArrayList<>();

        TrackedManagedFactory parentFactory = null;
        List<TrackedManagedFactory> requiredFactories = new ArrayList<>();
        List<TrackedManagedFactory> affectedFactories = new ArrayList<>();

        public List<String> waitingFor = new ArrayList<>();

        public boolean isUpToDate = false;

        public boolean isActive = false;

        public TrackedManagedFactory(TrackedFactory factory) throws Exception {
            reload(factory);
        }

        public void reload(TrackedFactory factory) throws Exception {
            this.jsonFactorySection = null;
            this.factoryId = null;
            this.parent = null;
            this.requirements = new ArrayList<>();
            this.trackedFactory = factory;
            this.waitingFor.clear();
            this.jsonOrigin = JSONUtils.getJsonRootObjectUnsafe(this.trackedFactory.getConfigString());
            if (this.jsonOrigin.has(EntaxyFactory.CONFIGURATION.FACTORY_SECTION_NAME)) {
                this.jsonFactorySection =
                        this.jsonOrigin.get(EntaxyFactory.CONFIGURATION.FACTORY_SECTION_NAME).getAsJsonObject();
                if (this.jsonFactorySection.has(EntaxyFactory.CONFIGURATION.FACTORY.ID))
                    this.factoryId = this.jsonFactorySection.get(EntaxyFactory.CONFIGURATION.FACTORY.ID).getAsString();
                if (this.jsonFactorySection.has(EntaxyFactory.CONFIGURATION.FACTORY.PARENT))
                    this.parent = this.jsonFactorySection.get(EntaxyFactory.CONFIGURATION.FACTORY.PARENT).getAsString();
                if (this.jsonFactorySection.has(EntaxyFactory.CONFIGURATION.FACTORY.REQUIRES)) {
                    JsonElement je = this.jsonFactorySection.get(EntaxyFactory.CONFIGURATION.FACTORY.REQUIRES);
                    if (je.isJsonArray()) {
                        JsonArray ja = je.getAsJsonArray();
                        ja.forEach(item -> this.requirements.add(item.getAsString()));
                    }
                    if (je.isJsonPrimitive()) {
                        this.requirements.add(je.getAsString());
                    }
                }
            }
        }

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

        public void setParent(String parentValue) {
            this.parent = parentValue;
            JsonObject fs = this.jsonOrigin.get(EntaxyFactory.CONFIGURATION.FACTORY_SECTION_NAME).getAsJsonObject();
            fs.remove(EntaxyFactory.CONFIGURATION.FACTORY.PARENT);
            fs.addProperty(EntaxyFactory.CONFIGURATION.FACTORY.PARENT, parentValue);
        }

        public void attachParent(TrackedManagedFactory parentTmf) {
            if (this.parentFactory != parentTmf)
                detachParent();
            this.parentFactory = parentTmf;
            parentTmf.attachAffected(this);
            this.isUpToDate = false;
        }

        public void detachParent() {
            if (this.parentFactory != null) {
                this.parentFactory.detachAffected(this);
                this.parentFactory = null;
            }
        }

        public void addRequirement(String req) {
            if (!this.requirements.contains(req))
                this.requirements.add(req);
        }

        public void attachRequirement(TrackedManagedFactory requiredTmf) {
            if (!this.requiredFactories.contains(requiredTmf))
                this.requiredFactories.add(requiredTmf);
            requiredTmf.attachAffected(this);
        }

        public void detachRequirements() {
            for (TrackedManagedFactory tmf : requiredFactories)
                tmf.detachAffected(this);
            this.requiredFactories.clear();
        }

        public void attachAffected(TrackedManagedFactory affectedTmf) {
            if (!this.affectedFactories.contains(affectedTmf))
                this.affectedFactories.add(affectedTmf);
        }

        public void detachAffected(TrackedManagedFactory affectedTmf) {
            this.affectedFactories.remove(affectedTmf);
        }

        public void addWaiting(String waitFactoryId) {
            if (!this.waitingFor.contains(waitFactoryId))
                this.waitingFor.add(waitFactoryId);
        }

        public boolean isWaiting(String waitFactoryId) {
            return this.waitingFor.contains(waitFactoryId);
        }

        public void stopWaiting(String waitFactoryId) {
            this.waitingFor.remove(waitFactoryId);
        }

        public boolean isConsistent() {
            return this.waitingFor.isEmpty();
        }

        public void updateConfiguration(JsonObject jsonObject) {
            this.jsonFinal = jsonObject.deepCopy();
            this.isUpToDate = true;
        }

        public void activate() {
            this.isActive = true;
        }

        public void deactivate() {
            this.trackedFactory.unregister();
            this.isActive = false;
        }

    }

}
