/*-
 * ~~~~~~licensing~~~~~~
 * artifact-management
 * ==========
 * 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.artifact.installer.builder.impl.cluster;

import java.util.ArrayList;
import java.util.HashMap;
import java.util.LinkedList;
import java.util.List;
import java.util.Map;
import java.util.stream.Collectors;

import javax.management.openmbean.CompositeData;
import javax.management.openmbean.TabularData;

import org.apache.karaf.cellar.bundle.BundleState;
import org.apache.karaf.cellar.bundle.management.CellarBundleMBean;
import org.apache.karaf.cellar.core.ClusterManager;
import org.apache.karaf.cellar.core.Configurations;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import ru.entaxy.esb.system.common.exception.BundleNotFound;
import ru.entaxy.platform.base.support.CommonUtils;
import ru.entaxy.platform.base.support.osgi.OSGIUtils;
import ru.entaxy.platform.core.artifact.installer.builder.InstallationResult;
import ru.entaxy.platform.core.artifact.installer.builder.impl.InstallationResultImpl;
import ru.entaxy.platform.core.artifact.installer.builder.typed.impl.CommonBundleInstallerHelper;
import ru.entaxy.platform.core.artifact.installer.builder.typed.impl.CommonBundleInstallerHelperConfig;

public class ClusterCommonBundleInstallerHelper extends ClusterTypedInstallerHelper
        implements CommonBundleInstallerHelper {

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

    protected CommonBundleInstallerHelperConfig config;

    protected ClassLoader backup;

    @Override
    public void setConfig(CommonBundleInstallerHelperConfig config) {
        this.config = config;
    }

    protected void replaceClassLoader() {
        this.backup = Thread.currentThread().getContextClassLoader();
        Thread.currentThread().setContextClassLoader(this.getClass().getClassLoader());
    }

    protected void restoreClassLoader() {
        if (this.backup != null) {
            Thread.currentThread().setContextClassLoader(this.backup);
            this.backup = null;
        }
    }

    @Override
    public InstallationResultImpl install(InstallationResultImpl installationResult) {
        // ClusterTypedInstallerHelper _helper = (ClusterTypedInstallerHelper)helper;

        // InstallationResultImpl installationResult = InstallationResultImpl
        // .create()
        // .property("artifact", artifact);

        CompositeData existingBundle = null;

        List<String> groupsToInstall = new ArrayList<>();
        for (String groupName : getGroups())
            if (checkGroup(groupName))
                if (!groupsToInstall.contains(groupName))
                    groupsToInstall.add(groupName);

        if (groupsToInstall.size() == 0) {
            return installationResult.failed("No groups to install to");
        }

        replaceClassLoader();

        CellarBundleMBean bundleService = null;
        ClusterManager clusterManager = null;
        try {
            bundleService = OSGIUtils.services().ofClass(CellarBundleMBean.class).waitService(20000).get();
            clusterManager = OSGIUtils.services().ofClass(ClusterManager.class).waitService(20000).get();
        } catch (Exception e) {
            log.trace(e.getMessage());
        }

        if ((bundleService == null) || (clusterManager == null)) {
            // TODO throw exception
            return installationResult
                    .failed("Can't install/update: CellarBundleMBean or ClusterManager not available");
        }

        // TODO decide if we really need several groups
        // now we use only the first one
        String groupToInstall = groupsToInstall.get(0);

        if (this.config.isUpdate() || this.config.isUpdateOnly() || this.config.isInstallOnlyIfMissing()
                || this.config.isStartBundleOnly() || this.config.isStopBundleOnly()) {


            String bundleId = this.config.getBundleId();

            if (!CommonUtils.isValid(bundleId)) {
                // TODO throw exception
                return installationResult
                        .failed("Can't update: bundleId can not be defined");
            }

            // if not bundles found BundleService throws exception
            try {
                existingBundle = getBundleData(bundleId, bundleService, groupToInstall);
            } catch (Exception e) {
                // NOOP
            }

            Map<String, List<BundleState>> foundBundlesMap = new HashMap<>();

            for (String s : groupsToInstall) {
                Map<String, BundleState> clusterBundles =
                        clusterManager.getMap(org.apache.karaf.cellar.bundle.Constants.BUNDLE_MAP
                                + Configurations.SEPARATOR
                                + s);
                List<BundleState> foundClusterBundles = clusterBundles.values().stream()
                        .filter(b -> b.getSymbolicName().equals(bundleId))
                        .collect(Collectors.toList());
                if (foundClusterBundles.size() > 0)
                    foundBundlesMap.put(s, foundClusterBundles);
            }

            if ((existingBundle != null) && foundBundlesMap.isEmpty()) {
                if (this.config.isForceInstall()) {
                    log.warn("Bundle [{}] is found locally only but cluster installation is forced", bundleId);
                } else {
                    // bundle is only installed locally but expected to be in cluster
                    restoreClassLoader();
                    return installationResult.failed("Bundle ["
                            .concat(bundleId).concat("]")
                            .concat(" is installed locally only; cluster state must be restored"));
                }
            }

            if (existingBundle != null && !foundBundlesMap.isEmpty()) {

                installationResult.object(existingBundle);

                if (this.config.isInstallOnlyIfMissing()) {

                    installationResult.notChanged();

                } else

                if (this.config.isUpdate() || this.config.isUpdateOnly()) {

                    try {
                        if (this.config.isStartBundle())
                            bundleService.updateStart(groupToInstall, bundleId, this.config.getBundleLocation());
                        else
                            bundleService.update(groupToInstall, bundleId, this.config.getBundleLocation());

                        installationResult.updated();
                    } catch (Exception e) {
                        installationResult.failed().error(e);
                    }
                } else

                if (this.config.isStartBundleOnly()) {

                    try {
                        bundleService.start(groupToInstall, bundleId);
                        installationResult.started()
                                .property(InstallationResult.PROP_BUNDLE_STATUS,
                                        getStatusByName(bundleId, bundleService, groupToInstall));
                    } catch (Exception e) {
                        // TODO Auto-generated catch block
                        installationResult.failed().error(e);
                    }

                } else

                if (this.config.isStopBundleOnly()) {

                    try {
                        bundleService.stop(groupToInstall, bundleId);
                        installationResult.stopped()
                                .property(InstallationResult.PROP_BUNDLE_STATUS,
                                        getStatusByName(bundleId, bundleService, groupToInstall));
                    } catch (Exception e) {
                        // TODO Auto-generated catch block
                        installationResult.failed().error(e);
                    }

                }

                restoreClassLoader();

                return installationResult;
            }
        }

        if (this.config.isUpdateOnly()) {
            return installationResult.failed("UpdateOnly failed: bundle to update not found");
        }

        try {
            if (this.config.getStartLevel() > 0) {
                bundleService.install(groupToInstall, this.config.getBundleLocation(), this.config.getStartLevel(),
                        this.config.isStartBundle());
            } else {
                bundleService.install(groupToInstall, this.config.getBundleLocation(), this.config.isStartBundle());
            }
            installationResult.installed()
                    .property(InstallationResult.PROP_BUNDLE_STATUS,
                            getStatusByLocation(this.config.getBundleLocation(), bundleService, groupToInstall));
        } catch (Exception e) {
            installationResult.error(e)
                    .failed("Installing bundle failed: Exception");
        }

        replaceClassLoader();

        return installationResult.installed();
    }

    @Override
    public InstallationResultImpl uninstall(InstallationResultImpl installationResult) {
        CompositeData existingBundle = null;

        List<String> groupsToInstall = new ArrayList<>();
        for (String groupName : getGroups())
            if (checkGroup(groupName))
                if (!groupsToInstall.contains(groupName))
                    groupsToInstall.add(groupName);

        if (groupsToInstall.size() == 0) {
            return installationResult.failed("No groups to install to");
        }

        CellarBundleMBean bundleService = OSGIUtils.getService(CellarBundleMBean.class);

        if (bundleService == null) {
            // TODO throw exception
            return installationResult
                    .failed("Can't install/update: CellarBundleMBean not available");
        }

        // TODO decide if we really need several groups
        // now we use only the first one
        String groupToInstall = groupsToInstall.get(0);
        String bundleId = this.config.getBundleId();

        if (!CommonUtils.isValid(bundleId)) {
            // TODO throw exception
            return installationResult
                    .failed("Can't update: bundleId can not be defined");
        }

        // if not bundles found BundleService throws exception
        try {
            existingBundle = getBundleData(bundleId, bundleService, groupToInstall);
        } catch (Exception e) {
            // NOOP
        }

        if (existingBundle != null) {
            try {
                bundleService.uninstall(groupToInstall, bundleId);
                installationResult.uninstalled();
            } catch (Exception e) {
                installationResult.failed("Failed uninstalling bundle [" + bundleId + "]").error(e);
            }
        } else {
            installationResult.notChanged().message("BUNDLE NOT FOUND");
        }
        return installationResult;
    }

    private String getStatusByLocation(String bundleLocation, CellarBundleMBean cellarBundleMBean, String cellarGroup)
            throws Exception {
        TabularData tabularData = cellarBundleMBean.getBundles(cellarGroup);
        String status = null;
        for (Object value : tabularData.values()) {
            CompositeData compositeData = (CompositeData) value;
            if (bundleLocation.equals(compositeData.get("location"))) {
                status = compositeData.get("status").toString();
            }
        }
        if (status == null) {
            throw new BundleNotFound();
        }
        return status.toUpperCase();
    }

    private String getStatusByName(String bundleName, CellarBundleMBean cellarBundleMBean, String cellarGroup)
            throws Exception {

        CompositeData data = getBundleData(bundleName, cellarBundleMBean, cellarGroup);
        if ((data != null) && (data.containsKey("status"))) {
            Object obj = data.get("status");
            return obj == null ? null : obj.toString();
        } ;
        return null;
    }


    private List<CompositeData> getBundlesData(String symbolicName, CellarBundleMBean cellarBundleMBean,
            String cellarGroup) throws Exception {
        TabularData tabularData = cellarBundleMBean.getBundles(cellarGroup);
        List<CompositeData> results = new LinkedList<>();
        for (Object value : tabularData.values()) {
            CompositeData compositeData = (CompositeData) value;
            if (symbolicName.equals(compositeData.get("name"))
                    || symbolicName.equals(compositeData.get("symbolic_name"))) {
                results.add(compositeData);
            }
        }
        return results;
    }

    private CompositeData getBundleData(String symbolicName, CellarBundleMBean cellarBundleMBean, String cellarGroup)
            throws Exception {
        List<CompositeData> results = getBundlesData(symbolicName, cellarBundleMBean, cellarGroup);
        if (results.size() == 0)
            return null;
        else
            return results.get(0);
    }

}
