/*
 * Licensed to the Apache Software Foundation (ASF) under one or more
 * contributor license agreements.  See the NOTICE file distributed with
 * this work for additional information regarding copyright ownership.
 * The ASF licenses this file to You under the Apache License, Version 2.0
 * (the "License"); you may not use this file except in compliance with
 * the License.  You may obtain a copy of the License at
 *
 *      http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */
package org.apache.unomi.shell.migration.service;

import groovy.lang.GroovyClassLoader;
import groovy.lang.GroovyShell;
import groovy.util.GroovyScriptEngine;
import org.apache.commons.lang3.StringUtils;
import org.apache.http.auth.AuthScope;
import org.apache.http.auth.UsernamePasswordCredentials;
import org.apache.http.client.CredentialsProvider;
import org.apache.http.impl.client.BasicCredentialsProvider;
import org.apache.http.impl.client.CloseableHttpClient;
import org.apache.karaf.shell.api.console.Session;
import org.apache.unomi.shell.migration.MigrationException;
import org.apache.unomi.shell.migration.MigrationService;
import org.apache.unomi.shell.migration.utils.HttpUtils;
import org.osgi.framework.Bundle;
import org.osgi.framework.BundleContext;
import org.osgi.framework.Version;
import org.osgi.framework.wiring.BundleWiring;
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 java.io.IOException;
import java.net.URL;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.util.*;
import java.util.concurrent.TimeUnit;
import java.util.stream.Collectors;
import java.util.stream.Stream;

import static org.apache.unomi.shell.migration.service.MigrationConfig.*;

@Component(service = MigrationService.class, immediate = true)
public class MigrationServiceImpl implements MigrationService {

    public static final String MIGRATION_FS_ROOT_FOLDER = "migration";
    public static final Path MIGRATION_FS_SCRIPTS_FOLDER = Paths.get(System.getProperty( "karaf.data" ), MIGRATION_FS_ROOT_FOLDER, "scripts");

    private BundleContext bundleContext;

    @Reference(cardinality = ReferenceCardinality.MANDATORY)
    private MigrationConfig migrationConfig;

    @Activate
    public void activate(ComponentContext componentContext) {
        this.bundleContext = componentContext.getBundleContext();
    }

    public void migrateUnomi(String originVersion, boolean skipConfirmation, Session session) throws Exception {
        System.out.println("Migrating Unomi...");
        // Wait for config to be loaded by file install, in case of unomi.autoMigrate the OSGI conf may take a few seconds to be loaded correctly.
        waitForMigrationConfigLoad(60, 1);

        // Load migration scrips
        Set<MigrationScript> scripts = loadOSGIScripts();
        scripts.addAll(loadFileSystemScripts());

        // Create migration context
        Files.createDirectories(MIGRATION_FS_SCRIPTS_FOLDER);
        MigrationContext context = new MigrationContext(session, migrationConfig);
        context.tryRecoverFromHistory();

        // no origin version, just print available scripts
        if (originVersion == null) {
            displayMigrations(scripts, context);
            context.printMessage("Select your migration starting point by specifying the current version (e.g. 1.2.0) or the last script that was already run (e.g. 1.2.1)");
            return;
        }

        // Check that there is some migration scripts available from given version
        Version fromVersion = new Version(originVersion);
        scripts = filterScriptsFromVersion(scripts, fromVersion);
        if (scripts.size() == 0) {
            context.printMessage("No migration scripts available found starting from version: " + originVersion);
            return;
        } else {
            context.printMessage("The following migration scripts starting from version: " + originVersion + " will be executed.");
            displayMigrations(scripts, context);
        }

        // Check for user approval before migrate
        if (!skipConfirmation && context.askUserWithAuthorizedAnswer(
                "[WARNING] You are about to execute a migration, this a very sensitive operation, are you sure? (yes/no): ",
                Arrays.asList("yes", "no")).equalsIgnoreCase("no")) {
            context.printMessage("Migration process aborted");
            return;
        }

        // Handle credentials
        CredentialsProvider credentialsProvider = null;
        String login = context.getConfigString(CONFIG_ES_LOGIN);
        if (StringUtils.isNotEmpty(login)) {
            credentialsProvider = new BasicCredentialsProvider();
            UsernamePasswordCredentials credentials
                    = new UsernamePasswordCredentials(login, context.getConfigString(CONFIG_ES_PASSWORD));
            credentialsProvider.setCredentials(AuthScope.ANY, credentials);
        }

        try (CloseableHttpClient httpClient = HttpUtils.initHttpClient(context.getConfigBoolean(CONFIG_TRUST_ALL_CERTIFICATES), credentialsProvider)) {

            // Compile scripts
            context.setHttpClient(httpClient);
            scripts = parseScripts(scripts, context);

            // Start migration
            context.printMessage("Starting migration process from version: " + originVersion);
            for (MigrationScript migrateScript : scripts) {
                context.printMessage("Starting execution of: " + migrateScript);
                try {
                    migrateScript.getCompiledScript().run();
                } catch (MigrationException e) {
                    context.printException("Error executing: " + migrateScript);
                    throw e;
                } catch (Exception e) {
                    context.printException("Error executing: " + migrateScript, e);
                    throw e;
                }

                context.printMessage("Finish execution of: " + migrateScript);
            }

            // Persist final flag in history
            context.performMigrationStep("migrationStatus", () -> { /* nothing it's just a marker to persist in the history to know that everything is finished */ });
        }
    }

