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

import java.io.ByteArrayInputStream;
import java.io.InputStream;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.stream.Collectors;

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

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.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.DeployedArtifact;
import ru.entaxy.platform.core.artifact.ext.ArtifactExtended;
import ru.entaxy.platform.core.artifact.ext.binary.UntypedBinaryInstaller;
import ru.entaxy.platform.core.artifact.ext.features.FeatureInstaller;
import ru.entaxy.platform.core.artifact.ext.features.FeaturesInstaller;
import ru.entaxy.platform.core.artifact.installer.builder.ClusterInstaller;
import ru.entaxy.platform.core.artifact.installer.builder.InstallationResult;
import ru.entaxy.platform.core.artifact.service.ArtifactService;
import ru.entaxy.platform.integration.applications.ApplicationItem;
import ru.entaxy.platform.integration.applications.EntaxyApplication;
import ru.entaxy.platform.integration.applications.EntaxyApplicationRevision.REVISION_STATUS;
import ru.entaxy.platform.integration.applications.descriptor.ApplicationComponent;
import ru.entaxy.platform.integration.applications.descriptor.ApplicationFeatures;
import ru.entaxy.platform.integration.applications.descriptor.ApplicationRequirement;
import ru.entaxy.platform.integration.applications.exceptions.RevisionCausedException;
import ru.entaxy.platform.integration.applications.exceptions.RevisionException;
import ru.entaxy.platform.integration.applications.exceptions.RevisionIllegalTransition;
import ru.entaxy.platform.integration.applications.exceptions.RevisionNotConfigurable;
import ru.entaxy.platform.integration.applications.exceptions.RevisionRequirementsNotSatisfied;
import ru.entaxy.platform.integration.applications.impl.ServiceHelper;
import ru.entaxy.platform.runtime.cellar.sequence.CellarSequenceManager;

public class RevisionHelper {

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

    public static interface RevisionHelperAdapter {
        REVISION_STATUS getCurrentStatus();

        void updateStatus(REVISION_STATUS newStatus);

        int getRevisionNumber();

        void writeItem(String location, InputStream inputStream) throws Exception;
    }

    protected ApplicationContentImpl applicationContent;
    protected RevisionHelperAdapter adapter;

    protected int revisionNumber;

    protected REVISION_STATUS currentStatus;

    protected String applicationName;

    protected String version;

    public RevisionHelper(ApplicationContentImpl applicationContent, RevisionHelperAdapter adapter) {
        super();
        this.applicationContent = applicationContent;
        this.adapter = adapter;
        this.revisionNumber = this.adapter.getRevisionNumber();
        this.currentStatus = this.adapter.getCurrentStatus();
        this.applicationName = applicationContent.getDescriptor().name;
        this.version = applicationContent.getDescriptor().version;
    }

    public void configure() throws RevisionException {
        if (applicationContent.getEditables().isEmpty())
            throw new RevisionNotConfigurable(applicationName,
                    version, revisionNumber);
        // revision 0 is not configured !!!
        if (revisionNumber == 0) {
            throw new RevisionNotConfigurable(applicationName,
                    version, revisionNumber);
        }
        if (!REVISION_STATUS.NEW.equals(currentStatus) && !REVISION_STATUS.CONFIGURED.equals(currentStatus)
                && !REVISION_STATUS.DEPLOYED.equals(currentStatus))
            throw new RevisionIllegalTransition(applicationName, version, revisionNumber, currentStatus,
                    REVISION_STATUS.CONFIGURED);
        currentStatus = REVISION_STATUS.CONFIGURED;
        adapter.updateStatus(currentStatus);
    }

