/*-
 * ~~~~~~licensing~~~~~~
 * application-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.integration.applications.management.impl;

import java.io.File;
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.StandardCopyOption;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Base64;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Optional;
import java.util.stream.Collectors;

import javax.management.DynamicMBean;
import javax.management.MBeanRegistration;
import javax.management.NotCompliantMBeanException;

import org.apache.commons.io.FileUtils;
import org.apache.karaf.features.FeaturesService;
import org.osgi.service.component.annotations.Component;
import org.osgi.service.component.annotations.Reference;
import org.osgi.service.component.annotations.ReferenceCardinality;
import org.osgi.service.component.annotations.ServiceScope;

import ru.entaxy.esb.platform.base.management.core.ManagementCore;
import ru.entaxy.esb.platform.base.management.core.api.AnnotatedMBean;
import ru.entaxy.esb.resources.EntaxyResource;
import ru.entaxy.esb.resources.EntaxyResourceService;
import ru.entaxy.platform.base.support.CommonUtils;
import ru.entaxy.platform.base.support.JSONUtils;
import ru.entaxy.platform.base.support.osgi.bundle.CapabilityDescriptor;
import ru.entaxy.platform.base.support.osgi.feature.FeatureCapabilityHelper;
import ru.entaxy.platform.config.runtime.ObjectConfigEditor;
import ru.entaxy.platform.integration.applications.ApplicationItem;
import ru.entaxy.platform.integration.applications.ApplicationManager;
import ru.entaxy.platform.integration.applications.EntaxyApplication;
import ru.entaxy.platform.integration.applications.EntaxyApplicationRevision;
import ru.entaxy.platform.integration.applications.EntaxyApplicationVersion;
import ru.entaxy.platform.integration.applications.descriptor.ApplicationRequirement;
import ru.entaxy.platform.integration.applications.exceptions.ApplicationException;
import ru.entaxy.platform.integration.applications.exceptions.ApplicationVersionHasInstalledException;
import ru.entaxy.platform.integration.applications.exceptions.ItemNotFoundException;
import ru.entaxy.platform.integration.applications.exceptions.StorageException;
import ru.entaxy.platform.integration.applications.management.ApplicationsMBean;
import ru.entaxy.platform.objects.runtime.EntaxyRuntimeObject;
import ru.entaxy.platform.objects.runtime.EntaxyRuntimeObjectService;

@Component(
        service = {DynamicMBean.class, MBeanRegistration.class, ApplicationsMBean.class},
        property = {ManagementCore.JMX_OBJECTNAME + "=" + ApplicationsMBean.Q_MAIN_MODULE_S},
        scope = ServiceScope.SINGLETON,
        immediate = true
)
public class ApplicationsMBeanImpl extends AnnotatedMBean<ApplicationsMBean> implements ApplicationsMBean {

    public static final String EXPRESSION_PREFIX = "${";
    public static final String EXPRESSION_PREFIX_WITH_UNDERLINES = "$__{";

    @Reference(cardinality = ReferenceCardinality.MANDATORY)
    public ApplicationManager applicationManager;

    @Reference(cardinality = ReferenceCardinality.MANDATORY)
    EntaxyRuntimeObjectService entaxyRuntimeObjectService;

    @Reference(cardinality = ReferenceCardinality.MANDATORY)
    EntaxyResourceService entaxyResourceService;

    public ApplicationsMBeanImpl() throws NotCompliantMBeanException {
        super(ApplicationsMBean.class);
    }

    @Override
    public String readAsTree() {
        final String TITLE = "title";
        final String CHILDREN = "children";

        List<Map<String, Object>> applicationStorages = new ArrayList<>();
        applicationManager.getActiveStorages().forEach(storage -> {

            Map<String, Object> applicationStorage = new HashMap<>();
            applicationStorage.put(TITLE, storage.getName());
            applicationStorage.put(CHILDREN, storage.getApplications().stream().map(application -> {
                Map<String, Object> applicationMap = new HashMap<>();
                applicationMap.put(TITLE, application.getName());
                applicationMap.put(CHILDREN, application.getVersions().stream().map(version -> {
                    Map<String, Object> versionMap = new HashMap<>();
                    versionMap.put(TITLE, version.getVersionNumber());
                    versionMap.put(CHILDREN, version.getRevisions().stream().map(revision -> {
                        Map<String, Object> revisionMap = new HashMap<>();
                        String revisionTitle = revision.getRevisionNumber() +
                                " [" + revision.getStatus().toString() + "]";
                        revisionMap.put(TITLE, revisionTitle);
                        return revisionMap;
                    }).collect(Collectors.toList()));
                    return versionMap;
                }).collect(Collectors.toList()));
                return applicationMap;
            }).collect(Collectors.toList()));

            applicationStorages.add(applicationStorage);
        });

        return JSONUtils.GSON.toJson(applicationStorages);
    }

    @Override
    public void importFromRepository(String storage, String mavenUrl) throws Exception {
        applicationManager.importApplicationFromRepository(storage, mavenUrl);
    }

    @Override
    public void importFromBase64String(String storage, String fileContentBase64) throws Exception {
        byte[] byteArray = Base64.getDecoder().decode(fileContentBase64);

        Path temp;
        try {
            temp = Files.createTempFile("TEMP", null);
        } catch (IOException e) {
            throw e;
        }

        File tempFile = temp.toFile().getAbsoluteFile();
        tempFile.getParentFile().mkdirs();

        try (FileOutputStream fileOutputStream = new FileOutputStream(tempFile)) {
            fileOutputStream.write(byteArray);

            importFromFile(storage, tempFile.getPath());
        } catch (RuntimeException e) {
            throw e;
        } finally {
            FileUtils.deleteQuietly(tempFile);
        }
    }

    private void importFromFile(String storage, String filePath) throws ApplicationException, StorageException {
        applicationManager.importApplicationFromFile(storage, filePath);
    }

    @Override
    public void removeApplication(String storage, String applicationName) throws Exception {
        EntaxyApplication application = getApplication(storage, applicationName);
        List<EntaxyApplicationVersion> versions = application.getVersions();
        for (EntaxyApplicationVersion version : versions) {
            checkNotInstalled(application, version);
        }
        
        applicationManager.getStorage(storage).removeApplication(application);
    }

    @Override
    public List<Map<String, Object>> listApplicationItems(String storage,
                                                          String applicationName,
                                                          String applicationVersion) {
        return getApplicationVersion(storage, applicationName, applicationVersion).getLastRevision()
                .getItems().stream().map(item -> {
                    Map<String, Object> itemMap = new HashMap<>();
                    itemMap.put("id", item.getId());
                    itemMap.put("type", item.getType().toString());
                    itemMap.put("revisionable", item.isRevisionable());
                    itemMap.put("editable", item.isEditable());
                    return itemMap;
                }).collect(Collectors.toList());
    }

    @Override
    public List<Map<String, Object>> listApplicationRequirements(String storage,
                                                                 String applicationName,
                                                                 String applicationVersion) {
        return getApplicationVersion(storage, applicationName, applicationVersion).getLastRevision()
                .getDescriptor().getRequirements().stream().map(requirement -> {
                    Map<String, Object> requirementMap = new HashMap<>();
                    requirementMap.put("id", requirement.getId());
                    requirementMap.put("type", requirement.getType().toString());
                    requirementMap.put("scope", requirement.getScope().toString());
                    requirementMap.put("status", checkRequirement(requirement));
                    return requirementMap;
                }).collect(Collectors.toList());
    }

    // copied from application-shell
    private boolean checkRequirement(ApplicationRequirement requirement) {
        switch (requirement.type) {
            case OBJECT:
                EntaxyRuntimeObject runtimeObject = entaxyRuntimeObjectService.getRuntimeObject(requirement.id);
                return runtimeObject != null && !runtimeObject.isGhost();
            case RESOURCE:
                EntaxyResource entaxyResource = entaxyResourceService.getResource(requirement.id);
                return entaxyResource.exists();
            case BUNDLE:
            case CONFIG:
                // TODO implement
                return true;
            default:
                return true;
        }
    }

    @Override
    public Map<String, String> readApplicationVersionLastAndInstalledRevision(String storage,
                                                                              String applicationName,
                                                                              String applicationVersion) {
        EntaxyApplicationVersion version = getApplicationVersion(storage, applicationName, applicationVersion);
        Map<String, String> revisions = new HashMap<>(2);
        revisions.put("lastRevision", String.valueOf(version.getLastRevision().getRevisionNumber()));
        revisions.put("installedRevision", getInstalledRevisionValue(version));
        return revisions;
    }

    protected final static String APP_NAMESPACE = "entaxy.application";

    @Reference
    FeaturesService featuresService;

    // copied from application-shell
    protected String getInstalledRevisionValue(EntaxyApplicationVersion version) {
        try {
            List<FeatureCapabilityHelper> helpers = Arrays.asList(featuresService.listInstalledFeatures()).stream()
                    .map(f -> new FeatureCapabilityHelper(f))
                    .filter(h -> h.isCapabilityProvided(APP_NAMESPACE))
                    .collect(Collectors.toList());

            for (FeatureCapabilityHelper helper : helpers) {
                List<CapabilityDescriptor> descriptors = helper.getProvidedCapabilities(APP_NAMESPACE);
                for (CapabilityDescriptor descriptor : descriptors) {
                    String nameValue = descriptor.getAttributes()
                            .getOrDefault("name", "").toString();
                    String versionValue = descriptor.getAttributes()
                            .getOrDefault("version", "").toString();
                    if (version.getApplication().getName().equals(nameValue)
                            && version.getVersionNumber().equals(versionValue)) {
                        String revision = descriptor.getAttributes()
                                .getOrDefault("revision", "").toString();
                        if (CommonUtils.isValid(revision))
                            return revision;
                    }
                }
            }
        } catch (Exception ignore) {
            // NOOP
//            ignore.printStackTrace();
        }
        return "-";
    }

    @Override
    public List<String> readApplicationItemTypes() {
        return Arrays.stream(EntaxyApplication.ITEM_TYPE.values())
                .map(itemType -> itemType.toString()).collect(Collectors.toList());
    }

    @Override
    public List<String> readApplicationItemScopes() {
        return Arrays.stream(EntaxyApplication.ITEM_SCOPE.values())
                .map(scope -> scope.toString()).collect(Collectors.toList());
    }

    @Override
    public void removeVersion(String storage, String applicationName, String versionToRemove) throws ApplicationVersionHasInstalledException {
        EntaxyApplication application = getApplication(storage, applicationName);
        EntaxyApplicationVersion version = application.getVersion(versionToRemove);
        checkNotInstalled(application, version);
        
        application.removeVersion(versionToRemove);
    }

    protected void checkNotInstalled(EntaxyApplication application, EntaxyApplicationVersion version)
            throws ApplicationVersionHasInstalledException {
        List<EntaxyApplicationRevision> revisions = version.getRevisions();
        for (EntaxyApplicationRevision revision : revisions) {
            if(EntaxyApplicationRevision.REVISION_STATUS.INSTALLED.equals(revision.getStatus())) {
                throw new ApplicationVersionHasInstalledException(application.getName(), version.getVersionNumber());
            }
        }
    }

    @Override
    public void createRevision(String storage,
                               String applicationName,
                               String applicationVersion,
                               int prototypeRevision) throws Exception {
        if (prototypeRevision > -1) {
            getApplicationVersion(storage, applicationName, applicationVersion).createRevision(prototypeRevision);
        } else {
            getApplicationVersion(storage, applicationName, applicationVersion).createRevision();
        }
    }

    @Override
    public void configureRevision(String storage,
                                  String applicationName,
                                  String applicationVersion,
                                  String applicationVersionRevision) throws Exception {
        getApplicationRevision(storage, applicationName, applicationVersion, applicationVersionRevision).configure();
    }

    @Override
    public void deployRevision(String storage,
                               String applicationName,
                               String applicationVersion,
                               String applicationVersionRevision) throws Exception {
        getApplicationRevision(storage, applicationName, applicationVersion, applicationVersionRevision).deploy();
    }

    @Override
    public void installRevision(String storage,
                                String applicationName,
                                String applicationVersion,
                                String applicationVersionRevision) throws Exception {
        getApplicationRevision(storage, applicationName, applicationVersion, applicationVersionRevision).install();
    }

    @Override
    public void uninstallRevision(String storage,
                                String applicationName,
                                String applicationVersion,
                                String applicationVersionRevision) throws Exception {
        getApplicationRevision(storage, applicationName, applicationVersion, applicationVersionRevision).uninstall();
    }

    @Override
    public String readRevisionStatus(String storage,
                                     String applicationName,
                                     String applicationVersion,
                                     String applicationVersionRevision) throws Exception {
        return getApplicationRevision(storage, applicationName, applicationVersion, applicationVersionRevision)
                .getStatus().toString();
    }

    @Override
    public List<Map<String, String>> listRevisionConfigurations(String storage,
                                                                String applicationName,
                                                                String applicationVersion,
                                                                String applicationVersionRevision) throws Exception {
        return getApplicationRevision(storage, applicationName, applicationVersion, applicationVersionRevision)
                .getItems().stream()
                .filter(item -> EntaxyApplication.ITEM_TYPE.CONFIG.equals(item.getType()))
                .map(item -> {
                    Map<String, String> itemMap = new HashMap<>();
                    itemMap.put("id", item.getId());
                    itemMap.put("location", item.getLocation());
                    return itemMap;
                }).collect(Collectors.toList());
    }

    @Override
    public Map<String, Object> readRevisionConfiguration(String storage,
                                                         String applicationName,
                                                         String applicationVersion,
                                                         String applicationVersionRevision,
                                                         String configId) throws Exception {

        ApplicationItem configItem = getConfigItem(storage, applicationName, applicationVersion,
                applicationVersionRevision, configId);

        ObjectConfigEditor editor = getEditor(configId, configItem);

        List<Map<String, Object>> fields = new ArrayList<>();

        String configuredType = null;
        String configuredId = null;

        try {
            for (String name : editor.getPropertiesNames()) {
                ObjectConfigEditor.ConfigProperty property = editor.getAsConfigProperty(name);

                if (!property.hint.isInternal()) {
                    Map<String, Object> field = new HashMap<>();

                    field.put("name", name);
                    field.put("displayName", property.hint.getDisplayName());
                    field.put("type", property.hint.getType());
                    field.put("importedValue", property.configValue);
                    field.put("localValue", property.currentValue);

                    fields.add(field);
                } else {
                    if ("__entaxy_configuredId".equals(name)) {
                        configuredId = property.configValue.toString();
                    } else if ("__entaxy_configuredType".equals(name)) {
                        configuredType = property.configValue.toString();
                    }
                }
            }
        } catch (Exception e) {
            throw e;
        } finally {
            editor.clear();
        }

        Map<String, Object> configuration = new HashMap<>();
        configuration.put("id", configuredId);
        configuration.put("type", configuredType);
        configuration.put("fields", fields);

        return configuration;
    }

    @Override
    public void fillInRevisionConfigurationWithLocalValues(String storage,
                                          String applicationName,
                                          String applicationVersion,
                                          String applicationVersionRevision,
                                          String configId) throws Exception {

        ApplicationItem configItem = getConfigItem(storage, applicationName, applicationVersion,
                applicationVersionRevision, configId);

        ObjectConfigEditor editor = getEditor(configId, configItem);

        InputStream is = null;
        try {
            for (String propertyName : editor.getPropertiesNames()) {
                ObjectConfigEditor.ConfigProperty property = editor.getAsConfigProperty(propertyName);

                if (property.currentValue != null) {
                    editor.setValue(propertyName, property.currentValue.toString());
                } else {
                    throw new RuntimeException("Cannot set local values to configuration [" + configId + "]");
                }
            }

            editor.save();

            is = new FileInputStream(editor.getConfigFile());
            configItem.update(is);
        } catch (Exception e) {
            throw e;
        } finally {
            editor.clear();
            if (is != null) {
                is.close();
            }
        }
    }

    @Override
    public void saveRevisionConfiguration(String storage,
                                          String applicationName,
                                          String applicationVersion,
                                          String applicationVersionRevision,
                                          String configId,
                                          Map<String, Object> properties) throws Exception {

        ApplicationItem configItem = getConfigItem(storage, applicationName, applicationVersion,
                applicationVersionRevision, configId);

        ObjectConfigEditor editor = getEditor(configId, configItem);

        InputStream is = null;
        try {
            for (String propertyName : properties.keySet()) {
                ObjectConfigEditor.ConfigProperty property = editor.getAsConfigProperty(propertyName);

                if (property == null) {
                    throw new RuntimeException("Property [" + propertyName + "] is not found");
                }

                Object propertyValue = properties.get(propertyName);

                if (propertyValue instanceof String && ((String) propertyValue).contains(EXPRESSION_PREFIX)) {
                    propertyValue = ((String) propertyValue).replace(EXPRESSION_PREFIX, EXPRESSION_PREFIX_WITH_UNDERLINES);
                }

                editor.setValue(propertyName, propertyValue == null ? "" : propertyValue.toString());
            }

            editor.save();

            is = new FileInputStream(editor.getConfigFile());
            configItem.update(is);
        } catch (Exception e) {
            throw e;
        } finally {
            editor.clear();
            if (is != null) {
                is.close();
            }
        }
    }

    private ApplicationItem getConfigItem(String storage,
                                          String applicationName,
                                          String applicationVersion,
                                          String applicationVersionRevision,
                                          String configId) throws Exception {
        Optional<ApplicationItem> configItemOpt =
                getApplicationRevision(storage, applicationName, applicationVersion, applicationVersionRevision)
                        .getItems().stream().filter(item ->
                                EntaxyApplication.ITEM_TYPE.CONFIG.equals(item.getType()) &&
                                        configId.equals(item.getId())).findFirst();
        if (configItemOpt.isEmpty()) {
            throw new ItemNotFoundException(applicationName, applicationVersion, configId);
        }

        return configItemOpt.get();
    }

    private ObjectConfigEditor getEditor(String configId, ApplicationItem configItem) throws Exception {

        Path temp = Files.createTempFile(configId, null);
        File tempFile = temp.toFile().getAbsoluteFile();
        tempFile.getParentFile().mkdirs();

        ObjectConfigEditor editor;
        try (InputStream is = configItem.getInputStream()) {
            Files.copy(is, tempFile.toPath(), StandardCopyOption.REPLACE_EXISTING);

            editor = ObjectConfigEditor.create(tempFile);
            editor.loadCurrent(configItem.getId());
            editor.setPid(configId);
        } catch (Exception e) {
            FileUtils.deleteQuietly(tempFile);
            throw e;
        }

        return editor;
    }

    private EntaxyApplication getApplication(String storage, String applicationName) {
        return applicationManager.getStorage(storage).getApplication(applicationName);
    }

    private EntaxyApplicationVersion getApplicationVersion(String storage,
                                                           String applicationName,
                                                           String applicationVersion) {
        return getApplication(storage, applicationName).getVersion(applicationVersion);
    }

    private EntaxyApplicationRevision getApplicationRevision(String storage,
                                                             String applicationName,
                                                             String applicationVersion,
                                                             String applicationVersionRevision) throws Exception {
        return getApplicationVersion(storage, applicationName, applicationVersion)
                .getRevision(applicationVersionRevision);
    }
}
