/*-
 * ~~~~~~licensing~~~~~~
 * connection-initializer
 * ==========
 * 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.esb.platform.runtime.core.initializer.connection;

import java.io.File;
import java.io.FilenameFilter;
import java.net.URL;
import java.util.List;
import java.util.Optional;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.stream.Collectors;

import org.osgi.service.component.ComponentContext;
import org.osgi.service.component.annotations.Activate;
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.ReferencePolicy;
import org.osgi.service.component.annotations.ReferencePolicyOption;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import com.google.gson.JsonObject;

import ru.entaxy.esb.platform.runtime.core.initializer.api.AbstractStandaloneInitializer;
import ru.entaxy.esb.platform.runtime.core.initializer.api.Initialized;
import ru.entaxy.esb.platform.runtime.core.initializer.api.StandaloneInitializer;
import ru.entaxy.platform.base.objects.factory.exceptions.FactoryNotFoundException;
import ru.entaxy.platform.base.support.FileUtils;
import ru.entaxy.platform.base.support.JSONUtils;
import ru.entaxy.platform.core.artifact.ArtifactCoordinates;
import ru.entaxy.platform.core.producer.api.EntaxyProducerService;
import ru.entaxy.platform.core.producer.api.EntaxyProducerUtils;
import ru.entaxy.platform.core.producer.api.ProducerResult;
import ru.entaxy.platform.core.producer.api.ProducerResult.CommandResult;
import ru.entaxy.platform.core.producer.executor.generationmodel.GenerateTask;
import ru.entaxy.platform.core.producer.executor.generationmodel.GenerationModel;
import ru.entaxy.platform.core.producer.executor.installer.InstalledObjectList;
import ru.entaxy.platform.core.producer.executor.objectmodel.ObjectModel;

@Component(service = ConnectionInitializer.class, immediate = true)
@StandaloneInitializer(id = "connections")
public class ConnectionInitializer extends AbstractStandaloneInitializer {

    protected static final Logger log = LoggerFactory.getLogger(ConnectionInitializer.class);

    private static final String INIT_FILES_PATH = System.getProperty("karaf.etc")
            + File.separator
            + "init";

    @Reference(cardinality = ReferenceCardinality.MANDATORY, policy = ReferencePolicy.DYNAMIC,
            policyOption = ReferencePolicyOption.GREEDY, target = "(id=core)")
    volatile Initialized coreInitializedMarker;

    @Reference(cardinality = ReferenceCardinality.MANDATORY, policy = ReferencePolicy.DYNAMIC,
            policyOption = ReferencePolicyOption.GREEDY, target = "(id=datasources)")
    volatile Initialized datasourcesInitializedMarker;


    @Reference
    EntaxyProducerService entaxyProducerService;

    ExecutorService threadPool = Executors.newCachedThreadPool();

    int globalRetriesCount = 10;

    int globalRetryInterval = 2000;

    boolean factoryFilesTaskResult = false;

    public void setFactoryFilesTaskResult(boolean factoryFilesTaskResult) {
        this.factoryFilesTaskResult = factoryFilesTaskResult;
    }

    protected void checkAndRegister() {
        if (factoryFilesTaskResult)
            register();
    }


    protected class FactoryFilesTask implements Runnable {

        protected CommandResult getCommandResult(final String id, ProducerResult producerResult) {
            Optional<CommandResult> result =
                    producerResult.getCommandResults().stream().filter(c -> c.commandId.equals(id))
                            .findFirst();
            if (result.isEmpty())
                return null;
            return result.get();
        }

        protected boolean checkResult(ProducerResult producerResult) {
            CommandResult cr = getCommandResult("install", producerResult);
            if (cr == null)
                return false;
            if (!cr.result || cr.skipped)
                return false;

            try {
                GenerationModel generationModel = producerResult.findResultObject(GenerationModel.class);
                List<GenerateTask> generateTasks = generationModel.getTasks().stream()
                        .filter(t -> (t instanceof GenerateTask))
                        .filter(t -> !t.isToMerge())
                        .map(t -> (GenerateTask) t)
                        .collect(Collectors.toList());

                InstalledObjectList installedList = producerResult.findResultObject(InstalledObjectList.class);

                log.debug("FactoryFilesTask :: Generated:[{}]; Installed:[{}]", generateTasks.size(),
                        installedList.size());

                return installedList.size() == generateTasks.size();

            } catch (Exception e) {
                return false;
            }

            // return true;
        }

        @Override
        public void run() {

            int retriesLeft = globalRetriesCount;

            File initDir = new File(INIT_FILES_PATH);
            if (!initDir.exists() || !initDir.isDirectory()) {
                log.info("FactoryFilesTask :: initDir not available");
                return;
            }

            File[] files = initDir.listFiles(new FilenameFilter() {

                @Override
                public boolean accept(File dir, String name) {
                    return name.endsWith(".json");
                }
            });

            if ((files == null) || (files.length == 0)) {
                log.info("FactoryFilesTask :: files not found in initDir");
                return;
            }
            while (retriesLeft > 0) {
                try {

                    for (int i = 0; i < files.length; i++) {
                        log.debug("FactoryFilesTask :: FOUND FILE :: " + files[i].getName());

                        //
                        // check if the file contains objects
                        //

                        FileUtils.FileHelper helper = new FileUtils.FileHelper(files[i].getAbsolutePath());
                        if (!helper.isReadable()) {
                            // TODO throw exception
                            log.warn("FactoryFilesTask :: file {} is not readable", files[i].getAbsolutePath());
                            continue;
                        }
                        JsonObject jsonData;
                        try {
                            URL url = files[i].toURI().toURL();
                            jsonData = JSONUtils.getJsonRootObject(url);
                        } catch (Exception e) {
                            log.warn("FactoryFilesTask :: Configuration loading failed:" + e.getMessage());
                            continue;
                        }

                        ObjectModel objectModel = new ObjectModel();

                        try {
                            objectModel.load(jsonData);
                            objectModel.checkRefs();
                        } catch (Exception e) {
                            log.warn("FactoryFilesTask :: Error loading model from " + files[i].getAbsolutePath(), e);
                            continue;
                        }

                        if (objectModel.objects.size() == 0)
                            continue;

                        log.debug("FactoryFilesTask :: CONTAINS OBJECTS :: " + files[i].getName());

                        //
                        // check availability of factories
                        //
                        List<String> factoryIds = objectModel.objects.stream()
                                .map(obj -> obj.factoryId)
                                .collect(Collectors.toList());
                        for (String factoryId : factoryIds) {
                            String message = "CHECK FACTORY :: " + factoryId + "..";
                            if (entaxyProducerService.findFactoryById(factoryId) == null) {
                                message += " NOT FOUND";
                                log.debug(message);
                                throw new FactoryNotFoundException("Factory not found: " + factoryId);// (this,
                                // "Factory
                                // not
                                // found:
                                // "
                                // +
                                // factoryId,
                                // "",
                                // null);
                            }
                            message += " FOUND";
                            log.debug(message);
                        }

                        log.debug("FactoryFilesTask :: FACTORIES AVAILABLE :: " + files[i].getName());

                        //
                        // produce objects
                        //

                        EntaxyProducerUtils.InstructionsBuilder builder = EntaxyProducerUtils.instructions()
                                .lifecycle("general")
                                .any()
                                .command("add-config")
                                .command("pre-generate")
                                .set(EntaxyProducerService.INSTRUCTIONS.PRINT_OUTPUT, false)
                                .command("build")
                                .set(EntaxyProducerService.INSTRUCTIONS.ARTIFACT.VERSION_POLICY,
                                        ArtifactCoordinates.VERSION_POLICY_DATED_EMBEDDED)
                                .command("deploy")
                                .set("deployLocal", true);

                        if (helper.isChanged()) {
                            log.info("FactoryFilesTask :: File is new or changed, install/update connections: "
                                    + files[i].getAbsolutePath());
                            // we need to create/update connections with new timestamp
                            String oldTimestamp = helper.getTimestamp();
                            String newTimestamp = helper.updateTimestamp();

                            builder
                                    .command("build")
                                    .set(EntaxyProducerService.INSTRUCTIONS.ARTIFACT.TIMESTAMP, newTimestamp)
                                    .command("install")
                                    .set("update", "")
                                    .set("installLocal", true)
                                    .set("startLevel", 75);

                            String instructions = builder
                                    .getInstructionsString();

                            log.info("FactoryFilesTask :: running with instructions [" + instructions + "]");

                            ProducerResult result = entaxyProducerService.produce(jsonData, instructions);

                            log.debug("FactoryFilesTask :: last result command ["
                                    + result.getLastCommandResult().commandId + "]");

                            if (!result.getAllExceptions().isEmpty()) {
                                throw new Exception("ProducerResult contains exceptions");
                            }

                            if (!checkResult(result)) {
                                throw new Exception("ProducerResult not valid");
                            }
                            helper.updateMd5();
                        } else {
                            log.info("FactoryFilesTask :: File is not changed, install/check connection: "
                                    + files[i].getAbsolutePath());

                            String oldTimestamp = helper.getTimestamp();

                            builder
                                    .command("build")
                                    .set(EntaxyProducerService.INSTRUCTIONS.ARTIFACT.TIMESTAMP, oldTimestamp)
                                    .command("install")
                                    .set("installOnlyIfMissing", true)
                                    .set("installLocal", true)
                                    .set("startLevel", 75);

                            String instructions = builder
                                    .getInstructionsString();

                            ProducerResult result = entaxyProducerService.produce(jsonData, instructions);
                            if (!result.getAllExceptions().isEmpty()) {
                                throw new Exception("ProducerResult contains exceptions");
                            }
                            if (!checkResult(result)) {
                                throw new Exception("ProducerResult not valid");
                            }
                        }

                    }

                    setFactoryFilesTaskResult(true);
                    checkAndRegister();
                    return;

                } catch (Exception e) {
                    log.error(
                            String.format("Error in FactoryFilesTask; iteration=%d", globalRetriesCount - retriesLeft),
                            e);
                    try {
                        Thread.sleep(globalRetryInterval);
                    } catch (InterruptedException e1) {
                        log.warn("FactoryFilesTask interrupted");
                        checkAndRegister();
                        return;
                    }
                }
                retriesLeft--;
            }

            checkAndRegister();

        }

    }

    @Activate
    public void activate(ComponentContext componentContext) {
        log.info("ConnectionInitializer started");

        setBundleContext(componentContext.getBundleContext());

        threadPool.execute(new FactoryFilesTask());
    }
}