    public void deploy() throws RevisionException {
        if (REVISION_STATUS.INSTALLED.equals(currentStatus) || REVISION_STATUS.UNINSTALLED.equals(currentStatus)
                || REVISION_STATUS.DEPLOYED.equals(currentStatus))
            throw new RevisionIllegalTransition(applicationName, version, revisionNumber, currentStatus,
                    REVISION_STATUS.DEPLOYED);

        try {
            ArtifactService artifactService =
                    OSGIUtils.services().ofClass(ArtifactService.class).waitService(2000).get();

            if (artifactService == null)
                throw new IllegalArgumentException("ArtifactService not available");

            Map<String, ApplicationComponent> componentMap = new HashMap<>();
            componentMap.put(applicationContent.getDescriptor().getFeatures().internalLocation,
                    applicationContent.getDescriptor().getFeatures());
            for (ApplicationComponent comp : applicationContent.getDescriptor().getComponents())
                componentMap.put(comp.internalLocation, comp);

            for (ApplicationItem item : applicationContent.getItems()) {

                ApplicationComponent comp = componentMap.get(item.getLocation());
                if (comp == null)
                    continue;

                Artifact artifact = Artifacts.create(ArtifactExtended.ARTIFACT_CATEGORY_UNTYPED_BINARY);
                artifact.getCoordinates().set(ArtifactCoordinates.fromUrl(comp.mavenLocation));
                try (InputStream is = item.getInputStream()) {
                    artifact.setContent(is);
                    artifactService.deployShared(artifact);
                }
            }

        } catch (RevisionException rethrow) {
            throw rethrow;
        } catch (Exception e) {
            throw new RevisionCausedException(applicationName, e, version, revisionNumber);
        }


        currentStatus = REVISION_STATUS.DEPLOYED;
        adapter.updateStatus(currentStatus);

    }

    public void install() throws RevisionException {
        if (!REVISION_STATUS.DEPLOYED.equals(currentStatus) && !REVISION_STATUS.UNINSTALLED.equals(currentStatus))
            throw new RevisionIllegalTransition(applicationName, version, revisionNumber, currentStatus,
                    REVISION_STATUS.INSTALLED);
        List<ApplicationRequirement> failed = checkRequirements();
        if (!failed.isEmpty())
            throw new RevisionRequirementsNotSatisfied(applicationName, version, revisionNumber, failed);

        try {
            ArtifactService artifactService =
                    OSGIUtils.services().ofClass(ArtifactService.class).waitService(2000).get();

            if (artifactService == null)
                throw new IllegalArgumentException("ArtifactService not available");

            CellarSequenceManager sequenceManager =
                    OSGIUtils.services().ofClass(CellarSequenceManager.class).waitService(2000).get();

            if (sequenceManager == null)
                throw new IllegalArgumentException("CellarSequenceManager not available");


            String sequenceId = applicationContent.getDescriptor().getFeatures().mavenLocation;

            FeaturesInstaller installer =
                    artifactService.installers().cluster().typed(FeaturesInstaller.class).refresh()
                            .inSequence(sequenceId);
            installer.setSourceLocation(applicationContent.getDescriptor().getFeatures().getFullTargetLocation());
            InstallationResult result = installer.install();

            if (!result.isSuccessful()) {
                if (result.getError() != null)
                    throw new Exception(result.getMessage(), result.getError());
            }

            FeatureInstaller featureInstaller = artifactService.installers().cluster().typed(FeatureInstaller.class);
            String featureVersion = ArtifactCoordinates
                    .fromUrl(applicationContent.getDescriptor().getFeatures().mavenLocation).getVersion();
            InstallationResult featureResult = featureInstaller
                    .inSequence(sequenceId)
                    .feature(applicationContent.getDescriptor().getFeatures().getId())
                    .version(featureVersion)
                    .noRefresh()
                    .upgrade()
                    .install();

            // set up sequence

            sequenceManager.getSequence(sequenceId).getEvent().setWaitLast(false);

            // process sequence

            sequenceManager.produceSequence(sequenceId);
            sequenceManager.releaseSequence(sequenceId);

            if (!featureResult.isSuccessful()) {
                InstallationResult firstFailedSubresult = null;
                for (InstallationResult res : featureResult.getSubResults())
                    if (!res.isSuccessful()) {
                        firstFailedSubresult = res;
                        break;
                    }
                if (firstFailedSubresult == null)
                    throw new Exception("Feature installation failed");

                if (firstFailedSubresult.getError() != null)
                    throw new Exception("Feature installation failed: [" + firstFailedSubresult.getObject() != null
                            ? firstFailedSubresult.getObject().toString()
                            : "unknown" + "]", firstFailedSubresult.getError());
                else
                    throw new Exception("Feature installation failed: [" + firstFailedSubresult.getObject() != null
                            ? firstFailedSubresult.getObject().toString()
                            : "unknown" + "]");
            }

            List<String> failedfeatures = new ArrayList<>();
            for (InstallationResult res : featureResult.getSubResults())
                if (!res.isSuccessful()) {
                    // TODO implement rollback if we have failed subresults
                    if (res.getObject() != null)
                        failedfeatures.add(res.getObject().toString());
                    else
                        failedfeatures.add("unknown_" + failedfeatures.size());
                }

            if (!failedfeatures.isEmpty())
                throw new Exception("Feature installation failed: ["
                        + failedfeatures.stream().collect(Collectors.joining(",")) + "]");
        } catch (RevisionException rethrow) {
            throw rethrow;
        } catch (Exception e) {
            throw new RevisionCausedException(applicationName, e, version, revisionNumber);
        }

        currentStatus = REVISION_STATUS.INSTALLED;
        adapter.updateStatus(currentStatus);

    }

