/*-
 * ~~~~~~licensing~~~~~~
 * application-impl
 * ==========
 * 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.impl.build;

import java.io.File;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.OutputStream;
import java.net.MalformedURLException;
import java.net.URI;
import java.net.URL;
import java.nio.file.Paths;
import java.util.ArrayList;
import java.util.Calendar;
import java.util.Collections;
import java.util.Comparator;
import java.util.HashMap;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import java.util.Map.Entry;
import java.util.UUID;
import java.util.stream.Collectors;

import org.apache.commons.io.FileUtils;
import org.osgi.framework.Bundle;
import org.osgi.framework.FrameworkUtil;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import com.google.gson.Gson;
import com.google.gson.GsonBuilder;
import com.google.gson.JsonElement;

import ru.entaxy.base.generator.template.Template;
import ru.entaxy.base.generator.template.TemplateAwareGenerator;
import ru.entaxy.esb.platform.runtime.base.connecting.generator.Generated;
import ru.entaxy.esb.platform.runtime.base.connecting.generator.factory.GeneratorFactory;
import ru.entaxy.platform.core.artifact.ArtifactCoordinates;
import ru.entaxy.platform.integration.applications.ApplicationProjectContent.STANDARD_PROPERTIES;
import ru.entaxy.platform.integration.applications.EntaxyApplication;
import ru.entaxy.platform.integration.applications.EntaxyApplication.ITEM_TYPE;
import ru.entaxy.platform.integration.applications.descriptor.ApplicationComponent;
import ru.entaxy.platform.integration.applications.descriptor.ApplicationContentItem;
import ru.entaxy.platform.integration.applications.descriptor.ApplicationDescriptor;
import ru.entaxy.platform.integration.applications.descriptor.ApplicationFeatures;
import ru.entaxy.platform.integration.applications.descriptor.ApplicationRequirement;
import ru.entaxy.platform.integration.applications.impl.ServiceHelper;
import ru.entaxy.platform.integration.applications.impl.build.AbstractStorage.BuildResult;
import ru.entaxy.platform.integration.applications.impl.jar.FolderBasedJarBuilder;
import ru.entaxy.platform.integration.applications.impl.project.ApplicationProjectContentImpl;
import ru.entaxy.platform.integration.applications.impl.project.ApplicationProjectItemImpl;
import ru.entaxy.platform.integration.applications.impl.project.item.BundleItemImpl;
import ru.entaxy.platform.integration.applications.impl.project.item.ConfigItemImpl;
import ru.entaxy.platform.integration.applications.impl.project.item.ObjectItemImpl;
import ru.entaxy.platform.integration.applications.impl.project.item.ResourceItemImpl;
import ru.entaxy.platform.objects.runtime.EntaxyRuntimeObject;

public class ApplicationBuilder {

    private static final Logger LOG = LoggerFactory.getLogger(ApplicationBuilder.class);

    protected static Gson GSON = new GsonBuilder().setPrettyPrinting().create();

    public static final String DEFAULT_VERSION = "1.0.0";

    public static final String DEFAULT_APP_NAME = "app-0";

    protected static final String CONTENT_ROOT_NAME = "content";

    protected static final String TEMP_ROOT_NAME = "temp";

    protected static final URI ROOT_FILE_URI =
            Paths.get(System.getProperty("karaf.home"), "data/tmp/.appbuilder").toAbsolutePath().toUri();

    protected static File ROOT_FILE;

    protected static boolean IS_INITED = false;

    protected static void initRootFile() {
        if (IS_INITED)
            return;
        ROOT_FILE = new File(ROOT_FILE_URI);
        ROOT_FILE.mkdirs();
        IS_INITED = true;
    }

    final protected ArtifactCoordinates coordinates = new ArtifactCoordinates();

    protected ApplicationProjectContentImpl applicationContent;

    protected String buildRootName = UUID.randomUUID().toString();

    protected File buildRoot;

    protected File buildContentRoot;

    protected File buildTempRoot;

    protected Map<String, BundleDescriptor> bundleDescriptors = new HashMap<>();

    protected Map<String, ResourceDescriptor> resourceDescriptors = new HashMap<>();

    protected Map<String, ConfigDescriptor> configDescriptors = new HashMap<>();

    protected Map<String, AbstractDescriptor> itemToDescriptorMap = new HashMap<>();

    protected BundleStorage bundleStorage;

    protected ResourceStorage resourceStorage;

    protected ConfigStorage configStorage;

    protected ApplicationDescriptor descriptor;

    protected File buildResult;

    public ApplicationBuilder(ApplicationProjectContentImpl applicationContent) {
        super();
        initRootFile();
        this.applicationContent = applicationContent;
        coordinates.groupId(applicationContent.getProperties().getOrDefault(STANDARD_PROPERTIES.GROUP,
                EntaxyApplication.DEFAULT_GROUP_ID).toString())
                .version(applicationContent.getProperties().getOrDefault(STANDARD_PROPERTIES.VERSION,
                        DEFAULT_VERSION).toString())
                .artifactId(applicationContent.getProperties().getOrDefault(STANDARD_PROPERTIES.APPLICATION,
                        DEFAULT_APP_NAME).toString());
        buildRoot = new File(Paths.get(ROOT_FILE.getAbsolutePath(), buildRootName).toAbsolutePath().toUri());
        buildRoot.mkdirs();

        buildContentRoot = new File(Paths.get(buildRoot.getAbsolutePath(), CONTENT_ROOT_NAME).toAbsolutePath().toUri());
        buildContentRoot.mkdirs();

        buildTempRoot = new File(Paths.get(buildRoot.getAbsolutePath(), TEMP_ROOT_NAME).toAbsolutePath().toUri());
        buildTempRoot.mkdirs();

        bundleStorage = new BundleStorage(buildContentRoot, buildTempRoot);

        resourceStorage = new ResourceStorage(buildContentRoot, buildTempRoot);

        configStorage = new ConfigStorage(buildContentRoot, buildTempRoot);
    }

    public ArtifactCoordinates getCoordinates() {
        return coordinates;
    }

    public void prepareContent() {

        // get only included items
        List<ApplicationProjectItemImpl> includedItems = applicationContent.getItemsMap().values().stream()
                .filter(item -> !item.isRequired()).collect(Collectors.toList());

        // get by type
        List<BundleItemImpl> includedBundles = new ArrayList<>();
        List<ObjectItemImpl> includedObjects = new ArrayList<>();
        List<ConfigItemImpl> includedConfigs = new ArrayList<>();
        List<ResourceItemImpl> includedResources = new ArrayList<>();

        for (ApplicationProjectItemImpl item : includedItems)
            if (item instanceof ConfigItemImpl)
                includedConfigs.add((ConfigItemImpl) item);
            else if (item instanceof ObjectItemImpl)
                includedObjects.add((ObjectItemImpl) item);
            else if (item instanceof BundleItemImpl)
                includedBundles.add((BundleItemImpl) item);
            else if (item instanceof ResourceItemImpl)
                includedResources.add((ResourceItemImpl) item);
            else
                continue;

        // collect bundles for objects
        for (ObjectItemImpl item : includedObjects) {
            // actualize object info
            EntaxyRuntimeObject runtimeObject =
                    ServiceHelper.getInstance().getEntaxyRuntimeObjectService().getRuntimeObject(
                            item.getObject().getObjectFullId());
            String containerId = runtimeObject.getContainer().getId();
            BundleDescriptor desc;
            if (!bundleDescriptors.containsKey(containerId)) {
                long bundleId = Long.parseLong(containerId);
                Bundle bundle = FrameworkUtil.getBundle(getClass()).getBundleContext().getBundle(bundleId);
                desc = BundleDescriptor.forBundle(bundle);
                if (!desc.consistent) {
                    LOG.error(String.format("Descritor is inconcistent for bundle [%s]", containerId));
                    continue;
                }
                bundleDescriptors.put(containerId, desc);
                bundleStorage.addDescriptor(desc);
            } else {
                desc = bundleDescriptors.get(containerId);
            }
            itemToDescriptorMap.put(item.getId(), desc);
        }

        // collect other bundles
        for (BundleItemImpl item : includedBundles) {
            // TODO implement
        }

        // collect resources
        for (ResourceItemImpl item : includedResources) {
            ResourceDescriptor desc;
            if (!resourceDescriptors.containsKey(item.getId())) {
                desc = ResourceDescriptor.forResource(item.getResource());
                resourceStorage.addDescriptor(desc);
            } else {
                desc = resourceDescriptors.get(item.getId());
            }
            itemToDescriptorMap.put(item.getId(), desc);
        }

        // collect configs
        for (ConfigItemImpl item : includedConfigs) {
            ConfigDescriptor desc = null;
            EntaxyRuntimeObject configObject =
                    ServiceHelper.getInstance().getEntaxyRuntimeObjectService().getRuntimeObject(
                            item.getObject().getObjectFullId());
            if (!configDescriptors.containsKey(item.getId())) {

                /*
                
                
                if (!configObject.getIncomiingSubordRelations().isEmpty()) {
                    // this is object config
                    Optional<EntaxyRuntimeRelation> rel = configObject.getIncomiingSubordRelations().stream()
                            .filter(r -> "config".equals(r.getName())).findFirst();
                    if (rel.isPresent()) {
                        EntaxyRuntimeObject configuredObject = rel.get().getMain();
                
                        // check data consistency
                        if (configuredObject.getObjectFullId()
                                .equals(ObjectConfig.getConfiguredObjectId(configObject.getId()))) {
                
                            
                        }
                
                    }
                }
                if (desc == null) {
                    // this is custom config
                    desc = ConfigDescriptor.forConfig(item.getId());
                }
                */

                desc = ConfigDescriptor.forConfig(item.getId(), getCoordinates());

                configStorage.addDescriptor(desc);
            } else {
                desc = configDescriptors.get(item.getId());
            }
            itemToDescriptorMap.put(item.getObject().getObjectFullId(), desc);
        }

        bundleStorage.prepare();
        configStorage.prepare();
        resourceStorage.prepare();

        BuildResult result = resourceStorage.build(coordinates);
        for (Entry<String, AbstractDescriptor> entry : result.changes.entrySet()) {
            itemToDescriptorMap.put(entry.getKey(), entry.getValue());
        }
        for (Entry<String, AbstractDescriptor> entry : result.newDescriptors.entrySet()) {
            if (entry.getValue() instanceof BundleDescriptor) {
                // register descriptor
                bundleDescriptors.put(entry.getKey(), (BundleDescriptor) entry.getValue());



                // add descriptor to storage
                bundleStorage.prepareDescriptor((BundleDescriptor) entry.getValue());
            }
        }

        result = bundleStorage.build(coordinates);
        // process result
        // NOOP

    }

    public void generateDescriptor() {
        descriptor = new ApplicationDescriptor();

        descriptor.name = coordinates.getArtifactId();
        descriptor.group = coordinates.getGroupId();
        descriptor.version = coordinates.getVersion();
        descriptor.buildBy = "";
        descriptor.buildOn = Calendar.getInstance().getTimeInMillis() + "";

        descriptor.mavenUrl = coordinates.toMavenString();
        descriptor.mavenAsset = coordinates.toAssetName();

        // get requirements
        List<ApplicationProjectItemImpl> reqs = applicationContent.getItemsMap().values().stream()
                .filter(item -> item.isRequired()).collect(Collectors.toList());
        Map<String, ApplicationRequirement> requirementsMap = new LinkedHashMap<>();
        for (ApplicationProjectItemImpl req : reqs) {
            ApplicationRequirement appReq = convertToRequirement(req);
            requirementsMap.put(appReq.id, appReq);
        }
        for (ApplicationRequirement appReq : requirementsMap.values())
            descriptor.requirements.add(appReq);

        Map<String, ApplicationContentItem> contentItems = new LinkedHashMap<>();
        Map<String, ApplicationComponent> components = new LinkedHashMap<>();
        for (Entry<String, AbstractDescriptor> entry : itemToDescriptorMap.entrySet()) {
            String itemId = entry.getKey();
            ApplicationProjectItemImpl itemImpl = applicationContent.getItemsMap().get(itemId);
            ApplicationContentItem contentItem = new ApplicationContentItem();
            contentItem.id = itemImpl.getId();
            contentItem.type = itemImpl.getType();
            contentItem.componentId = entry.getValue().getDescriptorId();
            contentItems.put(contentItem.id, contentItem);
            ApplicationComponent component = convertToComponent(entry.getValue());
            if (component == null) {
                LOG.error("Failed converting descriptor [{}]/[{}] to component", entry.getValue().getDescriptorId(),
                        entry.getValue().type.name());
                continue;
            } else {
                // components.put(entry.getKey(), component);
                // exclude duplicates
                components.putIfAbsent(component.getId(), component);
            }
        }

        for (ApplicationContentItem item : contentItems.values())
            descriptor.items.add(item);

        List<ApplicationComponent> componentList = new ArrayList<>(components.values());
        Collections.sort(componentList, new Comparator<ApplicationComponent>() {

            @Override
            public int compare(ApplicationComponent o1, ApplicationComponent o2) {
                if (o1.priority > o2.priority)
                    return 1;
                if (o1.priority < o2.priority)
                    return -1;
                return 0;
            }
        });

        descriptor.components.addAll(componentList);

    }

    public void saveDescriptor() {
        File descriptorFile =
                new File(Paths.get(buildContentRoot.getAbsolutePath(), EntaxyApplication.DESCRIPTOR_NAME)
                        .toAbsolutePath().toUri());
        try (OutputStream bos = new FileOutputStream(descriptorFile)) {
            JsonElement je = GSON.toJsonTree(descriptor);
            String jsonData = je.toString();
            bos.write(jsonData.getBytes());
        } catch (Exception e) {
            LOG.error("Error saving descriptor file", e);
        }
    }

    protected ApplicationComponent convertToComponent(AbstractDescriptor descriptor) {
        switch (descriptor.type) {
            case BUNDLE:
                return convertToComponent((BundleDescriptor) descriptor);
            case RESOURCE:
                return convertToComponent((ResourceDescriptor) descriptor);
            case CONFIG:
                return convertToComponent((ConfigDescriptor) descriptor);
            default:
                return null;
        }
    }

    protected ApplicationComponent convertToComponent(BundleDescriptor descriptor) {
        ApplicationComponent result = new ApplicationComponent();

        result.id = descriptor.getDescriptorId();
        result.internalLocation = descriptor.finalLocation;
        result.mavenLocation = descriptor.finalMavenLocation;
        result.priority = descriptor.priority;
        result.type = ITEM_TYPE.BUNDLE;

        String temp = descriptor.installLocation;
        int index1 = temp.lastIndexOf(':');
        if (index1 > 0) {
            result.targetProtocol = temp.substring(0, index1);
            temp = temp.substring(index1 + 1);
        }
        int index2 = temp.indexOf('?');
        if (index2 > 0) {
            result.targetQueryString = temp.substring(index2 + 1);
            temp = temp.substring(0, index2);
        }
        result.targetLocation = temp;

        return result;
    }

    protected ApplicationComponent convertToComponent(ResourceDescriptor descriptor) {
        ApplicationComponent result = new ApplicationComponent();

        result.id = descriptor.getDescriptorId();
        result.internalLocation = descriptor.finalLocation;
        result.mavenLocation = "";
        result.targetLocation = descriptor.resourceUrl;
        result.priority = 25;
        result.type = ITEM_TYPE.RESOURCE;
        result.targetLocation = descriptor.resourceUrl;

        return result;
    }

    protected ApplicationComponent convertToComponent(ConfigDescriptor descriptor) {
        ApplicationComponent result = new ApplicationComponent();

        result.id = descriptor.getDescriptorId();
        result.internalLocation = descriptor.finalLocation;
        result.mavenLocation = descriptor.finalMavenLocation;
        result.targetLocation = descriptor.pid + ".cfg";
        result.priority = 25;
        result.type = ITEM_TYPE.CONFIG;

        return result;
    }

    protected ApplicationRequirement convertToRequirement(ApplicationProjectItemImpl sourceItem) {
        ApplicationRequirement result = new ApplicationRequirement();
        result.id = sourceItem.getId();
        result.type = sourceItem.getType();
        result.scope = sourceItem.getScope();
        return result;
    }

    public void generateFeatures() {
        Template template =
                ServiceHelper.getInstance().getTemplateService().getTemplateById("applications.builder.features");
        TemplateAwareGenerator generator = GeneratorFactory.createGenerator(template);
        if (generator != null) {
            String repoName = String.format("%s.%s-%s", coordinates.getGroupId(), coordinates.getArtifactId(),
                    coordinates.getVersion());
            String featureName = coordinates.getGroupId().concat(".").concat(coordinates.getArtifactId());
            Map<String, Object> params = new HashMap<>();
            params.put("descriptor", descriptor);
            params.put("repoName", repoName);
            params.put("featureName", featureName);
            params.put("featureVersion", coordinates.getVersion());
            Generated generated;
            try {
                generated = generator.generate(template, params);
            } catch (Exception e) {
                LOG.error("Error generating features", e);
                return;
            }
            if ((generated == null) || (generated.getObject() == null)) {
                LOG.error("Generated NULL");
            }
            ArtifactCoordinates featuresCoordinates =
                    ArtifactCoordinates.fromUrl(coordinates.toMavenString()).classifier("features").type("xml");
            String path = featuresCoordinates.toRepositoryPath();
            File featuresFile = new File(Paths.get(bundleStorage.rootDirectory.getAbsolutePath(), path).toUri());
            featuresFile.getAbsoluteFile().getParentFile().mkdirs();

            try (OutputStream fos = new FileOutputStream(featuresFile)) {
                fos.write(generated.getObject().toString().getBytes());
            } catch (Exception e) {
                LOG.error("Error writing features file", e);
            }

            descriptor.features = new ApplicationFeatures();
            descriptor.features.id = featureName;
            descriptor.features.internalLocation =
                    buildContentRoot.toPath().relativize(featuresFile.toPath()).toString().replace('\\', '/');
            descriptor.features.mavenLocation = featuresCoordinates.toMavenString();
            descriptor.features.targetProtocol = "mvn";
            descriptor.features.targetLocation = descriptor.features.mavenLocation;

        }
    }

    public void pack() {
        File targetFile =
                new File(Paths.get(buildRoot.getAbsolutePath(), "result", coordinates.toRepositoryPath()).toUri());
        targetFile.getAbsoluteFile().getParentFile().mkdirs();
        FolderBasedJarBuilder builder = new FolderBasedJarBuilder(targetFile, buildContentRoot);
        builder.putAttribute("Entaxy-Application", coordinates.getArtifactId());
        builder.putAttribute("Entaxy-ApplicationGroup", coordinates.getGroupId());
        builder.putAttribute("Entaxy-ApplicationVersion", coordinates.getVersion());
        try {
            builder.loadFiles();
        } catch (IllegalStateException | IOException e) {
            LOG.error("Error building application file", e);
        }
        try {
            builder.close();
        } catch (IOException e) {
            LOG.error("Error building application file", e);
            return;
        }
        buildResult = targetFile;
    }

    public void clear() {
        try {
            FileUtils.deleteDirectory(buildRoot);
        } catch (IOException e) {
            LOG.error("Error clearing ApplicationBuilder", e);
        }
    }

    public URL build() {

        prepareContent();
        generateDescriptor();
        generateFeatures();
        saveDescriptor();
        pack();

        if (buildResult == null)
            return null;

        try {
            return buildResult.toURI().toURL();
        } catch (MalformedURLException e) {
            LOG.error("Error creating URL for [" + buildResult.getAbsolutePath() + "]", e);
            return null;
        }
    }

    public ApplicationDescriptor getDescriptor() {
        return descriptor;
    }

    public File getBuildResult() {
        return buildResult;
    }

}
