/**
 * 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
 * <p>
 * http://www.apache.org/licenses/LICENSE-2.0
 * <p>
 * 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.postinstall.task;

import com.google.auto.service.AutoService;
import org.jboss.as.controller.OperationFailedException;
import org.jboss.as.controller.PathAddress;
import org.jboss.dmr.ModelNode;
import org.jboss.installer.auto.AutomaticInstallationParsingException;
import org.jboss.installer.core.FlatListPostInstallConfig;
import org.jboss.installer.core.InstallationData;
import org.jboss.installer.postinstall.CliPostInstallTask;
import org.jboss.installer.postinstall.PostInstallTask;
import org.jboss.installer.postinstall.TaskPrinter;
import org.jboss.installer.postinstall.server.DomainServer;
import org.jboss.installer.postinstall.server.StandaloneServer;
import org.jboss.installer.postinstall.task.utils.FilePermissionUtils;

import java.io.IOException;
import java.nio.file.Path;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Map;
import java.util.Set;
import java.util.function.BiFunction;

import static org.jboss.as.controller.operations.common.Util.createEmptyOperation;
import static org.jboss.installer.core.LoggerUtils.taskLog;

@AutoService(PostInstallTask.class)
public class CredentialStoreInstallTask implements CliPostInstallTask {

    public static final String TASK_NAME_KEY = "credential_store.task.name";
    protected static final String KEY_CREDENTIAL_STORE_DIR = "secret-key-store";
    private static final String SECRET_KEY_STORE_RESOLVER = "secret-key-store-resolver";
    protected static final String KEY_CREDENTIAL_STORE_NAME = "secret-key-credential-store";

    @Override
    public boolean applyToStandalone(InstallationData data, StandaloneServer server, TaskPrinter printer) {
        printer.print("tasks.add_cred_store.started", server.currentConfiguration());
        final Config config = data.getConfig(Config.class);
        final ModelNode initialStoreOp = addSecretKeyStoreOp(data.getTargetFolder(), false);
        final ModelNode resolverOp = addSecretKeyStoreResolverOp();

        final String encryptedPassword;
        try {
            server.execute(initialStoreOp, "Install secret key credential store");
            server.execute(resolverOp, "Install secret key resolver");
            final ModelNode res = server.execute(encryptPasswordOp(config.password), "Encrypt password");
            encryptedPassword = res.get("expression").asString();
        } catch (OperationFailedException e) {
            taskLog.error("CLI operation failed", e);
            printer.print("tasks.add_cred_store.failed");
            printer.print(e);
            return false;
        }

        final ModelNode addKsOp = addCredentialStoreOp(config, encryptedPassword);
        try {
            server.execute(addKsOp, "Install keystore");
        } catch (OperationFailedException e) {
            taskLog.error("CLI operation failed", e);
            printer.print("tasks.add_cred_store.failed");
            printer.print(e);
            return false;
        }

        final Path initialKeyStore = data.getTargetFolder().resolve(StandaloneServer.CONFIG_DIR).resolve(KEY_CREDENTIAL_STORE_DIR);
        try {
            FilePermissionUtils.setOwnerOnly(initialKeyStore);
        } catch (IOException | FilePermissionUtils.UnsupportedFileSystemException e) {
            taskLog.warn("Failed to limit access to credential store + " + initialKeyStore, e);
            printer.print("tasks.permission_cred_store.failed", initialKeyStore.toString());
            // complete the task, but log a warning
            return true;
        }

        return true;
    }

    @Override
    public boolean applyToDomain(InstallationData data, DomainServer server, TaskPrinter printer) {
        printer.print("tasks.add_cred_store.started", server.currentConfiguration());
        final Config config = data.getConfig(Config.class);

        final ModelNode initialStoreOp = addSecretKeyStoreOp(data.getTargetFolder(), true);
        final ModelNode resolverOp = addSecretKeyStoreResolverOp();

        final String encryptedPassword;
        try {
            if (server.currentConfiguration().equals("host.xml")) {
                server.executeOnProfiles(initialStoreOp, "Install secret key credential store");
                server.executeOnProfiles(resolverOp, "Install secret key resolver");
            }

            server.execute(server.wrapInHost(initialStoreOp), "Install secret key credential store");
            server.execute(server.wrapInHost(resolverOp), "Install secret key resolver");
            final ModelNode res = server.execute(server.wrapInHost(encryptPasswordOp(config.password)), "Encrypt password");
            encryptedPassword = res.get("expression").asString();
        } catch (OperationFailedException e) {
            taskLog.error("CLI operation failed", e);
            printer.print("tasks.add_cred_store.failed");
            printer.print(e);
            return false;
        }

        final ModelNode addKsOp = addCredentialStoreOp(config, encryptedPassword);
        try {
            // only execute this part once
            if (server.currentConfiguration().equals("host.xml")) {
                server.executeOnProfiles(addKsOp, "Install keystore on domain");
            }
            server.execute(server.wrapInHost(addKsOp), "Install keystore on host");
        } catch (OperationFailedException e) {
            taskLog.error("CLI operation failed", e);
            printer.print("tasks.add_cred_store.failed");
            printer.print(e);
            return false;
        }

        final Path initialKeyStore = data.getTargetFolder().resolve(DomainServer.CONFIG_DIR).resolve(KEY_CREDENTIAL_STORE_DIR);
        try {
            FilePermissionUtils.setOwnerOnly(initialKeyStore);
        } catch (IOException | FilePermissionUtils.UnsupportedFileSystemException e) {
            taskLog.warn("Failed to limit access to credential store + " + initialKeyStore, e);
            printer.print("tasks.permission_cred_store.failed", initialKeyStore.toString());
            // complete the task, but log a warning
            return true;
        }

        printer.print("tasks.add_cred_store.finished", config.getPath());
        return true;
    }

    private ModelNode addSecretKeyStoreOp(Path baseFolder, boolean domain) {
        final ModelNode addKsOp = createEmptyOperation("add",
                PathAddress.pathAddress("subsystem", "elytron").append("secret-key-credential-store", KEY_CREDENTIAL_STORE_NAME));
        addKsOp.get("path").set(Path.of(KEY_CREDENTIAL_STORE_DIR, "properties-store.cs").toString());
        addKsOp.get("relative-to").set(domain?"jboss.domain.config.dir":"jboss.server.config.dir");
        return addKsOp;
    }

    private ModelNode addSecretKeyStoreResolverOp() {
        final ModelNode addKsOp = createEmptyOperation("add",
                PathAddress.pathAddress("subsystem", "elytron").append("expression", "encryption"));
        final ModelNode resolver = new ModelNode();
        resolver.get("name").set(SECRET_KEY_STORE_RESOLVER);
        resolver.get("credential-store").set(KEY_CREDENTIAL_STORE_NAME);
        resolver.get("secret-key").set("key");
        addKsOp.get("resolvers").add(resolver);
        return addKsOp;
    }

    private ModelNode encryptPasswordOp(String password) {
        final ModelNode addKsOp = createEmptyOperation("create-expression",
                PathAddress.pathAddress("subsystem", "elytron").append("expression", "encryption"));
        addKsOp.get("resolver").set(SECRET_KEY_STORE_RESOLVER);
        addKsOp.get("clear-text").set(password);
        return addKsOp;
    }

    private ModelNode addCredentialStoreOp(Config config, String encryptedPassword) {
        final ModelNode addKsOp = createEmptyOperation("add",
                PathAddress.pathAddress("subsystem", "elytron").append("credential-store", config.getStoreName()));
        addKsOp.get("path").set(config.getPath());
        final ModelNode modelNode = new ModelNode();
        modelNode.get("clear-text").set(encryptedPassword);
        addKsOp.get("credential-reference").set(modelNode);
        addKsOp.get("create").set(true);
        if (config.getRelativeTo() != null) {
            addKsOp.get("relative-to").set(config.getRelativeTo());
        }
        return addKsOp;
    }

    @Override
    public String getName() {
        return TASK_NAME_KEY;
    }

    @Override
    public String getSerializationName() {
        return "add-credential-store";
    }

    @Override
    public Class<? extends InstallationData.PostInstallConfig> getConfigClass() {
        return Config.class;
    }

    public static class Config extends FlatListPostInstallConfig {

        private String storeName;
        private String relativeTo;
        private String path;
        private String password;

        public Config() {

        }

        public Config(String storeName, String relativeTo, String path, String password) {
            this.storeName = storeName;
            this.relativeTo = relativeTo;
            this.path = path;
            this.password = password;
        }

        public String getStoreName() {
            return storeName;
        }

        public String getRelativeTo() {
            return relativeTo;
        }

        public String getPath() {
            return path;
        }

        public String getPassword() {
            return password;
        }

        @Override
        protected Map<String, String> listAttributes() {
            final HashMap<String, String> attrs = new HashMap<>();
            attrs.put("storeName", storeName);
            attrs.put("relativeTo", relativeTo);
            attrs.put("path", path);
            return attrs;
        }

        @Override
        protected Set<String> listVariables() {
            final HashSet<String> vars = new HashSet<>();
            vars.add("credential-store.password");
            return vars;
        }

        @Override
        protected void acceptAttributes(Map<String, String> attributes, BiFunction<String, String, String> variableResolver) throws AutomaticInstallationParsingException {
            this.storeName = attributes.get("storeName");
            this.relativeTo = attributes.get("relativeTo");
            this.path = attributes.get("path");
            this.password = variableResolver.apply("credential-store.password", "Credential store password:");
        }
    }
}
