/*-
 * ~~~~~~licensing~~~~~~
 * object-producer-core
 * ==========
 * 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.core.producer.executor.support;

import java.util.HashMap;
import java.util.Map;

import org.osgi.framework.Constants;
import org.osgi.service.component.annotations.Component;
import org.osgi.service.component.annotations.Reference;
import org.osgi.service.component.annotations.ReferenceCardinality;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import ru.entaxy.esb.platform.runtime.base.connecting.generator.Generated;
import ru.entaxy.platform.base.objects.EntaxyObject.FIELDS;
import ru.entaxy.platform.base.objects.EntaxyObject.HEADERS;
import ru.entaxy.platform.base.objects.factory.EntaxyFactory;
import ru.entaxy.platform.base.support.CommonUtils;
import ru.entaxy.platform.core.artifact.Artifact;
import ru.entaxy.platform.core.artifact.ArtifactManifest;
import ru.entaxy.platform.core.artifact.Artifacts;
import ru.entaxy.platform.core.artifact.Blueprint;
import ru.entaxy.platform.core.artifact.DeployedArtifact;
import ru.entaxy.platform.core.artifact.Manifested;
import ru.entaxy.platform.core.artifact.capabilities.ManifestCapabilityHelper;
import ru.entaxy.platform.core.artifact.installer.builder.ClusterInstaller;
import ru.entaxy.platform.core.artifact.installer.builder.InstallationResult;
import ru.entaxy.platform.core.artifact.installer.builder.Installer;
import ru.entaxy.platform.core.artifact.installer.builder.LocalInstaller;
import ru.entaxy.platform.core.artifact.installer.builder.typed.BlueprintInstaller;
import ru.entaxy.platform.core.artifact.service.ArtifactService;
import ru.entaxy.platform.core.producer.api.EntaxyProducerService;
import ru.entaxy.platform.core.producer.executor.CommandInstructions;
import ru.entaxy.platform.core.producer.executor.builder.BuiltObject;
import ru.entaxy.platform.core.producer.executor.builder.DefaultBuiltObject;
import ru.entaxy.platform.core.producer.executor.builder.ObjectBuilder;
import ru.entaxy.platform.core.producer.executor.commands.Deploy;
import ru.entaxy.platform.core.producer.executor.commands.Install;
import ru.entaxy.platform.core.producer.executor.deployer.DefaultDeployedObject;
import ru.entaxy.platform.core.producer.executor.deployer.DeployedObject;
import ru.entaxy.platform.core.producer.executor.deployer.ObjectDeployer;
import ru.entaxy.platform.core.producer.executor.generationmodel.GeneratedHeaders;
import ru.entaxy.platform.core.producer.executor.installer.DefaultInstalledObject;
import ru.entaxy.platform.core.producer.executor.installer.InstalledObject;
import ru.entaxy.platform.core.producer.executor.installer.ObjectInstaller;

@Component(service = {ObjectBuilder.class, ObjectDeployer.class, ObjectInstaller.class}, immediate = true)
public class ArtifactSupport implements ObjectBuilder, ObjectDeployer, ObjectInstaller {

    private static final Logger log = LoggerFactory.getLogger(ArtifactSupport.class);

    // TODO move to EntaxyFactory
    public static final String DIRECTIVE_SKIP_PROVIDE = "@SKIP_PROVIDE";

    // TODO move to EntaxyFactory
    public static final String HEADER_OBJECT_MAP = "@OBJECT-MAP";

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

    /*
     * ObjectBuilder
     */

    @Override
    public boolean isBuildable(Generated generated) {
        Artifact artifact = Artifacts.fromGenerated(generated);
        if ((artifact == null) || Artifact.ARTIFACT_CATEGORY_UNKNOWN.equals(artifact.getCategory())
                || !CommonUtils.isValid(artifact.getCategory())) {
            return false;
        }
        return true;
    }

    @Override
    public BuiltObject build(Generated generated, Map<String, Object> instructions) throws Exception {

        LocalPrintOutput printer = new LocalPrintOutput(instructions);

        Artifact artifact = Artifacts.fromGenerated(generated);
        if ((artifact == null) || Artifact.ARTIFACT_CATEGORY_UNKNOWN.equals(artifact.getCategory())
                || !CommonUtils.isValid(artifact.getCategory())) {
            log.info("Artifact is not buildable");
            return null;
        }
        log.info("Built artifact of category [{}]", artifact.getCategory());
        Map<String, String> headers = GeneratedHeaders.getHeaders(
                // generated.getProperties()
                EntaxyObjectPropertiesHelper.getMainObjectProperties(generated.getProperties())).getAsStringMap();

        String mainObjectValue = headers.get(HEADERS.MAIN_OBJECT);
        if (!CommonUtils.isValid(mainObjectValue))
            throw new Exception("Main object not found");
        String[] mainObjectData = mainObjectValue.split(":");
        if (mainObjectData.length < 2)
            throw new Exception("Main object not complete: [" + mainObjectValue + "]");

        String versionPolicy = generated.getProperties().getOrDefault(
                EntaxyProducerService.INSTRUCTIONS.ARTIFACT.VERSION_POLICY,
                instructions.getOrDefault(
                        EntaxyProducerService.INSTRUCTIONS.ARTIFACT.VERSION_POLICY, ""))
                .toString();
        String timestamp = generated.getProperties().getOrDefault(
                EntaxyProducerService.INSTRUCTIONS.ARTIFACT.TIMESTAMP,
                instructions.getOrDefault(
                        EntaxyProducerService.INSTRUCTIONS.ARTIFACT.TIMESTAMP, ""))
                .toString();
        artifact.getCoordinates()
                .groupId(mainObjectData[1])
                .artifactId(mainObjectData[0])
                .version("1")
                .versionPolicy(versionPolicy)
                .timestamp(timestamp);

        if (artifact instanceof Manifested) {
            ArtifactManifest manifest = ((Manifested) artifact).getManifest();

            // Generate provided capabilities for every included object
            ManifestCapabilityHelper helper = new ManifestCapabilityHelper(manifest);
            helper.setMultipleNamespacesSupported(true);

            Map<String, String> objectMap = new HashMap<>();
            String objectMapValue = headers.getOrDefault(HEADER_OBJECT_MAP, "");
            if (CommonUtils.isValid(objectMapValue)) {
                String[] entries = objectMapValue.split(",");
                for (String entry : entries) {
                    String[] data = entry.split("::");
                    if (data.length < 2)
                        continue;
                    // we invert map from published id to origin id
                    objectMap.put(data[1], data[0]);
                }
            }

            headers.remove(HEADER_OBJECT_MAP);

            manifest.getCustomAttributes().putAll(headers);

            String objectsValue = headers.getOrDefault(HEADERS.GENERATED_OBJECTS, "");
            if (CommonUtils.isValid(objectsValue)) {
                String[] objects = objectsValue.split(",");

                for (int i = 0; i < objects.length; i++) {
                    String[] objectData = objects[i].split(":");
                    if (objectData.length < 2)
                        continue;
                    String objectId = objectData[0];
                    String objectType = objectData[1];
                    if (!CommonUtils.isValid(objectId) || !CommonUtils.isValid(objectType))
                        continue;
                    objectId = objectId.trim();
                    objectType = objectType.trim();

                    if (objectMap.containsKey(objectId + ":" + objectType)) {
                        String mappedValue = objectMap.get(objectId + ":" + objectType);
                        String[] mappedData = mappedValue.split(":");
                        if (mappedData.length > 1) {
                            objectId = mappedData[0];
                            objectType = mappedData[1];
                        }
                    }

                    Map<String, Object> attributes = null;
                    Map<String, Object> objectProperties = EntaxyObjectPropertiesHelper
                            .getPropertiesFor(objectId, objectType, generated.getProperties());
                    boolean skipProvide = Boolean.parseBoolean(
                            objectProperties.getOrDefault(DIRECTIVE_SKIP_PROVIDE, false).toString());
                    if (skipProvide)
                        continue;
                    if (objectProperties.containsKey(FIELDS.FIELDS_TO_PUBLISH)) {
                        Object map = objectProperties.get(FIELDS.FIELDS_TO_PUBLISH);
                        if (map != null) {
                            attributes = (Map<String, Object>) ((Map<String, Object>) map).get(objectId);
                            for (String directive : EntaxyFactory.CONFIGURATION.DIRECTIVES.getDesignDirectives())
                                attributes.remove(directive);
                        }
                    }
                    helper.provideCapability(objectType).attributes(attributes);
                }

                helper.save();

            }
        }
        artifact.getProperties().putAll(generated.getProperties());
        // TODO get value from manifest
        // ArtifactManifest must be improved to provide read access to all attributes
        if (!artifact.getProperties().containsKey(Constants.BUNDLE_SYMBOLICNAME)) {
            artifact.getProperties().put(Constants.BUNDLE_SYMBOLICNAME, artifact.getCoordinates().getGroupId()
                    + "." + artifact.getCoordinates().getArtifactId());
        }
        printer.printOutput("\n\t == " + artifact.getCoordinates().toString() + " ==\n");
        printer.printOutput(new String(artifact.asByteArray()));
        printer.printOutput("\n\t == \n");
        return new DefaultBuiltObject(artifact);
    }

    /*
     * ObjectDeployer
     */

    @Override
    public boolean isDeployable(BuiltObject object) {
        return object.getObject() instanceof Artifact;
    }

    @Override
    public DeployedObject deploy(BuiltObject object, Map<String, Object> instructions) throws Exception {

        if (!(object.getObject() instanceof Artifact)) {
            return null;
        }

        LocalPrintOutput printer = new LocalPrintOutput(instructions);

        boolean deployLocal = false;

        Object deployLocalValue = instructions.get(Deploy.DEPLOY_LOCAL_INSTRUCTION);
        if (deployLocalValue != null)
            if (deployLocalValue instanceof Boolean)
                deployLocal = (Boolean) deployLocalValue;

        Artifact artifact = (Artifact) object.getObject();

        DeployedArtifact da = deployLocal
                ? artifactService.deployLocal(artifact)
                : artifactService.deployShared(artifact);

        printer.printOutput("DEPLOYED: ["
                + da.getLocation()
                + "]");

        return new DefaultDeployedObject(da);
    }

    /*
     * ObjectInstaller
     */

    @Override
    public boolean isInstallable(DeployedObject object) {
        return object.getObject() instanceof DeployedArtifact;
    }

    @Override
    public InstalledObject install(DeployedObject object, Map<String, Object> instructions) throws Exception {

        if (!isInstallable(object))
            return null;

        LocalPrintOutput printer = new LocalPrintOutput(instructions);

        CommandInstructions commandInstructions = new CommandInstructions(instructions);

        boolean installLocal =
                commandInstructions.has(Install.INSTALL_LOCAL_INSTRUCTION)
                        ? commandInstructions.getBoolean(Install.INSTALL_LOCAL_INSTRUCTION)
                        : false;

        boolean installOnlyIfMissing =
                commandInstructions.has(Install.INSTALL_ONLY_IF_MISSING_INSTRUCTION)
                        ? commandInstructions.getBoolean(Install.INSTALL_ONLY_IF_MISSING_INSTRUCTION)
                        : false;

        String update =
                commandInstructions.has(Install.UPDATE_INSTRUCTION)
                        ? commandInstructions.getString(Install.UPDATE_INSTRUCTION)
                        : null;

        long startLevelValue =
                commandInstructions.has(Install.START_LEVEL_INSTRUCTION)
                        ? commandInstructions.getLong(Install.START_LEVEL_INSTRUCTION)
                        : -1;

        DeployedArtifact da = (DeployedArtifact) object.getObject();

        InstallationResult result = null;

        Installer<?> installer = null;

        String artifactUpdate = update;

        printer.printOutput("-> Installing artifact: [" + da.getArtifact().getCoordinates().toString() + "]");
        if (installLocal) {
            LocalInstaller localInstaller = artifactService.installers().local()
                    .artifact(da);
            installer = localInstaller;
            printer.printOutput("-> Installing locally");
        } else {
            ClusterInstaller clusterInstaller = artifactService.installers().cluster()
                    .artifact(da);
            installer = clusterInstaller;
            printer.printOutput("-> Installing clustered");
        }

        // TODO add support for other types when they appear
        if (da.getArtifact().getCategory().equals(Blueprint.ARTIFACT_CATEGORY_BLUEPRINT)) {
            // we're installing blueprint
            printer.printOutput("-> Installing: " + da.getArtifact().getCategory());
            BlueprintInstaller blueprintInstaller = installer.typed(BlueprintInstaller.class);
            if (installOnlyIfMissing)
                blueprintInstaller.installOnlyIfMissing();
            if (artifactUpdate != null) {
                if (!CommonUtils.isValid(artifactUpdate)) {
                    artifactUpdate = da
                            .getArtifact().getProperties()
                            .getOrDefault(Constants.BUNDLE_SYMBOLICNAME, "")
                            .toString();
                }
                blueprintInstaller.update(artifactUpdate);
            }
            if (startLevelValue > 0)
                blueprintInstaller.startLevel((int) startLevelValue);
            result = blueprintInstaller.start().install();
        } else {
            printer.printOutput("-> Unknown category: " + da.getArtifact().getCategory());
        }

        if (result != null) {
            if (result.getResult().equals(InstallationResult.Result.FAILED)) {
                if (result.getError() != null)
                    throw (Exception) result.getError();
                throw new RuntimeException(result.getMessage());
            } else {
                for (String key : da.getArtifact().getProperties().keySet())
                    result.getProperties().putIfAbsent(key, da.getArtifact().getProperties().get(key));
                // TODO imrove Coordinates: add "asMap" method
                result.getProperties().put("artifact.artifactId", da.getArtifact().getCoordinates().getArtifactId());
                result.getProperties().put("artifact.groupId", da.getArtifact().getCoordinates().getGroupId());
                result.getProperties().put("artifact.version", da.getArtifact().getCoordinates().getVersion());
                result.getProperties().put("artifact.type", da.getArtifact().getCoordinates().getType());
                result.getProperties().put("artifact.classifier", da.getArtifact().getCoordinates().getClassifier());
            }
        }


        return new DefaultInstalledObject(result);
    }
}