    public void installDev() throws RevisionException {
        if (!REVISION_STATUS.DEPLOYED.equals(currentStatus) && !REVISION_STATUS.UNINSTALLED.equals(currentStatus))
            throw new RevisionIllegalTransition(applicationName, version, revisionNumber, currentStatus,
                    REVISION_STATUS.INSTALLED);
        List<ApplicationRequirement> failed = checkRequirements();
        if (!failed.isEmpty())
            throw new RevisionRequirementsNotSatisfied(applicationName, version, revisionNumber, failed);

        try {

            /*
             * Install features repo & *-dev feature: configs only
             */

            ArtifactService artifactService =
                    OSGIUtils.services().ofClass(ArtifactService.class).waitService(2000).get();

            if (artifactService == null)
                throw new IllegalArgumentException("ArtifactService not available");

            CellarSequenceManager sequenceManager =
                    OSGIUtils.services().ofClass(CellarSequenceManager.class).waitService(2000).get();

            if (sequenceManager == null)
                throw new IllegalArgumentException("CellarSequenceManager not available");


            String sequenceId = applicationContent.getDescriptor().getFeatures().mavenLocation;

            FeaturesInstaller installer =
                    artifactService.installers().cluster().typed(FeaturesInstaller.class).refresh()
                            .inSequence(sequenceId);
            installer.setSourceLocation(applicationContent.getDescriptor().getFeatures().getFullTargetLocation());
            InstallationResult result = installer.install();

            if (!result.isSuccessful()) {
                if (result.getError() != null)
                    throw new Exception(result.getMessage(), result.getError());
            }

            FeatureInstaller featureInstaller = artifactService.installers().cluster().typed(FeatureInstaller.class);
            String featureVersion = ArtifactCoordinates
                    .fromUrl(applicationContent.getDescriptor().getFeatures().mavenLocation).getVersion();
            InstallationResult featureResult = featureInstaller
                    .inSequence(sequenceId)
                    .feature(applicationContent.getDescriptor().getFeatures().getDevId())
                    .version(featureVersion)
                    .noRefresh()
                    .upgrade()
                    .install();

            // set up sequence

            sequenceManager.getSequence(sequenceId).getEvent().setWaitLast(false);

            // process sequence

            sequenceManager.produceSequence(sequenceId);
            sequenceManager.releaseSequence(sequenceId);

            if (!featureResult.isSuccessful()) {
                InstallationResult firstFailedSubresult = null;
                for (InstallationResult res : featureResult.getSubResults())
                    if (!res.isSuccessful()) {
                        firstFailedSubresult = res;
                        break;
                    }
                if (firstFailedSubresult == null)
                    throw new Exception("Feature installation failed");

                if (firstFailedSubresult.getError() != null)
                    throw new Exception("Feature installation failed: [" + firstFailedSubresult.getObject() != null
                            ? firstFailedSubresult.getObject().toString()
                            : "unknown" + "]", firstFailedSubresult.getError());
                else
                    throw new Exception("Feature installation failed: [" + firstFailedSubresult.getObject() != null
                            ? firstFailedSubresult.getObject().toString()
                            : "unknown" + "]");
            }

            List<String> failedfeatures = new ArrayList<>();
            for (InstallationResult res : featureResult.getSubResults())
                if (!res.isSuccessful()) {
                    // TODO implement rollback if we have failed subresults
                    if (res.getObject() != null)
                        failedfeatures.add(res.getObject().toString());
                    else
                        failedfeatures.add("unknown_" + failedfeatures.size());
                }

            if (!failedfeatures.isEmpty())
                throw new Exception("Feature installation failed: ["
                        + failedfeatures.stream().collect(Collectors.joining(",")) + "]");

            /*
             * Install components (bundles & blueprints) one by one
             */

            Map<String, ApplicationComponent> componentMap = new HashMap<>();
            for (ApplicationComponent comp : applicationContent.getDescriptor().getComponents())
                if (EntaxyApplication.ITEM_TYPE.BUNDLE.equals(comp.getType()))
                    componentMap.put(comp.internalLocation, comp);


            for (ApplicationItem item : applicationContent.getItems()) {

                ClusterInstaller ci = artifactService.installers().cluster();

                ApplicationComponent comp = componentMap.get(item.getLocation());
                if (comp == null)
                    continue;

                Artifact artifact = Artifacts.create(ArtifactExtended.ARTIFACT_CATEGORY_UNTYPED_BINARY);
                artifact.getCoordinates().set(ArtifactCoordinates.fromUrl(comp.mavenLocation));

                DeployedArtifact da = new DeployedArtifact() {

                    @Override
                    public String getLocation() {
                        return comp.getFullTargetLocation();
                    }

                    @Override
                    public Artifact getArtifact() {
                        return artifact;
                    }
                };

                InstallationResult installationResult =
                        ci.artifact(da).sourceLocation(comp.getFullTargetLocation()).typed(UntypedBinaryInstaller.class)
                                .startLevel(comp.getPriority())
                                .update()
                                .start()
                                .install();

                if (!installationResult.isSuccessful())
                    if (installationResult.getError() != null)
                        throw new Exception(installationResult.getError());
                    else
                        throw new Exception("Installation exceptin: [" + installationResult.getError() + "]");

            }



        } catch (RevisionException rethrow) {
            throw rethrow;
        } catch (Exception e) {
            throw new RevisionCausedException(applicationName, e, version, revisionNumber);
        }

    }