    private void waitForMigrationConfigLoad(int maxTry, int secondsToSleep) throws InterruptedException {
        while (!migrationConfig.getConfig().containsKey("felix.fileinstall.filename")) {
            maxTry -= 1;
            if (maxTry == 0) {
                throw new IllegalStateException("Waited too long for migration config to be available");
            } else {
                TimeUnit.SECONDS.sleep(secondsToSleep);
            }
        }
    }

    private void displayMigrations(Set<MigrationScript> scripts, MigrationContext context) {
        Version previousVersion = new Version("0.0.0");
        for (MigrationScript migration : scripts) {
            if (migration.getVersion().getMajor() > previousVersion.getMajor() || migration.getVersion().getMinor() > previousVersion.getMinor()) {
                context.printMessage("From " + migration.getVersion().getMajor() + "." + migration.getVersion().getMinor() + ".0:");
            }
            context.printMessage("- " + migration);
            previousVersion = migration.getVersion();
        }
    }

    private Set<MigrationScript> filterScriptsFromVersion(Set<MigrationScript> scripts, Version fromVersion) {
        return scripts.stream()
                .filter(migrateScript -> fromVersion.compareTo(migrateScript.getVersion()) < 0)
                .collect(Collectors.toCollection(TreeSet::new));
    }

    private Set<MigrationScript> parseScripts(Set<MigrationScript> scripts, MigrationContext context) {
        Map<String, GroovyShell> shellsPerBundle = new HashMap<>();

        return scripts.stream()
                .peek(migrateScript -> {
                    // fallback on current bundle if the scripts is not provided by OSGI
                    Bundle scriptBundle = migrateScript.getBundle() != null ? migrateScript.getBundle() : bundleContext.getBundle();
                    if (!shellsPerBundle.containsKey(scriptBundle.getSymbolicName())) {
                        shellsPerBundle.put(scriptBundle.getSymbolicName(), buildShellForBundle(scriptBundle, context));
                    }
                    migrateScript.setCompiledScript(shellsPerBundle.get(scriptBundle.getSymbolicName()).parse(migrateScript.getScript()));
                })
                .collect(Collectors.toCollection(TreeSet::new));
    }

    private Set<MigrationScript> loadOSGIScripts() throws IOException {
        SortedSet<MigrationScript> migrationScripts = new TreeSet<>();
        for (Bundle bundle : bundleContext.getBundles()) {
            Enumeration<URL> scripts = bundle.findEntries("META-INF/cxs/migration", "*.groovy", true);
            if (scripts != null) {
                // check for shell

                while (scripts.hasMoreElements()) {
                    URL scriptURL = scripts.nextElement();
                    migrationScripts.add(new MigrationScript(scriptURL, bundle));
                }
            }
        }

        return migrationScripts;
    }

    private Set<MigrationScript> loadFileSystemScripts() throws IOException {
        // check migration folder exists
        if (!Files.isDirectory(MIGRATION_FS_SCRIPTS_FOLDER)) {
            return Collections.emptySet();
        }

        List<Path> paths;
        try (Stream<Path> walk = Files.walk(MIGRATION_FS_SCRIPTS_FOLDER)) {
            paths = walk
                    .filter(path -> !Files.isDirectory(path))
                    .filter(path -> path.toString().toLowerCase().endsWith("groovy"))
                    .collect(Collectors.toList());
        }

        SortedSet<MigrationScript> migrationScripts = new TreeSet<>();
        for (Path path : paths) {
            migrationScripts.add(new MigrationScript(path.toUri().toURL(), null));
        }
        return migrationScripts;
    }

    private GroovyShell buildShellForBundle(Bundle bundle, MigrationContext context) {
        GroovyClassLoader groovyLoader = new GroovyClassLoader(bundle.adapt(BundleWiring.class).getClassLoader());
        GroovyScriptEngine groovyScriptEngine = new GroovyScriptEngine((URL[]) null, groovyLoader);
        GroovyShell groovyShell = new GroovyShell(groovyScriptEngine.getGroovyClassLoader());
        groovyShell.setVariable("migrationContext", context);
        groovyShell.setVariable("bundleContext", bundle.getBundleContext());
        return groovyShell;
    }

    public void setMigrationConfig(MigrationConfig migrationConfig) {
        this.migrationConfig = migrationConfig;
    }
}
