/*
 * JBoss, Home of Professional Open Source.
 * Copyright 2021 Red Hat, Inc., and individual contributors
 * as indicated by the @author tags.
 *
 * Licensed 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.jboss.installer.actions.impl;

import org.jboss.galleon.ProvisioningDescriptionException;
import org.jboss.galleon.ProvisioningException;
import org.jboss.galleon.api.config.GalleonFeaturePackConfig;
import org.jboss.galleon.api.config.GalleonProvisioningConfig;
import org.jboss.galleon.universe.FeaturePackLocation;
import org.jboss.installer.Installer;
import org.jboss.installer.actions.ActionException;
import org.jboss.installer.actions.ActionResult;
import org.jboss.installer.actions.InstallerAction;
import org.jboss.installer.core.LanguageUtils;
import org.jboss.installer.core.MavenRepositoryLoader;
import org.jboss.installer.core.UnarchiveProgressCallback;
import org.jboss.installer.core.UnarchiveProgressCallbackImpl;
import org.jboss.installer.core.BuildProperties;
import org.jboss.uninstaller.UninstallerDataWriter;
import org.wildfly.channel.Repository;
import org.wildfly.channel.UnresolvedMavenArtifactException;
import org.wildfly.prospero.actions.ProvisioningAction;
import org.wildfly.prospero.api.Console;
import org.wildfly.prospero.api.MavenOptions;
import org.wildfly.prospero.api.ProvisioningDefinition;
import org.wildfly.prospero.api.ProvisioningProgressEvent;
import org.wildfly.prospero.api.exceptions.ArtifactResolutionException;
import org.wildfly.prospero.api.exceptions.NoChannelException;
import org.wildfly.prospero.api.exceptions.OperationException;
import org.wildfly.prospero.api.exceptions.UnresolvedChannelMetadataException;

import java.beans.PropertyChangeListener;
import java.beans.PropertyChangeSupport;
import java.io.IOException;
import java.net.URISyntaxException;
import java.net.URL;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.security.CodeSource;
import java.util.ArrayList;
import java.util.List;
import java.util.Map;
import java.util.Objects;

import static org.jboss.galleon.Constants.TRACK_CONFIGS;
import static org.jboss.galleon.Constants.TRACK_LAYOUT_BUILD;
import static org.jboss.galleon.Constants.TRACK_PACKAGES;
import static org.jboss.installer.actions.impl.MavenLocalRepositoryHelper.determineLocalRepository;
import static org.jboss.installer.screens.ComponentSelectionScreen.DOCS_PACKAGE;
import static org.jboss.installer.screens.InstallationScreen.CLEANUP_STAGE_KEY;
import static org.jboss.installer.screens.InstallationScreen.CONFIG_STAGE_KEY;
import static org.jboss.installer.screens.InstallationScreen.DOWNLOAD_STAGE_KEY;
import static org.jboss.installer.screens.InstallationScreen.EXTRACT_ARCHIVE_KEY;
import static org.jboss.installer.screens.InstallationScreen.EXTRA_CONFIG_STAGE_KEY;
import static org.jboss.installer.screens.InstallationScreen.FEATURE_PACK_STAGE_KEY;
import static org.jboss.installer.screens.InstallationScreen.MODULES_STAGE_KEY;
import static org.jboss.installer.screens.InstallationScreen.PACKAGES_STAGE_KEY;

import static org.jboss.installer.core.LoggerUtils.taskLog;

public class InstallEap implements InstallerAction {

    public static final String MAVEN_REPO_PATH = System.getProperty("maven.repo.path");
    private static final String INSTALL_EAP_PROVISION_FAILED = "install.eap.provision.failed";
    private final PropertyChangeListener changeListener;
    private final Path targetPath;
    private final List<String> excludedPackages;

    private final Map<String, URL> mavenRepositories;
    private final LanguageUtils langUtils;
    private final ProvisioningExceptionHandler exceptionHandler;

    public InstallEap(Path targetPath, PropertyChangeListener changeListener, List<String> excludedPackages,
                      Map<String, URL> mavenRepositories, LanguageUtils langUtils) {
        this.targetPath = targetPath;
        this.changeListener = changeListener;
        this.excludedPackages = excludedPackages;
        this.mavenRepositories = mavenRepositories;
        this.langUtils = langUtils;
        this.exceptionHandler = new ProvisioningExceptionHandler(langUtils);
    }

    @Override
    public ActionResult perform(PropertyChangeSupport propertyChangeSupport) throws ActionException {
        propertyChangeSupport.addPropertyChangeListener(changeListener);

        Path localRepoPath;
        UnarchiveProgressCallback unarchiveProgressCallback = new UnarchiveProgressCallbackImpl(propertyChangeSupport);
        try {
            if (MAVEN_REPO_PATH != null) {
                localRepoPath = Paths.get(MAVEN_REPO_PATH);
            } else {
                localRepoPath = determineLocalRepository();
            }

            taskLog.debug("localRepoPath set to : " + localRepoPath);

            final MavenOptions mavenOptions = MavenOptions.builder()
                    .setLocalCachePath(localRepoPath)
                    .setOffline(false)
                    .build();

            final List<Repository> repos = new ArrayList<>();
            for (String repoId : mavenRepositories.keySet()) {
                String repoUrl = MavenRepositoryLoader
                        .extractIfNeeded(mavenRepositories.get(repoId), unarchiveProgressCallback)
                        .toExternalForm();
                repos.add(new Repository(repoId, repoUrl));
                taskLog.debug("load maven repository " + repoId + " : " + repoUrl);
            }

            final String fplGA = System.getProperty(Installer.INSTALLER_OVERRIDE_FPL);
            final String manifestGA = System.getProperty(Installer.INSTALLER_OVERRIDE_MANIFEST);

            final ProvisioningDefinition.Builder defBuilder = ProvisioningDefinition.builder()
                    .setOverrideRepositories(repos);

            if (manifestGA != null && ! manifestGA.isEmpty()) {
                defBuilder.setManifest(manifestGA);
            }

            if (fplGA != null && !fplGA.isEmpty()) {
                defBuilder.setFpl(fplGA);
            } else {
                // uses profile defined in prospero
                defBuilder.setProfile(BuildProperties.PROSPERO_INSTALLATION_PROFILE);
            }

            final ProvisioningDefinition def = defBuilder.build();

            final Console console = new ConsoleAdapter(propertyChangeSupport);

            final GalleonProvisioningConfig provisioningConfig = excludePackages(def.toProvisioningConfig());

            new ProvisioningAction(targetPath, mavenOptions, console).provision(provisioningConfig, def.resolveChannels(null));
            createUninstallerJar();

            // fire the even to let UI know we're really done now
            propertyChangeSupport.firePropertyChange(ProvisioningProgressEvent.EventType.COMPLETED.name(), null, Stage.CLEANUP);
        } catch (ProvisioningException e) {
            throw exceptionHandler.handle(e);
        } catch (IOException | NoChannelException e) {
            throw new ActionException(langUtils.getString(INSTALL_EAP_PROVISION_FAILED), e);
        } catch (ArtifactResolutionException | UnresolvedChannelMetadataException e) {
            throw new ActionException(langUtils.getString(INSTALL_EAP_PROVISION_FAILED) + "\n" + e.getMessage(), e);
        } catch (OperationException e) {
            taskLog.error("Unable to install server", e);
            throw exceptionHandler.handle(e);
        } catch (UnresolvedMavenArtifactException e) {

        }

        return ActionResult.success();
    }

    private GalleonProvisioningConfig excludePackages(GalleonProvisioningConfig originConfig) throws ProvisioningDescriptionException {
        final GalleonProvisioningConfig.Builder builder = GalleonProvisioningConfig.builder(originConfig);

        final FeaturePackLocation fpl = FeaturePackLocation.fromString(BuildProperties.EAP_FEATURE_PACK_LOCATION);
        final GalleonFeaturePackConfig eapFpConfig = originConfig.getFeaturePackDep(fpl.getProducer());
        final GalleonFeaturePackConfig.Builder fpBuilder = GalleonFeaturePackConfig.builder(eapFpConfig);
        boolean docsExcluded = false;
        for (String excludedPackage : excludedPackages) {
            // exclude all sub-packages of docs
            if (excludedPackage.equals(DOCS_PACKAGE)) {
                docsExcluded = true;
                for (String includedPackage : eapFpConfig.getIncludedPackages()) {
                    if (includedPackage.startsWith(DOCS_PACKAGE + ".")) {
                        fpBuilder.removeIncludedPackage(includedPackage);
                    }
                }
            }
            fpBuilder.excludePackage(excludedPackage);
        }

        if (!docsExcluded) {
            fpBuilder.includePackage("docs.examples.configs");
        }

        builder.updateFeaturePackDep(fpBuilder.build());
        return builder.build();
    }

    public enum Stage {
        ZIP_EXTRACT(EXTRACT_ARCHIVE_KEY),
        FEATURE_PACK(FEATURE_PACK_STAGE_KEY),
        PACKAGES(PACKAGES_STAGE_KEY),
        DOWNLOAD(DOWNLOAD_STAGE_KEY),
        MODULES(MODULES_STAGE_KEY),
        CONFIGURATION(CONFIG_STAGE_KEY),
        EXTRA_CONFIG(EXTRA_CONFIG_STAGE_KEY),
        CLEANUP(CLEANUP_STAGE_KEY);

        private final String key;

        Stage(String key) {
            this.key = key;
        }

        public String getKey() {
            return key;
        }

        public static Stage fromGalleonStage(String updateStage) {
            switch (updateStage) {
                case TRACK_LAYOUT_BUILD:
                    return Stage.FEATURE_PACK;
                case TRACK_PACKAGES:
                    return Stage.PACKAGES;
                case TRACK_CONFIGS:
                    return Stage.CONFIGURATION;
                case "JBMODULES":
                    return Stage.MODULES;
                case "JBEXTRACONFIGS":
                    return Stage.EXTRA_CONFIG;
                case "JB_ARTIFACTS_RESOLVE":
                    return Stage.DOWNLOAD;
                default:
                    throw new IllegalArgumentException("Unknown stage" + updateStage);
            }
        }
    }

    public static class Progress {
        private String currentElement;
        private int progress;
        private Stage current;

        public Progress(String currentElement, int progress, Stage current) {
            this.currentElement = currentElement;
            this.progress = progress;
            this.current = current;
        }

        public String getCurrentElement() {
            return currentElement;
        }

        public int getProgress() {
            return progress;
        }

        public Stage getCurrent() {
            return current;
        }

        @Override
        public boolean equals(Object o) {
            if (this == o) return true;
            if (!(o instanceof Progress)) return false;
            Progress progress1 = (Progress) o;
            return getProgress() == progress1.getProgress() && Objects.equals(getCurrentElement(), progress1.getCurrentElement()) && getCurrent() == progress1.getCurrent();
        }

        @Override
        public int hashCode() {
            return Objects.hash(getCurrentElement(), getProgress(), getCurrent());
        }
    }

    private static class ConsoleAdapter implements Console {
        private final PropertyChangeSupport propertyChangeSupport;

        public ConsoleAdapter(PropertyChangeSupport propertyChangeSupport) {
            this.propertyChangeSupport = propertyChangeSupport;
        }

        @Override
        public void progressUpdate(ProvisioningProgressEvent update) {
            String type;
            Stage stage = Stage.fromGalleonStage(update.getStage());
            InstallEap.Progress progress = null;

            // todo: fold it into the enum
            switch (update.getEventType()) {
                case UPDATE:
                    progress = new InstallEap.Progress(null, (int) Math.round(update.getProgress()), stage);
                    break;
                case STARTING:
                    break;
                case COMPLETED:
                    break;
                default:
                    throw new IllegalArgumentException("Unknown event type: " + update.getEventType());
            }

            final Object payload = update.getEventType() == ProvisioningProgressEvent.EventType.UPDATE?progress:stage;
            // TODO: add new stages
            switch (update.getStage()) {
                case "JBEXTRACONFIGS":
                    propertyChangeSupport.firePropertyChange(update.getEventType().name(), null, payload);
                    if (update.getEventType() == ProvisioningProgressEvent.EventType.COMPLETED) {
                        propertyChangeSupport.firePropertyChange(ProvisioningProgressEvent.EventType.STARTING.name(), null, InstallEap.Stage.CLEANUP);
                    }
                    break;
                default:
                    propertyChangeSupport.firePropertyChange(update.getEventType().name(), null, payload);
                    break;
            }
        }

        @Override
        public void println(String text) {

        }
    }

    private void createUninstallerJar() throws ActionException {
        try {
            final CodeSource codeSource = this.getClass().getProtectionDomain().getCodeSource();
            final Path codeSourcePath = Path.of(codeSource.getLocation().toURI());

            new UninstallerDataWriter(targetPath, codeSourcePath).createJar();

        } catch (ActionException | URISyntaxException e) {
            throw new ActionException("Failed to create uninstaller jar: " + e.getMessage(), e);
        }
    }
}