    public void uninstall() throws RevisionException {
        if (!REVISION_STATUS.INSTALLED.equals(currentStatus))
            throw new RevisionIllegalTransition(applicationName, version, revisionNumber, currentStatus,
                    REVISION_STATUS.UNINSTALLED);

        try {
            ArtifactService artifactService =
                    OSGIUtils.services().ofClass(ArtifactService.class).waitService(2000).get();

            if (artifactService == null)
                throw new IllegalArgumentException("ArtifactService not available");

            CellarSequenceManager sequenceManager =
                    OSGIUtils.services().ofClass(CellarSequenceManager.class).waitService(2000).get();

            if (sequenceManager == null)
                throw new IllegalArgumentException("CellarSequenceManager not available");


            String sequenceId = applicationContent.getDescriptor().getFeatures().mavenLocation;

            FeaturesInstaller installer =
                    artifactService.installers().cluster().typed(FeaturesInstaller.class).refresh()
                            .inSequence(sequenceId);
            installer.setSourceLocation(applicationContent.getDescriptor().getFeatures().getFullTargetLocation());
            InstallationResult result = installer.uninstall();

            if (!result.isSuccessful()) {
                if (result.getError() != null)
                    throw new Exception(result.getMessage(), result.getError());
            }

            FeatureInstaller featureInstaller = artifactService.installers().cluster().typed(FeatureInstaller.class);
            String featureVersion = ArtifactCoordinates
                    .fromUrl(applicationContent.getDescriptor().getFeatures().mavenLocation).getVersion();
            InstallationResult featureResult = featureInstaller
                    .inSequence(sequenceId)
                    .feature(applicationContent.getDescriptor().getFeatures().getId())
                    .version(featureVersion)
                    .noRefresh()
                    .uninstall();

            // set up sequence

            sequenceManager.getSequence(sequenceId).getEvent().setWaitLast(false);

            // process sequence

            sequenceManager.produceSequence(sequenceId);
            sequenceManager.releaseSequence(sequenceId);

            if (!featureResult.isSuccessful()) {
                InstallationResult firstFailedSubresult = null;
                for (InstallationResult res : featureResult.getSubResults())
                    if (!res.isSuccessful()) {
                        firstFailedSubresult = res;
                        break;
                    }
                if (firstFailedSubresult == null)
                    throw new Exception("Feature uninstallation failed");

                if (firstFailedSubresult.getError() != null)
                    throw new Exception("Feature uninstallation failed: [" + firstFailedSubresult.getObject() != null
                            ? firstFailedSubresult.getObject().toString()
                            : "unknown" + "]", firstFailedSubresult.getError());
                else
                    throw new Exception("Feature uninstallation failed: [" + firstFailedSubresult.getObject() != null
                            ? firstFailedSubresult.getObject().toString()
                            : "unknown" + "]");
            }

            List<String> failedfeatures = new ArrayList<>();
            for (InstallationResult res : featureResult.getSubResults())
                if (!res.isSuccessful()) {
                    // TODO implement rollback if we have failed subresults
                    if (res.getObject() != null)
                        failedfeatures.add(res.getObject().toString());
                    else
                        failedfeatures.add("unknown_" + failedfeatures.size());
                }

            if (!failedfeatures.isEmpty())
                throw new Exception("Feature installation failed: ["
                        + failedfeatures.stream().collect(Collectors.joining(",")) + "]");
        } catch (RevisionException rethrow) {
            throw rethrow;
        } catch (Exception e) {
            throw new RevisionCausedException(applicationName, e, version, revisionNumber);
        }

        currentStatus = REVISION_STATUS.UNINSTALLED;
        adapter.updateStatus(currentStatus);

    }

