/*
 * JBoss, Home of Professional Open Source.
 * Copyright 2022 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.postinstall.task.impl;

import static org.jboss.installer.postinstall.task.utils.ModelUtils.createEmptyOperation;
import static org.jboss.installer.postinstall.task.utils.ModelUtils.pathAddress;

import org.apache.commons.lang3.tuple.Pair;
import org.jboss.installer.postinstall.task.CredentialStoreConfig;
import org.jboss.installer.postinstall.task.utils.ModelDescriptionConstants;
import org.jboss.dmr.ModelNode;
import org.jboss.installer.core.InstallationData;
import org.jboss.installer.postinstall.TaskPrinter;
import org.jboss.installer.postinstall.server.EmbeddedServer;
import org.jboss.installer.postinstall.server.ServerOperationException;
import org.jboss.installer.postinstall.task.AbstractHttpsEnableConfig;
import org.jboss.installer.postinstall.task.utils.CredentialStoreUtil;

import java.io.File;
import java.util.ArrayList;
import java.util.List;
import java.util.Optional;
import java.util.Set;

abstract class AbstractHttpsEnableTask {

    public static final String JBOSS_SERVER_CONFIG_DIR = "jboss.server.config.dir";
    public static final String JBOSS_DOMAIN_CONFIG_DIR = "jboss.domain.config.dir";
    public static final String KEY_ALG = "RSA";
    public static final int KEY_SIZE = 2048;
    public static final String SERVER_KEY_ALIAS = "serverkey";
    public static final String CLIENT_KEY_ALIAS = "clientkey";

    protected abstract Class<? extends AbstractHttpsEnableConfig> getConfigClass();

    protected List<Pair<ModelNode, String>> getCommonOperations(InstallationData data, EmbeddedServer server,
                                                                String sslContextName,
                                                                String credentialAliasKeyStorePassword,
                                                                String credentialAliasTrustStorePassword,
                                                                String defaultKeyStoreFileName, String keyStoreName,
                                                                String keyManagerName,
                                                                String defaultTrustStoreFileName, String trustStoreName,
                                                                String trustManagerName,
                                                                TaskPrinter printer) throws ServerOperationException {
        return getCommonOperations(data, server, sslContextName, credentialAliasKeyStorePassword, credentialAliasTrustStorePassword,
                defaultKeyStoreFileName, keyStoreName, keyManagerName, defaultTrustStoreFileName, trustStoreName,
                trustManagerName, true, printer);
    }

    protected List<Pair<ModelNode, String>> getCommonOperations(InstallationData data, EmbeddedServer server,
                                                                String sslContextName,
                                                                String credentialAliasKeyStorePassword,
                                                                String credentialAliasTrustStorePassword,
                                                                String defaultKeyStoreFileName, String keyStoreName,
                                                                String keyManagerName,
                                                                String defaultTrustStoreFileName, String trustStoreName,
                                                                String trustManagerName,
                                                                boolean supportsAdvancedModifiableKeyStoreOperations,
                                                                TaskPrinter printer) throws ServerOperationException {
        final AbstractHttpsEnableConfig config = data.getConfig(getConfigClass());
        final Optional<CredentialStoreConfig> csConfig
                = Optional.ofNullable(data.getConfig(CredentialStoreConfig.class));

        final String keyStorePath = config.isDoGenerateStore() ? defaultKeyStoreFileName : config.getKeyStorePath();
        final String relativeTo = config.isDoGenerateStore() ? server.isDomain() ? JBOSS_DOMAIN_CONFIG_DIR: JBOSS_SERVER_CONFIG_DIR : null;

        final List<Pair<ModelNode, String>> ops = new ArrayList<>();

        if (csConfig.isPresent() && !server.aliasExists(credentialAliasKeyStorePassword, csConfig.get().getStoreName())) {
            printer.print("tasks.application_ssl.store_password",csConfig.get().getStoreName(), credentialAliasKeyStorePassword);
            server.encryptPassword(credentialAliasKeyStorePassword, csConfig.get().getStoreName(), config.getKeyStorePassword());
        }

        final ModelNode addKeystoreOp = getAddKeystoreOp(keyStoreName, keyStorePath, relativeTo, csConfig, credentialAliasKeyStorePassword, config.getKeyStorePassword());
        ops.add(Pair.of(addKeystoreOp, "Add keystore"));

        final ModelNode addKeyManagerOp = getAddKeymanagerOp(keyManagerName, keyStoreName, csConfig, credentialAliasKeyStorePassword, config.getKeyStorePassword());
        ops.add(Pair.of(addKeyManagerOp, "Add keymanager"));

        if (config.isDoGenerateStore() && supportsAdvancedModifiableKeyStoreOperations) {
            // generate the keystore only once for standalone and once for domain
            final boolean wasKeystoreGenerated = (server.isDomain() && config.isKeystoreGeneratedForDomain())
                            || (!server.isDomain() && config.isKeystoreGeneratedForStandalone());
            if (!wasKeystoreGenerated) {
                printer.print("tasks.application_ssl.generate_keystore");
                // generate key pair
                ops.add(Pair.of(getGenerateKeyPairOp(config.getDn(), config.getValidity(), keyStoreName), "Generate new key pair"));
                // store keystore
                ops.add(Pair.of(getStoreKeystoreOp(keyStoreName), "Store keystore"));
                // export new certificate from the key store
                final String certName = keyStorePath + ".pem";
                final String csrName = keyStorePath + ".csr";
                ops.add(Pair.of(getExportCertificateOp(new File(certName), relativeTo, keyStoreName), "Export certificate"));
                // generate signing request from the key store
                ops.add(Pair.of(getSigningRequestOp(new File(csrName), relativeTo, keyStoreName), "Generate signing request"));
            }
            if (server.isDomain()) {
                config.setKeystoreGeneratedForDomain(true);
            } else {
                config.setKeystoreGeneratedForStandalone(true);
            }
        }

        String usedTrustManagerName = null;
        // mutual/two-way SSL
        if (config.getTrustStorePath() != null || config.getClientCertPath() != null) {
            final String trustStorePath = config.getTrustStorePath() == null ? defaultTrustStoreFileName : config.getTrustStorePath();

            if (csConfig.isPresent() && !server.aliasExists(credentialAliasTrustStorePassword, csConfig.get().getStoreName())) {
                printer.print("tasks.application_ssl.store_password",csConfig.get().getStoreName(), credentialAliasTrustStorePassword);
                server.encryptPassword(credentialAliasTrustStorePassword, csConfig.get().getStoreName(), config.getTrustStorePassword());
            }

            //  add trust-store
            final ModelNode addTrustStoreOp = getAddKeystoreOp(trustStoreName, trustStorePath, relativeTo, csConfig, credentialAliasTrustStorePassword, config.getTrustStorePassword());
            ops.add(Pair.of(addTrustStoreOp, "Add truststore"));

            // import the client certificate
            if (config.getClientCertPath() != null && supportsAdvancedModifiableKeyStoreOperations) {
                final boolean wasClientCertImported = (server.isDomain() && config.isClientCertImportedForDomain())
                        || (!server.isDomain() && config.isClientCertImportedForStandalone());
                if (! wasClientCertImported) {
                    final ModelNode importCertOp = getImportCertificateOp(config.getClientCertPath(), true, config.isValidateCertificate(), trustStoreName);
                    ops.add(Pair.of(importCertOp, "Import client certificate"));
                    // store the trust store
                    ops.add(Pair.of(getStoreKeystoreOp(trustStoreName), "Store truststore"));
                    // import client certificate only once
                    config.setClientCertImportedForStandalone(true);
                }
                if (server.isDomain()) {
                    config.setClientCertImportedForDomain(true);
                } else {
                    config.setClientCertImportedForStandalone(true);
                }
            }

            // trust-manager
            ops.add(Pair.of(getAddTrustManagerOp(trustManagerName, trustStoreName), "Add trust manager"));
            usedTrustManagerName = trustManagerName;
        }

        final ModelNode addSslContextOp = getAddSslContextOp(config.getProtocols(), config.getCipherSuites(), config.getTls13cipherNames(),
                usedTrustManagerName, sslContextName, keyManagerName);
        ops.add(Pair.of(addSslContextOp, "Add ssl context"));

        return ops;
    }

    private ModelNode getAddKeystoreOp(String keystoreName, String path, String relativeTo, Optional<CredentialStoreConfig> storeConfig, String alias, String password) {
        // /subsystem=elytron/key-store=test:add(credential-reference={alias="", store=""}, path=, relative-to=)
        // /subsystem=elytron/key-store=test:add(credential-reference={clear-text=""}, path=, relative-to=)
        final ModelNode addKsOp = createEmptyOperation("add",
                pathAddress("subsystem", "elytron").add("key-store", keystoreName));
        addKsOp.get("path").set(path);
        if (relativeTo != null) {
            addKsOp.get("relative-to").set(relativeTo);
        }
        CredentialStoreUtil.addCredReference(addKsOp, storeConfig, alias, password, "credential-reference");
        addKsOp.get(ModelDescriptionConstants.OPERATION_HEADERS).get("allow-resource-service-restart").set(true);
        return addKsOp;
    }

    private ModelNode getAddTrustManagerOp(String trustManagerName, String keyStoreName) {
        // /subsystem=elytron/trust-manager=test:add(key-store=)
        final ModelNode addTMOp = createEmptyOperation("add",
                pathAddress("subsystem", "elytron").add("trust-manager", trustManagerName));
        addTMOp.get("key-store").set(keyStoreName);
        addTMOp.get(ModelDescriptionConstants.OPERATION_HEADERS).get("allow-resource-service-restart").set(true);
        return addTMOp;
    }

    private ModelNode getAddKeymanagerOp(String keyManagerName, String keyStoreName, Optional<CredentialStoreConfig> storeConfig, String alias, String password) {
        // /subsystem=elytron/key-manager=test:add(credential-reference={alias="", store=""}, path=)
        // /subsystem=elytron/key-manager=test:add(credential-reference={clear-text="foobar"}, path=)
        final ModelNode addKmOp = createEmptyOperation("add",
                pathAddress("subsystem", "elytron").add("key-manager", keyManagerName));
        CredentialStoreUtil.addCredReference(addKmOp, storeConfig, alias, password, "credential-reference");
        addKmOp.get("key-store").set(keyStoreName);
        addKmOp.get(ModelDescriptionConstants.OPERATION_HEADERS).get("allow-resource-service-restart").set(true);
        return addKmOp;
    }

    private ModelNode getGenerateKeyPairOp(String dn, String validity, String keyStoreName) {
        // /subsystem=elytron/key-store=test:generate-key-pair(distinguished-name=, algorithm=, key-size=, alias=, validity=)
        final ModelNode generateKPOp = createEmptyOperation("generate-key-pair",
                pathAddress("subsystem", "elytron").add("key-store", keyStoreName));
        generateKPOp.get("distinguished-name").set(dn);
        // algorithm and key-size are hard-coded
        generateKPOp.get("algorithm").set(KEY_ALG);
        generateKPOp.get("key-size").set(KEY_SIZE);
        generateKPOp.get("alias").set(SERVER_KEY_ALIAS);
        if (validity != null && validity.length() > 0) {
            generateKPOp.get("validity").set(validity);
        }
        generateKPOp.get(ModelDescriptionConstants.OPERATION_HEADERS).get("allow-resource-service-restart").set(true);
        return generateKPOp;
    }

    private ModelNode getImportCertificateOp(String clientCertPath, boolean trust, boolean validateCertificate, String trustStoreName) {
        // subsystem=elytron/key-store=test:import-certificate(alias=, path=, trust-cacerts=, validate=)
        final ModelNode importCertOp = createEmptyOperation("import-certificate",
                pathAddress("subsystem", "elytron").add("key-store", trustStoreName));
        importCertOp.get("alias").set(CLIENT_KEY_ALIAS);
        importCertOp.get("path").set(clientCertPath);
        importCertOp.get("trust-cacerts").set(trust);
        importCertOp.get("validate").set(validateCertificate);
        importCertOp.get(ModelDescriptionConstants.OPERATION_HEADERS).get("allow-resource-service-restart").set(true);
        return importCertOp;
    }

    private ModelNode getExportCertificateOp(File path, String relativeTo, String keyStoreName) {
        // /subsystem=elytron/key-store=test:export-certificate(path=, alias=, relative-to=, pem=)
        final ModelNode exportCertOp = createEmptyOperation("export-certificate",
                pathAddress("subsystem", "elytron").add("key-store", keyStoreName));
        exportCertOp.get("path").set(path.getPath());
        exportCertOp.get("alias").set(SERVER_KEY_ALIAS);
        if (relativeTo != null) {
            exportCertOp.get("relative-to").set(relativeTo);
        }
        exportCertOp.get("pem").set(Boolean.TRUE);
        exportCertOp.get(ModelDescriptionConstants.OPERATION_HEADERS).get("allow-resource-service-restart").set(true);
        return exportCertOp;
    }

    private ModelNode getSigningRequestOp(File path, String relativeTo, String keyStoreName) {
        final ModelNode exportCertOp = createEmptyOperation("generate-certificate-signing-request",
                pathAddress("subsystem", "elytron").add("key-store", keyStoreName));
        exportCertOp.get("path").set(path.getPath());
        exportCertOp.get("alias").set(SERVER_KEY_ALIAS);
        if (relativeTo != null) {
            exportCertOp.get("relative-to").set(relativeTo);
        }
        exportCertOp.get(ModelDescriptionConstants.OPERATION_HEADERS).get("allow-resource-service-restart").set(true);
        return exportCertOp;
    }

    private ModelNode getStoreKeystoreOp(String storeName) {
        // /subsystem=elytron/key-store=test:store
        final ModelNode storeKSOp = createEmptyOperation("store",
                pathAddress("subsystem", "elytron").add("key-store", storeName));
        storeKSOp.get(ModelDescriptionConstants.OPERATION_HEADERS).get("allow-resource-service-restart").set(true);
        return storeKSOp;
    }

    private ModelNode getAddSslContextOp(Set<String> protocols, String cipherSuites, String tls13CipherNames,
                                         String trustManagerName, String sslContextName, String keyManagerName) {
        // /subsystem=elytron/server-ssl-context=foobar:add(key-manager=foobar, trust-manager=, protocols=, cipher-suite-names=)
        final ModelNode addSslContextOp = createEmptyOperation("add",
                pathAddress("subsystem", "elytron").add("server-ssl-context", sslContextName));
        addSslContextOp.get("key-manager").set(keyManagerName);
        if (trustManagerName != null) {
            // mutual SSL
            addSslContextOp.get("trust-manager").set(trustManagerName);
            addSslContextOp.get("need-client-auth").set(Boolean.TRUE);
        }
        if (protocols != null && !protocols.isEmpty()) {
            for (String protocol : protocols) {
                addSslContextOp.get("protocols").add(protocol);
            }
        }
        if (cipherSuites != null && cipherSuites.length() > 0) {
            addSslContextOp.get("cipher-suite-filter").set(cipherSuites);
        }
        if (tls13CipherNames != null && tls13CipherNames.length() > 0) {
            addSslContextOp.get("cipher-suite-names").set(tls13CipherNames);
        }
        addSslContextOp.get(ModelDescriptionConstants.OPERATION_HEADERS).get("allow-resource-service-restart").set(true);
        return addSslContextOp;
    }

}
