/*-
 * ~~~~~~licensing~~~~~~
 * init-manager
 * ==========
 * 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.esb.platform.runtime.core.initializer;

import java.io.IOException;
import java.lang.reflect.Constructor;
import java.lang.reflect.InvocationTargetException;
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.Set;
import java.util.stream.Collectors;

import org.apache.camel.tooling.model.Strings;
import org.apache.felix.utils.properties.TypedProperties;
import org.apache.karaf.config.core.ConfigRepository;
import org.osgi.framework.Bundle;
import org.osgi.framework.BundleContext;
import org.osgi.framework.InvalidSyntaxException;
import org.osgi.framework.ServiceReference;
import org.osgi.framework.ServiceRegistration;
import org.osgi.framework.wiring.BundleWiring;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import ru.entaxy.esb.platform.runtime.core.initializer.api.Initialized;
import ru.entaxy.esb.platform.runtime.core.initializer.api.Initializer;
import ru.entaxy.esb.platform.runtime.core.initializer.api.InitializerException;

/**
 * 
 * Manager for initializers
 * 
 * TODO (if needed): 1. keep map "initializerClassName -> bundleId" 2. publish as a service 3.
 * provide interface with method "reinit(String className)"
 * 
 * 
 * @author sstarovoytenkov
 *
 */
public class InitManager {

    protected static final Logger log = LoggerFactory.getLogger(InitManager.class);

    public static final String PROP_SKIP_ALL = "skip.all";

    /**
     * pid of config to store initialization status (true/false) will be created on first start of
     * InitManager
     */
    protected final String pid = "ru.entaxy.esb.initializer";

    protected ConfigRepository configRepository;
    protected BundleContext bundleContext;

    // used when updating to avoid multiple extra initializations
    protected boolean skipAll = false;

    protected Map<String, Set<String>> initersToDeps = new HashMap<>();
    protected Map<String, Set<String>> depsToIniters = new HashMap<>();

    protected Map<String, InitializerMeta> initializers = new HashMap<>();

    protected List<String> waiting = new ArrayList<>();

    protected class InitializerMeta implements Initializer.Callback {

        String id;
        String className;
        boolean repeat;

        // Bundle bundle;
        long bundleId;

        BundleContext bundleContext;

        Map<String, InitializerMeta> dependsOn = new HashMap<>();
        Map<String, InitializerMeta> dependedBy = new HashMap<>();

        boolean executed = false;
        boolean toBeExecuted = true;

        int retries = 1;

        int interval = 1000;

        InitializedImpl initializedImpl;

        public void load(String initializerData) {
            String[] data = initializerData.split("\\?");

            this.className = data[0];
            String queryString = data.length > 1 ? data[1] : "";

            String[] queryStringData = queryString.split("&");
            Map<String, String> params = new HashMap<>();
            for (int i = 0; i < queryStringData.length; i++) {
                String[] paramData = queryStringData[i].split("=");
                params.put(paramData[0], paramData.length > 1 ? paramData[1] : "");
            }

            this.id = params.containsKey(Initializer.INITIALIZER_QUERY_STRING_PARAM_ID)
                    ? (Strings.isNullOrEmpty(params.get(Initializer.INITIALIZER_QUERY_STRING_PARAM_ID))
                            ? className
                            : params.get(Initializer.INITIALIZER_QUERY_STRING_PARAM_ID))
                    : className;

            String dependsOn = params.get(Initializer.INITIALIZER_QUERY_STRING_PARAM_DEPENDS_ON);
            if (!Strings.isNullOrEmpty(dependsOn)) {
                String[] dependencies = dependsOn.split(",");
                for (int i = 0; i < dependencies.length; i++) {
                    InitializerMeta meta = InitManager.this.getMetaById(dependencies[i]);
                    addDependency(meta);
                }
            }
            String repeat = params.get(Initializer.INITIALIZER_QUERY_STRING_PARAM_REPEAT);
            this.repeat = "true".equals(repeat);

            String retriesValue = params.get(Initializer.INITIALIZER_QUERY_STRING_PARAM_RETRIES);
            if (!Strings.isNullOrEmpty(retriesValue))
                try {
                    this.retries = Integer.parseInt(retriesValue);
                } catch (NumberFormatException e) {
                    log.error("Retries parameter [{}] is not an integer", retriesValue);
                }

            String intervalValue = params.get(Initializer.INITIALIZER_QUERY_STRING_PARAM_INTERVAL);
            if (!Strings.isNullOrEmpty(intervalValue))
                try {
                    this.interval = Integer.parseInt(intervalValue);
                } catch (NumberFormatException e) {
                    log.error("Interval parameter [{}] is not an integer", intervalValue);
                }
        }

        protected void addDependency(InitializerMeta meta) {
            synchronized (meta) {
                if (meta.toBeExecuted && !meta.executed)
                    synchronized (this.dependsOn) {
                        meta.addDependent(this);
                        this.dependsOn.put(meta.id, meta);
                    }
            }
        }

