/*-
 * ~~~~~~licensing~~~~~~
 * artifact-loader
 * ==========
 * 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.initializer.artifactloader;

import java.io.File;
import java.io.IOException;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.nio.file.WatchEvent;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.stream.Collectors;
import java.util.stream.Stream;

import org.apache.camel.tooling.model.Strings;
import org.apache.felix.utils.properties.Properties;
import org.osgi.service.component.annotations.Activate;
import org.osgi.service.component.annotations.Component;
import org.osgi.service.component.annotations.Deactivate;
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.platform.base.support.CommonUtils;
import ru.entaxy.platform.base.support.SimpleFileWatcher;
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.installer.builder.InstallationResult;
import ru.entaxy.platform.core.artifact.installer.builder.Installer;
import ru.entaxy.platform.core.artifact.installer.builder.typed.CommonBundleInstaller;
import ru.entaxy.platform.core.artifact.repository.impl.DeployedArtifactImpl;
import ru.entaxy.platform.core.artifact.service.ArtifactService;

@Component(service = ArtifactInstaller.class, immediate = true)
public class ArtifactInstaller implements SimpleFileWatcher.WatcherCallback {

    public static final Logger log = LoggerFactory.getLogger(ArtifactInstaller.class);

    public static final String ROOT_PATH = "init/install";

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

    protected ExecutorService watchExecutor = Executors.newSingleThreadExecutor();

    protected ExecutorService installExecutor = Executors.newCachedThreadPool();

    protected boolean installThreadIsRunning = false;

    protected Object installThreadLock = new Object();

    // protected List<InstallationTask> currentTasks = Collections.synchronizedList(new
    // ArrayList<>());

    protected final Map<String, InstallationTask> currentTasks = new HashMap<>();

    protected final Object currentTasksLock = new Object();

    protected SimpleFileWatcher installPropertiesWatcher = null;

    protected Path rootPath;


    @Activate
    public void activate() {
        watchExecutor = Executors.newSingleThreadExecutor();
        installExecutor = Executors.newCachedThreadPool();
    }

    @Deactivate
    public void deactivate() {
        watchExecutor.shutdown();
        installExecutor.shutdown();
    }

    public void start() {

        rootPath = Paths.get(System.getProperty("karaf.etc"), ROOT_PATH);

        try {
            installPropertiesWatcher = new SimpleFileWatcher(rootPath);
            installPropertiesWatcher.setWatcherCallback(this);
            watchExecutor.submit(installPropertiesWatcher);
        } catch (IOException e) {
            log.error("FAILED creating file watcher for ArtifactDeployer", e);
        }

        // do the initial job

        try (Stream<Path> pathStream = Files.walk(rootPath)) {
            List<File> fileList = pathStream
                    .map(p -> p.toFile())
                    .filter(f -> f.exists())
                    .filter(f -> !f.isDirectory())
                    .collect(Collectors.toList());
            for (File f : fileList) {
                log.info("INITIAL processing file: " + f.getAbsolutePath());
                List<InstallationTask> tasks = parseProperties(f.getAbsoluteFile().toPath());
                if (tasks != null && !tasks.isEmpty())
                    submitTasks(tasks);
            }

        } catch (Exception e) {
            log.error("Error processing files", e);
        }


    }

    protected List<InstallationTask> parseProperties(Path propertiesFile) {

        List<InstallationTask> result = new ArrayList<>();

        Properties properties;
        try {

            List<String> lines = Files.readAllLines(propertiesFile);

            for (String line : lines) {
                if (Strings.isNullOrEmpty(line))
                    continue;
                if (line.startsWith("#"))
                    continue;
                String[] parsed = line.split("=");
                if (parsed.length < 2) {
                    log.warn("incorrect line [{}] in file [{}]", line, propertiesFile);
                    continue;
                }
                String url = parsed[0].trim();
                String paramsData = parsed[1].trim();
                InstallationTask task = InstallationTask.parseParams(paramsData);
                task.artifactUrl = url;
                task.createArtifacts();
                result.add(task);
            }

            /*
            properties = new Properties(propertiesFile.toFile());
            
            for (Entry<String, String> entry : properties.entrySet()) {
                String url = entry.getKey();
                String paramsData = entry.getValue();
                InstallationTask task = InstallationTask.parseParams(paramsData);
                task.artifactUrl = url;
                task.createArtifacts();
                result.add(task);
            }
            */
        } catch (IOException e) {
            log.error("Failed reading file [" + propertiesFile.toString() + "]", e);
        }

        return result;

    }

    protected void submitTasks(List<InstallationTask> tasks) {
        log.info("SUBMITTING TASKS: " + tasks.size());
        int newTasks = 0;
        synchronized (currentTasksLock) {
            for (InstallationTask task : tasks)
                if (!this.currentTasks.containsKey(task.artifactUrl)) {
                    this.currentTasks.putIfAbsent(task.artifactUrl, task);
                    newTasks++;
                }
        }
        log.debug("TOTAL TASKS: " + this.currentTasks.size() + "; NEW TASKS: " + newTasks);
        if (newTasks == 0)
            return;
        synchronized (installThreadLock) {
            if (!installThreadIsRunning) {
                log.debug("STARTING INSTALLER");
                startInstaller();
            } else {
                log.debug("INSTALLER ALREADY STARTED");
            }
        }
    }

    protected void startInstaller() {
        if (installExecutor.isTerminated() || installExecutor.isShutdown()) {
            log.debug("INSTALLER IS TERMINATED -> RECREATING");
            installExecutor = Executors.newCachedThreadPool();
        }

        installExecutor.submit(new InstallerWorker());
    }

    /* -- WatcherCallback implementation */

    @Override
    public void fileCreated(WatchEvent<?> event, Path directory) {
        Path name = ((WatchEvent<Path>) event).context();
        Path fileFullPath = directory.resolve(name);
        log.info("NEW FILE FOUND: [{}]", fileFullPath);

        // let the file be finished:
        // on Windows it can happen that we read new file
        // while it's not finished so being empty
        try {
            Thread.sleep(1000);
        } catch (InterruptedException ignore) {
            // NOOP
        }

        List<InstallationTask> tasks = parseProperties(fileFullPath);
        log.debug("TASKS: [{}]", tasks == null ? 0 : tasks.size());
        if (tasks != null && !tasks.isEmpty())
            submitTasks(tasks);
    }

    protected class InstallerWorker implements Runnable {

        @Override
        public void run() {

            synchronized (installThreadLock) {
                if (installThreadIsRunning) {
                    log.debug(" >> ANOTHER THREAD IS ALREADY RUNNING");
                    return;
                }
                installThreadIsRunning = true;
            }

            List<InstallationTask> completed = new ArrayList<>();

            List<InstallationTask> expired = new ArrayList<>();

            log.debug(" >> TOTAL TASKS: " + currentTasks.size());

            Map<String, InstallationTask> localMap = new HashMap<>();

            synchronized (currentTasksLock) {
                localMap.putAll(currentTasks);
            }

            while (!localMap.isEmpty()) {

                for (InstallationTask task : localMap.values()) {

                    log.debug(" >> EXECUTING task for [{}]", task.artifactUrl);

                    Installer<?> installer = task.installShared ? artifactService.installers().cluster()
                            : artifactService.installers().local();

                    installer.artifact(task.deployedArtifact);

                    CommonBundleInstaller bundleInstaller = installer.typed(CommonBundleInstaller.class);
                    InstallationResult result =
                            bundleInstaller.installOnlyIfMissing()
                                    .forceInstall()
                                    .start().startLevel(task.startLevel).install();

                    if (result != null && result.isSuccessful()) {
                        completed.add(task);
                        log.info("Successfully installed: [{}]", task.artifactUrl);
                    } else {
                        task.retriesCount--;
                        if (task.retriesCount <= 0) {
                            expired.add(task);
                            log.warn("Installation failed: expired: [{}]", task.artifactUrl);
                            if (!result.isSuccessful() && CommonUtils.isValid(result.getMessage()))
                                log.error("Installation error for [{}]: {}", task.artifactUrl, result.getMessage());

                        }
                    }

                }

                synchronized (currentTasksLock) {
                    for (InstallationTask task : completed)
                        currentTasks.remove(task.artifactUrl);
                    for (InstallationTask task : expired)
                        currentTasks.remove(task.artifactUrl);
                    log.debug(" >> TOTAL TASKS LEFT: " + currentTasks.size());
                    localMap.clear();
                    localMap.putAll(currentTasks);
                }

                if (localMap.isEmpty()) {
                    log.debug(" >> EXITING");
                    break;
                } else {
                    log.debug(" >> CONTINUE");
                }

                try {
                    Thread.sleep(1000);
                } catch (InterruptedException e) {
                    log.debug("Interrupted");
                }
            }
            synchronized (installThreadLock) {
                installThreadIsRunning = false;
            }

        }

    }

    protected static class InstallationTask {

        protected static final String SHARED_TARGET = "shared";

        public static InstallationTask parseParams(String data) {
            InstallationTask result = new InstallationTask();

            String[] values = data.split(";");
            result.installShared = values[0].equals(SHARED_TARGET);

            if (values.length > 1)
                result.artifactCategory = values[1].trim();

            if (values.length > 2)
                try {
                    result.startLevel = Integer.parseInt(values[2].trim());
                } catch (Exception ignore) {

                }

            return result;
        }

        String artifactUrl = "";

        boolean installShared = true;

        String artifactCategory = "jar";

        int startLevel = 100;

        Artifact artifact;

        DeployedArtifact deployedArtifact;

        int retriesCount = 100;

        public void createArtifacts() {

            Artifact artifact = Artifacts.create(artifactCategory);

            artifact.getCoordinates().set(ArtifactCoordinates.fromUrl(artifactUrl));

            deployedArtifact = new DeployedArtifactImpl(artifact, "mvn:" + artifactUrl);

        }

    }

}