    protected List<ApplicationRequirement> checkRequirements() {
        List<ApplicationRequirement> result = new ArrayList<>();
        for (ApplicationRequirement req : applicationContent.getDescriptor().getRequirements()) {
            if (!checkRequirement(req))
                result.add(req);
        }
        return result;
    }

    protected boolean checkRequirement(ApplicationRequirement req) {
        if (EntaxyApplication.ITEM_TYPE.OBJECT.equals(req.getType())) {
            return ServiceHelper.getInstance().objectExists(req.getId());
        }
        if (EntaxyApplication.ITEM_TYPE.RESOURCE.equals(req.getType())) {
            return ServiceHelper.getInstance().resourceExists(req.getId());
        }
        return true;
    }

    public void update() throws Exception {
        updateComponentVersions();
        updateFeatures();
    }

    protected void updateComponentVersions() throws Exception {
        List<ApplicationComponent> components = new ArrayList<>();
        components.add(applicationContent.getDescriptor().features);

        components.addAll(applicationContent.getDescriptor().getComponents());

        List<String> revisionableIds = applicationContent.getItems().stream().filter(item -> item.isRevisionable())
                .map(item -> item.getId()).collect(Collectors.toList());

        for (ApplicationComponent component : components) {

            if (!revisionableIds.contains(component.id))
                continue;

            ArtifactCoordinates coordinates = ArtifactCoordinates.fromUrl(component.mavenLocation);
            coordinates.qualifier(String.format("REV_%03d", revisionNumber));
            component.mavenLocation = coordinates.toMavenString();
            coordinates = ArtifactCoordinates.fromUrl(component.targetLocation);
            // coordinates may be null for configs
            if (coordinates == null)
                continue;
            coordinates.qualifier(String.format("REV_%03d", revisionNumber));
            component.targetLocation = coordinates.toMavenString();
        }
    }

    protected void updateFeatures() throws Exception {

        Template template =
                ServiceHelper.getInstance().getTemplateService().getTemplateById("applications.builder.features");
        TemplateAwareGenerator generator = GeneratorFactory.createGenerator(template);

        if (generator != null) {
            ApplicationFeatures features = applicationContent.getDescriptor().getFeatures();
            ArtifactCoordinates coord = ArtifactCoordinates.fromUrl(features.mavenLocation);
            String repoName = String.format("%s.%s-%s", coord.getGroupId(), coord.getArtifactId(), coord.getVersion());
            String featureName = String.format("%s.%s", coord.getGroupId(), coord.getArtifactId());

            // convert to OSGI-style version
            String featureVersion = coord.getVersion().replace('-', '.');

            Map<String, Object> params = new HashMap<>();
            params.put("descriptor", applicationContent.getDescriptor());
            params.put("repoName", repoName);
            params.put("featureName", featureName);
            params.put("featureVersion", featureVersion);
            params.put("revisionNumber", revisionNumber + "");
            Generated generated;
            generated = generator.generate(template, params);
            if ((generated == null) || (generated.getObject() == null)) {
                throw new IllegalArgumentException("Generated features is null");
            }

            try (ByteArrayInputStream bis = new ByteArrayInputStream(generated.getObject().toString().getBytes())) {
                adapter.writeItem(features.internalLocation, bis);
            }

        } else {
            throw new IllegalArgumentException("Generator not defined for 'applications.builder.features'");
        }

    }
}
