/*-
 * ~~~~~~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;

import java.io.File;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.net.URL;
import java.nio.file.Files;
import java.nio.file.StandardCopyOption;
import java.util.ArrayList;
import java.util.Calendar;
import java.util.HashMap;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import java.util.jar.JarEntry;
import java.util.jar.JarFile;

import org.osgi.framework.ServiceException;
import org.osgi.service.component.annotations.Activate;
import org.osgi.service.component.annotations.Component;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

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

import ru.entaxy.platform.base.support.CommonUtils;
import ru.entaxy.platform.base.support.JSONUtils;
import ru.entaxy.platform.base.support.osgi.OSGIUtils;
import ru.entaxy.platform.core.artifact.Artifact;
import ru.entaxy.platform.core.artifact.ArtifactCoordinates;
import ru.entaxy.platform.core.artifact.Artifacts;
import ru.entaxy.platform.core.artifact.repository.ArtifactRepository;
import ru.entaxy.platform.core.artifact.service.ArtifactService;
import ru.entaxy.platform.integration.applications.ApplicationStorage.ApplicationImportContent;
import ru.entaxy.platform.integration.applications.ApplicationStorage.ApplicationImportItem;
import ru.entaxy.platform.integration.applications.EntaxyApplication;
import ru.entaxy.platform.integration.applications.EntaxyApplicationProjectVersion;
import ru.entaxy.platform.integration.applications.EntaxyApplicationRevision;
import ru.entaxy.platform.integration.applications.EntaxyApplicationVersion;
import ru.entaxy.platform.integration.applications.descriptor.ApplicationComponent;
import ru.entaxy.platform.integration.applications.descriptor.ApplicationDescriptor;
import ru.entaxy.platform.integration.applications.exceptions.ApplicationDescriptorIsInvalid;
import ru.entaxy.platform.integration.applications.exceptions.ApplicationDescriptorNotFound;

@Component(service = ExportImportHelper.class, immediate = true)
public class ExportImportHelper {

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

    public final Map<String, Exporter> exporters = new HashMap<>();

    public final Map<String, Importer> importers = new HashMap<>();

    protected static ExportImportHelper INSTANCE = null;

    @Activate
    public void activate() {
        exporters.put("file", new FileExporter());
        exporters.put("repository", new RepositoryExporter());
        importers.put("file", new FileImporter());
        importers.put("repository", new RepositoryImporter());
        ExportImportHelper.INSTANCE = this;
    }

    public void doExport(EntaxyApplicationProjectVersion version, String exporterType, Map<String, Object> properties)
            throws Exception {
        if (!exporters.containsKey(exporterType))
            return;
        Exporter exporter = exporters.get(exporterType);
        exporter.execute(version, properties);
    }

    public ApplicationImportContent doImport(String importerType, Map<String, Object> properties)
            throws Exception {
        if (!importers.containsKey(importerType))
            return null;
        Importer importer = importers.get(importerType);
        return importer.execute(properties);
    }

    // importers


    protected static class JarFileImportContent implements ApplicationImportContent {

        protected interface Cleaner {
            void clean() throws Exception;
        }

        protected static class FileCleaner implements Cleaner {

            protected File file;

            public FileCleaner(File file) {
                this.file = file;
            }

            @Override
            public void clean() throws Exception {
                file.delete();
            }

        }

        protected JarFile jarFile;

        protected Map<String, ApplicationImportItem> items;

        protected Cleaner cleaner;

        public JarFileImportContent(JarFile jarFile) {
            super();
            items = new HashMap<>();
            this.jarFile = jarFile;
            Iterator<JarEntry> entries = jarFile.entries().asIterator();
            while (entries.hasNext()) {
                JarEntryImportItem item = new JarEntryImportItem(jarFile, entries.next());
                items.put(item.getPath(), item);
            }
        }

        public void setCleaner(Cleaner cleaner) {
            this.cleaner = cleaner;
        }

        @Override
        public Map<String, ApplicationImportItem> getItems() {
            return items;
        }

        @Override
        public void clear() {
            try {
                jarFile.close();
            } catch (IOException ignore) {
                // NOOP
            }
            if (cleaner != null)
                try {
                    cleaner.clean();
                } catch (Exception ignore) {
                    // NOOP
                }
        }

    }

    protected static class JarEntryImportItem implements ApplicationImportItem {

        protected JarFile jarFile;
        protected JarEntry jarEntry;

        protected String name;
        protected String path;
        protected String location;

        public JarEntryImportItem(JarFile jarFile, JarEntry jarEntry) {
            super();
            this.jarEntry = jarEntry;
            this.jarFile = jarFile;
            init();
        }

        protected void init() {
            path = jarEntry.getName();
            int index = path.lastIndexOf('/');
            if (index > 0) {
                name = path.substring(index + 1);
                location = path.substring(0, index);
            } else {
                name = path;
                location = "";
            }
        }

        @Override
        public String getName() {
            return name;
        }

        @Override
        public String getLocation() {
            return location;
        }

        @Override
        public String getPath() {
            return path;
        }

        @Override
        public InputStream getInputStream() throws IOException {
            return jarFile.getInputStream(jarEntry);
        }

    }

    protected abstract static class Importer {

        public abstract ApplicationImportContent execute(Map<String, Object> properties)
                throws Exception;

    }

    protected static class FileImporter extends Importer {

        @Override
        public ApplicationImportContent execute(Map<String, Object> properties) throws Exception {
            String filePath = properties.getOrDefault("filePath", "").toString();
            if (!CommonUtils.isValid(filePath))
                throw new IllegalArgumentException("Invalid filePath: " + filePath);
            File targetFile = new File(filePath);
            if (!targetFile.exists())
                throw new IllegalArgumentException("File not exists: " + filePath);
            if (targetFile.isDirectory())
                throw new IllegalArgumentException("File is directory: " + filePath);
            JarFile jarFile = new JarFile(targetFile);
            return new JarFileImportContent(jarFile);
        }

    }

    protected static class RepositoryImporter extends Importer {

        @Override
        public ApplicationImportContent execute(Map<String, Object> properties) throws Exception {
            String mavenUrl = properties.getOrDefault("mavenUrl", "").toString();
            if (!CommonUtils.isValid(mavenUrl))
                throw new IllegalArgumentException("mavenUrl not set");
            if (!mavenUrl.startsWith("mvn:"))
                mavenUrl = "mvn:" + mavenUrl;

            URL url = new URL(mavenUrl);
            File tempFile = File.createTempFile("app-import-", Calendar.getInstance().getTimeInMillis() + "");
            try (InputStream is = url.openStream()) {
                Files.copy(is, tempFile.toPath(), StandardCopyOption.REPLACE_EXISTING);
            }

            Map<String, Object> props = new HashMap<>();
            props.put("filePath", tempFile.getAbsolutePath());

            ApplicationImportContent result = ExportImportHelper.INSTANCE.doImport("file", props);
            ((JarFileImportContent) result).setCleaner(new JarFileImportContent.FileCleaner(tempFile));

            return result;
        }

    }

    // exporters

    protected abstract static class Exporter {

        public abstract void execute(EntaxyApplicationProjectVersion version, Map<String, Object> properties)
                throws Exception;

    }

    protected static class FileExporter extends Exporter {

        @Override
        public void execute(EntaxyApplicationProjectVersion version, Map<String, Object> properties) throws Exception {
            String filePath = properties.getOrDefault("filePath", "").toString();
            if (filePath.endsWith("/")) {
                // it's directory
                String url = version.getBuildUrl().toString();
                String fileName = url.substring(url.lastIndexOf('/'));
                filePath = filePath.concat(fileName);
            }
            if (!CommonUtils.isValid(filePath))
                return;
            File targetFile = new File(filePath);
            targetFile.getAbsoluteFile().getParentFile().mkdirs();
            try (FileOutputStream fos = new FileOutputStream(targetFile);
                    InputStream is = version.getBuild();) {
                fos.write(is.readAllBytes());
            }
        }

    }

    protected static class RepositoryExporter extends Exporter {

        @Override
        public void execute(EntaxyApplicationProjectVersion version, Map<String, Object> properties) throws Exception {
            ArtifactService artifactService =
                    OSGIUtils.services().ofClass(ArtifactService.class).waitService(20000).get();
            if (artifactService == null)
                throw new ServiceException("Service not found: ArtifactService.class");

            String repositoryName = properties.getOrDefault("repositoryName", "").toString();
            if (!CommonUtils.isValid(repositoryName))
                throw new IllegalArgumentException("repositoryName not set");

            ArtifactRepository repo = artifactService.getRepository(repositoryName);
            if (repo == null)
                throw new IllegalArgumentException("Repository not found: " + repositoryName);
            if (repo.isReadOnly()) {
                throw new IllegalArgumentException("Repository " + repositoryName + " is read only.");
            }

            Artifact artifact = Artifacts.create(Artifact.ARTIFACT_CATEGORY_JAR);
            String mavenCoordinates = version.getBuildInfo().mavenUrl;
            artifact.getCoordinates().update(ArtifactCoordinates.fromUrl(mavenCoordinates));
            try (InputStream is = version.getBuild()) {
                artifact.setContent(is);
                repo.deploy(artifact);
            }

        }

    }
    public static class ApplicationImporter {

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

        protected StorageImportAdapter storageAdapter;

        protected ApplicationDescriptor applicationDescriptor = null;

        public ApplicationImporter(StorageImportAdapter storageAdapter) {
            super();
            this.storageAdapter = storageAdapter;
        }

        public void executeImport(ApplicationImportContent importContent) throws Exception {
            // check descriptor
            String applicationName = "unknown";
            ApplicationImportItem descriptorItem = importContent.getItems().get(EntaxyApplication.DESCRIPTOR_NAME);

            if (descriptorItem == null)
                throw new ApplicationDescriptorNotFound(applicationName);
            try (InputStream is = descriptorItem.getInputStream()) {
                String descriptorData = new String(is.readAllBytes());
                applicationDescriptor =
                        GSON.fromJson(JSONUtils.getJsonRootObject(descriptorData), ApplicationDescriptor.class);
            } catch (Exception e1) {
                throw new ApplicationDescriptorIsInvalid(applicationName);
            }
            if (applicationDescriptor == null)
                throw new ApplicationDescriptorIsInvalid(applicationName);

            // if something's wrong with descriptor, exception is thrown
            storageAdapter.checkDescriptor(applicationDescriptor);
            applicationName = applicationDescriptor.name;

            // if something's wrong, exception is thrown
            StorageApplicationContentAdapter contentAdapter =
                    storageAdapter.getContentAdapter(applicationName, applicationDescriptor);
            fillContent(applicationDescriptor, importContent, contentAdapter);
            EntaxyApplicationVersion createdVersion =
                    storageAdapter.getVersion(applicationName, applicationDescriptor.version);
            if (createdVersion == null) {
                throw new java.lang.InstantiationException("Version not created");
            }
            EntaxyApplicationRevision revision = createdVersion.createRevision();
            if (revision == null) {
                throw new java.lang.InstantiationException("Revision not created");
            }

        }

        public ApplicationDescriptor getApplicationDescriptor() {
            return applicationDescriptor;
        }

        protected void fillContent(ApplicationDescriptor descriptor, ApplicationImportContent source,
                StorageApplicationContentAdapter target) throws Exception {

            List<String> locationsToProcess = new ArrayList<>(source.getItems().keySet());

            // we skip descriptor 'cause it's already there
            locationsToProcess.remove(EntaxyApplication.DESCRIPTOR_NAME);

            // we ignore manifest
            locationsToProcess.remove("META-INF/MANIFEST.MF");


            // load features
            if (descriptor.features == null)
                throw new IllegalArgumentException("Features not found in descriptor");
            StorageItemAdapter item = target.createItem(descriptor.features);
            try (InputStream is = source.getItems().get(descriptor.features.internalLocation).getInputStream()) {
                item.write(is);
            } catch (Exception e) {
                LOG.error("Error writing [" + descriptor.features.internalLocation + "]", e);
            }
            locationsToProcess.remove(descriptor.features.internalLocation);

            // load components
            for (ApplicationComponent comp : descriptor.getComponents()) {
                item = target.createItem(comp);
                try (InputStream is = source.getItems().get(comp.getInternalLocation()).getInputStream()) {
                    item.write(is);
                } catch (Exception e) {
                    LOG.error("Error writing [" + comp.getInternalLocation() + "]", e);
                }
                locationsToProcess.remove(comp.getInternalLocation());
            }

            // left only resources
            for (String location : locationsToProcess) {
                try (InputStream is = source.getItems().get(location).getInputStream()) {
                    target.createResource(location, is);
                } catch (Exception e) {
                    LOG.error("Error writing [" + location + "]", e);
                }
            }

        }

    }
}
