/*-
 * ~~~~~~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.ByteArrayOutputStream;
import java.io.IOException;
import java.io.InputStream;
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.stream.Collectors;

import javax.management.DynamicMBean;
import javax.management.MBeanRegistration;
import javax.management.NotCompliantMBeanException;
import javax.ws.rs.NotFoundException;

import org.apache.commons.io.IOUtils;
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.objects.EntaxyObject;
import ru.entaxy.platform.base.support.CommonUtils;
import ru.entaxy.platform.base.support.JSONUtils;
import ru.entaxy.platform.core.artifact.service.ArtifactService;
import ru.entaxy.platform.integration.applications.ApplicationManager;
import ru.entaxy.platform.integration.applications.ApplicationProjectItem;
import ru.entaxy.platform.integration.applications.EntaxyApplication;
import ru.entaxy.platform.integration.applications.EntaxyApplicationProject;
import ru.entaxy.platform.integration.applications.EntaxyApplicationProjectVersion;
import ru.entaxy.platform.integration.applications.descriptor.ApplicationRequirement;
import ru.entaxy.platform.integration.applications.impl.ServiceHelper;
import ru.entaxy.platform.integration.applications.impl.project.ApplicationProjectItemImpl;
import ru.entaxy.platform.integration.applications.item.project.EntaxyObjectItem;
import ru.entaxy.platform.integration.applications.management.ProjectsMBean;
import ru.entaxy.platform.objects.runtime.EntaxyRuntimeObject;
import ru.entaxy.platform.objects.runtime.EntaxyRuntimeObjectService;

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

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

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

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

    @Reference(cardinality = ReferenceCardinality.MANDATORY)
    ArtifactService artifactService;

    public ProjectsMBeanImpl() throws NotCompliantMBeanException {
        super(ProjectsMBean.class);
    }

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

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

            Map<String, Object> projectStorage = new HashMap<>();
            projectStorage.put(TITLE, storage.getName());
            projectStorage.put(CHILDREN, storage.getProjects().stream().map(project -> {
                Map<String, Object> projectMap = new HashMap<>();
                projectMap.put(TITLE, project.getName());
                projectMap.put(CHILDREN, project.getVersions().stream().map(version -> {
                    Map<String, Object> versionMap = new HashMap<>();
                    versionMap.put(TITLE, version.getVersionNumber());

                    if (version.isBuildAvailable()) {
                        Map<String, Object> buildInfo = new HashMap<>(1);
                        List<Map<String, Object>> buildInfoList = new ArrayList<>(1);
                        buildInfo.put(TITLE, "build info");
                        buildInfoList.add(buildInfo);
                        versionMap.put(CHILDREN, buildInfoList);
                    }
                    return versionMap;
                }).collect(Collectors.toList()));
                return projectMap;
            }).collect(Collectors.toList()));

            projectStorages.add(projectStorage);
        });

        return JSONUtils.GSON.toJson(projectStorages);
    }

    @Override
    public List<Map<String, Object>> listProjectItems(String storage, String projectName, String projectVersion) {

        return getProjectVersion(storage, projectName, projectVersion)
                .getItems().stream().map(item -> {
                    checkState(item);
                    Map<String, Object> itemMap = new HashMap<>();
                    itemMap.put("status", getStatus(item));
                    itemMap.put("id", item.getId());
                    itemMap.put("type", item.getType().toString());
                    itemMap.put("scope", item.getScope().toString());
                    itemMap.put("required", item.isRequired());
                    itemMap.put("ignored", item.isIgnored());
                    itemMap.put("deleted", item.isDeleted());
                    if (EntaxyApplication.ITEM_TYPE.OBJECT.equals(item.getType()) && !item.isDeleted()) {
                        itemMap.put("displayName", ((EntaxyObjectItem) item).getObject().getDisplayName());
                        itemMap.put("runtimeType", ((EntaxyObjectItem) item).getObject().getType());
                    }
                    return itemMap;
                }).collect(Collectors.toList());
    }
    
    private void checkState(ApplicationProjectItem item) {
        switch (item.getType()) {
            case CONFIG:
            case OBJECT:
                EntaxyRuntimeObject runtimeObject = ServiceHelper.getInstance().getEntaxyRuntimeObjectService()
                        .getRuntimeObject(item.getId());
                if (runtimeObject == null)
                    ((ApplicationProjectItemImpl)item).setDeleted(true); 
                break;

            case RESOURCE:
                EntaxyResource entaxyResource =
                        ServiceHelper.getInstance().getEntaxyResourceService().getResource(item.getId());
                if (entaxyResource == null || !entaxyResource.exists())
                    ((ApplicationProjectItemImpl)item).setDeleted(true);
                break;

            default:
                break;
        }
    }
    
    private enum STATUS {
        INCLUDED,
        IGNORED,
        REQUIRED,
        GHOST,
        DELETED
    }

    @Override
    public List<String> readProjectItemStatuses() {
        return Arrays.stream(STATUS.values()).map(status -> status.toString()).collect(Collectors.toList());
    }

    // copied from application-shell
    private String getStatus(ApplicationProjectItem item) {
        if (item.isDeleted()) {
            return STATUS.DELETED.toString();
        }
        String status = STATUS.INCLUDED.toString();
        if (item.isGhost()) {
            status = STATUS.GHOST.toString();
        }
        if (item.isRequired()) {
            if (item.isIgnored()) {
                status = STATUS.IGNORED.toString();
            } else {
                status = STATUS.REQUIRED.toString();
            }
        }
        return status;
    }

    @Override
    public void createProject(String projectName, String storage, String initialVersion) throws Exception {
        String version = CommonUtils.isValid(initialVersion) ? initialVersion : "1.0.0";
        applicationManager.createProject(storage, projectName, version);
    }

    @Override
    public void removeProject(String storage, String projectName) throws Exception {
        applicationManager.getStorage(storage).removeProject(getProject(storage, projectName));
    }

    @Override
    public List<Map<String, String>> addObject(String storage, String projectName, String projectVersion, String objectId) {
        EntaxyRuntimeObject object = entaxyRuntimeObjectService.getRuntimeObject(objectId);

        getProjectVersion(storage, projectName, projectVersion)
                .addObject(object);

        return getRelatedObjects(object);
    }

    @Override
    public List<Map<String, String>> listObjects() {
        return entaxyRuntimeObjectService.getObjects().stream().map(object -> {
            return objectIntoMap(object);
        }).collect(Collectors.toList());
    }

    @Override
    public List<Map<String, String>> getRelatedObjects(String objectId) {
        EntaxyRuntimeObject object = entaxyRuntimeObjectService.getRuntimeObject(objectId);
        return getRelatedObjects(object);
    }

    private List<Map<String, String>> getRelatedObjects(EntaxyRuntimeObject object) {
        return object.getSubordRelations().stream().filter(relation -> {
            return EntaxyObject.OBJECT_RELATION.AGGREGATION.equals(relation.getType()) &&
                    !relation.getTarget().isColocatedWith(object);
        }).map(relation -> {
            EntaxyRuntimeObject relatedObject = relation.getTarget();
            return objectIntoMap(relatedObject);
        }).collect(Collectors.toList());
    }

    private Map<String, String> objectIntoMap(EntaxyRuntimeObject object) {
        Map<String, String> objectMap = new HashMap<>();
        objectMap.put("fullId", object.getObjectFullId());
        objectMap.put("displayName", object.getDisplayName());
        objectMap.put("type", object.getType());
        return objectMap;
    }

    @Override
    public void addResource(String storage, String projectName, String projectVersion,
                                                 String location) throws Exception {
        addResource(
                getProjectVersion(storage, projectName, projectVersion),
                entaxyResourceService.getResource(location));
    }

    private void addResource(EntaxyApplicationProjectVersion version, EntaxyResource resource)
            throws NotFoundException {
        if (resource != null) {
            if (resource.isFolder()) {
                for(EntaxyResource res : entaxyResourceService.getResources(resource.getURL())) {
                    addResource(version, res);
                }
            } else {
                version.addResource(resource);
            }
        } else {
            throw new NotFoundException("Cannot find the resource located at " + resource.getURL());
        }
    }

    @Override
    public void addItem(String storage, String projectName, String projectVersion, String itemId) throws Exception {
        ApplicationProjectItem projectItem = getProjectItem(storage, projectName, projectVersion, itemId);
        if (projectItem != null) {
            getProjectVersion(storage, projectName, projectVersion).addItem(projectItem);
        }
    }

    @Override
    public void ignoreItem(String storage, String projectName, String projectVersion, String itemId) throws Exception {
        ApplicationProjectItem projectItem = getProjectItem(storage, projectName, projectVersion, itemId);
        if (projectItem != null) {
            getProjectVersion(storage, projectName, projectVersion).ignoreItem(projectItem);
        }
    }

    @Override
    public void removeItem(String storage, String projectName, String projectVersion, String itemId) throws Exception {
        ApplicationProjectItem projectItem = getProjectItem(storage, projectName, projectVersion, itemId);
        if (projectItem != null) {
            getProjectVersion(storage, projectName, projectVersion).removeItem(projectItem);
        }
    }

    @Override
    public Map<String, List<String>> removeItems(String storage, String projectName, String projectVersion, List<String> itemIds)
            throws Exception {
        EntaxyApplicationProjectVersion version = getProjectVersion(storage, projectName, projectVersion);

        List<ApplicationProjectItem> items = version.getItems().stream()
                .filter(item -> itemIds.contains(item.getId())).collect(Collectors.toList());

        List<ApplicationProjectItem> itemsToRemove = new ArrayList<>();
        Map<ApplicationProjectItem, List<ApplicationProjectItem>> itemsImpossibleToRemove = new HashMap<>();

        for (ApplicationProjectItem item : items) {
            List<ApplicationProjectItem> requiredBy = item.getRequiredBy();
            if (requiredBy.isEmpty()) {
                itemsToRemove.add(item);
            } else {
                List<ApplicationProjectItem> requiredLeft = requiredBy.stream()
                        .filter(requiredByItem -> !itemIds.contains(requiredByItem.getId()))
                        .collect(Collectors.toList());
                if (requiredLeft.isEmpty()) {
                    itemsToRemove.add(item);
                } else {
                    itemsImpossibleToRemove.put(item, requiredLeft);
                }
            }
        }

        List<String> itemIdsToRemove = itemsToRemove.stream()
                .map(itemToRemove -> itemToRemove.getId()).collect(Collectors.toList());

        do {
            for (ApplicationProjectItem itemToRemove : itemsToRemove) {
                version.removeItem(itemToRemove);
            }
            itemsToRemove = version.getItems().stream()
                    .filter(item -> itemIdsToRemove.contains(item.getId())).collect(Collectors.toList());
        } while (itemsToRemove.size() > 0);

        if (itemsImpossibleToRemove.size() > 0) {
            Map<String, List<String>> result = new HashMap<>();
            for (ApplicationProjectItem itemToRemove : itemsImpossibleToRemove.keySet()) {
                if (itemIds.contains(itemToRemove.getId())) {
                    version.removeItem(itemToRemove);
                }
                result.put(getIdOrDisplayName(itemToRemove),
                        itemsImpossibleToRemove.get(itemToRemove).stream()
                                .map(item -> getIdOrDisplayName(item)).collect(Collectors.toList()));
            }
            return result;
        }

        return new HashMap<>();
    }

    private String getIdOrDisplayName(ApplicationProjectItem item) {
        String displayName = "";
        if (EntaxyApplication.ITEM_TYPE.OBJECT.equals(item.getType())) {
            displayName = ((EntaxyObjectItem) item).getObject().getDisplayName();
        }
        return CommonUtils.isValid(displayName) ? displayName : item.getId();
    }

    @Override
    public void createVersion(String storage, String projectName, String createdVersion, String prototypeVersion)
            throws Exception {
        if (CommonUtils.isValid(prototypeVersion)) {
            getProject(storage, projectName).createVersion(createdVersion, prototypeVersion);
        } else {
            getProject(storage, projectName).createVersion(createdVersion);
        }
    }

    @Override
    public void removeVersion(String storage, String projectName, String versionToRemove) {
        getProject(storage, projectName).removeVersion(versionToRemove);
    }

    @Override
    public void saveProjectVersion(String storage, String projectName, String projectVersion) {
        getProjectVersion(storage, projectName, projectVersion).save();
    }

    @Override
    public void buildProjectVersion(String storage, String projectName, String projectVersion) throws Exception {
        getProjectVersion(storage, projectName, projectVersion).build();
    }

    @Override
    public List<Map<String, String>> listBuiltProjectItems(String storage, String projectName, String projectVersion) {
        return getProjectVersion(storage, projectName, projectVersion).getBuildInfo().getItems().stream().map(item -> {
            Map<String, String> itemMap = new HashMap<>();
            itemMap.put("id", item.getId());
            itemMap.put("type", item.getType().toString());
            itemMap.put("component", item.getComponentId());
            return itemMap;
        }).collect(Collectors.toList());
    }

    @Override
    public List<Map<String, Object>> listBuiltProjectRequirements(String storage,
                                                                  String projectName,
                                                                  String projectVersion) {
        return getProjectVersion(storage, projectName, projectVersion).getBuildInfo().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 List<Map<String, String>> listBuiltProjectComponents(String storage,
                                                                String projectName,
                                                                String projectVersion) {
        return getProjectVersion(storage, projectName, projectVersion).getBuildInfo().getComponents()
                .stream().map(component -> {
                    Map<String, String> componentMap = new HashMap<>();
                    componentMap.put("id", component.getId());
                    componentMap.put("type", component.getType().toString());
                    componentMap.put("priority", String.valueOf(component.getPriority()));
                    return componentMap;
                }).collect(Collectors.toList());
    }

    @Override
    public String readMavenUrl(String storage, String projectName, String projectVersion) {
        EntaxyApplicationProjectVersion version = getProjectVersion(storage, projectName, projectVersion);
        if (version.isBuildAvailable()) {
            return version.getBuildInfo().getMavenUrl();
        } else {
            return null;
        }
    }

    @Override
    public List<String> listRepositories() {
        return artifactService.getRepositories().stream()
                .map(repository -> repository.getName()).collect(Collectors.toList());
    }

    @Override
    public void exportToRepository(String storage, String projectName, String projectVersion, String repositoryName)
            throws Exception {
        applicationManager.exportToRepository(getProjectVersion(storage, projectName, projectVersion), repositoryName);
    }

    @Override
    public String exportToFile(String storage, String projectName, String projectVersion) throws Exception {
        try (InputStream is = getProjectVersion(storage, projectName, projectVersion)
                .getBuildUrl().openConnection().getInputStream()) {
            return Base64.getEncoder().encodeToString(readFromStream(is));
        }
    }

    private byte[] readFromStream(InputStream stream) throws IOException {
        try (ByteArrayOutputStream baos = new ByteArrayOutputStream()) {
            IOUtils.copy(stream, baos);
            return baos.toByteArray();
        } catch (IOException e) {
            throw e;
        }
    }

    private EntaxyApplicationProject getProject(String storage, String projectName) {
        return applicationManager.getStorage(storage).getProject(projectName);
    }

    private EntaxyApplicationProjectVersion getProjectVersion(String storage,
                                                              String projectName,
                                                              String projectVersion) {
        return getProject(storage, projectName).getVersion(projectVersion);
    }

    private ApplicationProjectItem getProjectItem(
            String storage, String projectName, String projectVersion, String itemId) {

        return getProjectVersion(storage, projectName, projectVersion).getItems()
                .stream().filter(item -> item.getId().equals(itemId)).findFirst().orElse(null);
    }
}