        public void load(InitializerMeta other) {
            this.className = other.className;
            this.bundleId = other.bundleId;
            this.repeat = other.repeat;
            this.retries = other.retries;
            this.interval = other.interval;
            this.bundleContext = other.bundleContext;
            for (InitializerMeta meta : other.dependsOn.values())
                this.addDependency(meta);
        }

        public void addDependent(InitializerMeta meta) {
            synchronized (this.dependedBy) {
                this.dependedBy.put(meta.id, meta);
            }
        }

        public boolean isExecutedBefore() {
            return InitManager.this.getValueById(id);
        }

        public boolean canExecute() {
            return this.dependsOn.isEmpty();
        }

        public void notifyDependent() {
            for (InitializerMeta meta : this.dependedBy.values())
                meta.dependencyReady(this);
        }

        public Bundle getBundle() {
            return bundleContext.getBundle(bundleId);
        }

        public boolean execute() {
            /**
             * instantiate initializer and call "init" if no exceptions, set status to "true"
             * 
             * @see <code>Initializer</code>
             */
            Bundle bundle = getBundle();
            BundleWiring wiring = bundle.adapt(BundleWiring.class);
            ClassLoader cl = wiring.getClassLoader();
            Class<?> clazz;
            try {
                if (!InitManager.this.skipAll) {
                    clazz = cl.loadClass(className);
                    log.info("Loaded class {}", clazz.getName());
                    Constructor<?> constructor = clazz.getConstructor();
                    Initializer initializer = (Initializer) constructor.newInstance();
                    initializer.setBundleContext(bundle.getBundleContext());
                    initializer.setInitializerId(id);
                    initializer.init();
                }
                InitManager.this.updateById(id, true);
                this.executed = true;
                this.notifyDependent();
                return true;
            } catch (ClassNotFoundException | NoSuchMethodException | SecurityException | InstantiationException
                    | IllegalAccessException | IllegalArgumentException | InvocationTargetException
                    | InitializerException e) {
                log.error("Initializer error: class=" + className
                        + ", id=" + id + " -> [" + e.getMessage() + "] :: "
                        + (e instanceof InitializerException ? ((InitializerException) e).getDescription() : ""), e);
                return false;
            }

        }

        public void registerService() {
            this.initializedImpl = new InitializedImpl(bundleContext, id);
            this.initializedImpl.register();
        }

        public void runInitializer() {
            Thread thread = new Thread(new Executor(this));
            thread.setContextClassLoader(Thread.currentThread().getContextClassLoader());
            thread.start();
        }

        public void setCallback() {
            BundleWiring wiring = getBundle().adapt(BundleWiring.class);
            ClassLoader cl = wiring.getClassLoader();
            Class<?> clazz;
            try {
                clazz = cl.loadClass(className);
                log.info("Loaded class {}", clazz.getName());
                Constructor<?> constructor = clazz.getConstructor();
                Initializer initializer = (Initializer) constructor.newInstance();
                initializer.setCallback(this);
            } catch (ClassNotFoundException | NoSuchMethodException | SecurityException | InstantiationException
                    | IllegalAccessException | IllegalArgumentException | InvocationTargetException e) {
                // TODO Auto-generated catch block
                e.printStackTrace();
            }
        }

        public void dependencyReady(InitializerMeta meta) {
            synchronized (this.dependsOn) {
                this.dependsOn.remove(meta.id);
                if (!this.executed && this.canExecute())
                    // this.execute();
                    this.runInitializer();
            }
        }

        @Override
        public void inited(Initializer owner) {
            log.info("Initializer with id {} notified of successful init via callback", id);
            InitManager.this.updateById(id, true);
            this.executed = true;
            this.notifyDependent();
            owner.setCallback(null);
        }

    };

    protected static class InitializedImpl implements Initialized {


        protected BundleContext bundleContext;

        protected String initializerId;

        protected ServiceRegistration<Initialized> serviceRegistration;

        public InitializedImpl(BundleContext bundleContext, String id) {
            this.bundleContext = bundleContext;
            this.initializerId = id;
        }

        public void register() {
            try {
                Dictionary<String, Object> props = new Hashtable<>();
                props.put(PROP_INITIALIZER_ID, initializerId);
                this.serviceRegistration = bundleContext.registerService(Initialized.class, this, props);
            } catch (Exception e) {
                log.error("Error registering service for initializer [" + initializerId + "]", e);
            }
        }

        public void unregister() {
            if (serviceRegistration != null)
                try {
                    serviceRegistration.unregister();
                } catch (Exception e) {
                    log.error("Error unregistering service for initializer [" + initializerId + "]", e);
                }
            serviceRegistration = null;
        }

        public boolean isRegistered() {
            return serviceRegistration != null;
        }
    }

    protected static class Executor implements Runnable {

        InitializerMeta meta;

        public Executor(InitializerMeta meta) {
            this.meta = meta;
        }

        @Override
        public void run() {

            /**
             * check the initialization status
             */
            if (meta.isExecutedBefore()) {
                log.info("Initializer with id {} in bundle [{}] {} is already inited", meta.id,
                        meta.bundleId, meta.getBundle().getSymbolicName());

                if (meta.repeat)
                    log.info("Initializer with id {} is set to be repeated", meta.id);
                else {
                    meta.toBeExecuted = false;
                    meta.notifyDependent();
                    meta.registerService();
                    return;
                }
            }

            if (!meta.canExecute()) {
                String dependsOn = meta.dependsOn.values().stream().map((m) -> m.id).collect(Collectors.joining(","));
                log.info("Initializer with id {} is waiting for dependencies: {}", meta.id, dependsOn);

                /*
                 * synchronized (this.waiting) { this.waiting.add(meta.id); }
                 */
                return;
            }
            int retriesCount = meta.retries;
            boolean result = false;
            while (!result && (retriesCount > 0)) {
                log.info("Executing initializer with id {}: try {} of {}", meta.id, meta.retries - retriesCount + 1,
                        meta.retries);
                retriesCount--;
                result = meta.execute();

                if (result)
                    meta.registerService();

                if ((retriesCount > 0) && !result)
                    try {
                        log.info("Executing initializer with id {}: sleeping for {}", meta.id, meta.interval);
                        Thread.sleep(meta.interval);
                    } catch (InterruptedException e) {
                        log.error("Failed sleep for thread {} on initializer {}", Thread.currentThread().getId(),
                                meta.id);
                        e.printStackTrace();
                    }
            }

            // last try
            if (!result)
                meta.setCallback();


        }

    }

    protected InitializerMeta getMetaById(String id) {
        synchronized (initializers) {
            if (!initializers.containsKey(id)) {
                InitializerMeta meta = new InitializerMeta();
                meta.id = id;
                initializers.put(id, meta);
            }
            return initializers.get(id);
        }
    }

    public InitManager(BundleContext context) {
        this.bundleContext = context;

        ServiceReference<ConfigRepository> ref1 = this.bundleContext.getServiceReference(ConfigRepository.class);
        this.configRepository = this.bundleContext.getService(ref1);

        if (!getValueById(this.getClass().getName())) {
            updateById(this.getClass().getName(), true);
        }

        this.skipAll = getValueById(PROP_SKIP_ALL);

    }

    protected InitializerMeta createMeta(String initializerMeta, Bundle bundle) {
        InitializerMeta meta = new InitializerMeta();
        meta.load(initializerMeta);
        meta.bundleId = bundle.getBundleId();
        meta.bundleContext = bundleContext;

        synchronized (this.initializers) {
            if (this.initializers.containsKey(meta.id)) {
                this.initializers.get(meta.id).load(meta);
            } else {
                this.initializers.put(meta.id, meta);
            }
        }

        return this.initializers.get(meta.id);

    }

    /**
     * 
     * @param initializerClass class name read by tracker from Entaxy-Initializer-Class manifest
     *        header
     * @param bundle
     */
    public void init(String initializerClass, Bundle bundle) {
        log.info("Found initializer of class {} in bundle [{}] {}", initializerClass,
                bundle.getBundleId(), bundle.getSymbolicName());

        InitializerMeta meta = createMeta(initializerClass, bundle);

        meta.runInitializer();

        // Thread thread = new Thread(new Executor(meta));
        // thread.setContextClassLoader(Thread.currentThread().getContextClassLoader());
        // thread.start();

    }

    /*
     * protected void execute(InitializerMeta meta) { if (meta.execute()) synchronized
     * (this.waiting) { this.waiting.remove(meta.id); } for (String id: this.waiting) {
     * InitializerMeta waits = getMetaById(id); synchronized (waits) { if (waits.canExecute()) } } }
     */
    /**
     * 
     * @param initializerId class name to check
     * @return true if the initializer is already completed, false otherwise
     */
    protected boolean getValueById(String initializerId) {
        try {
            TypedProperties tp = this.configRepository.getConfig(pid);
            if (!tp.containsKey(initializerId))
                return false;
            return "true".equals(tp.get(initializerId).toString());
        } catch (IOException | InvalidSyntaxException e) {
            log.warn("Can't get property " + initializerId, e);
        }
        return false;
    }

    protected void updateById(String initializerId, boolean value) {
        log.info("Updating by id {}", initializerId);
        try {
            TypedProperties tp = this.configRepository.getConfig(pid);
            tp.put(initializerId, value ? "true" : "false");
            this.configRepository.update(pid, tp);
        } catch (IOException | InvalidSyntaxException e) {
            log.warn("Can't update property " + initializerId, e);
        }
    }

}
