/**
 * 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.secdom;

import org.jboss.as.controller.OperationFailedException;
import org.jboss.as.controller.PathAddress;
import org.jboss.dmr.ModelNode;
import org.jboss.installer.postinstall.TaskPrinter;
import org.jboss.installer.postinstall.server.EmbeddedServer;
import org.jboss.installer.postinstall.task.CredentialStoreInstallTask;
import org.jboss.installer.postinstall.task.utils.CredentialStoreUtil;

import java.nio.file.Path;
import java.nio.file.Paths;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import java.util.Optional;

import static org.jboss.as.controller.operations.common.Util.createEmptyOperation;

public class CertSecurity {

    private final TaskPrinter printer;

    public CertSecurity(TaskPrinter printer) {
        this.printer = printer;
    }

    public List<ModelNode> toOperations(EmbeddedServer server, String domainName, CertificateConfig config, Optional<CredentialStoreInstallTask.Config> credStoreConfig) throws OperationFailedException {
        final String trustStoreName = domainName + "-trust-store";
        final String trustManagerName = domainName + "-trust-manager";
        final String realmName = domainName + "-realm";
        final String authFactoryName = domainName + "-certHttpAuth";
        final String sslContextName = domainName + "-ssl-context";
        final String passwordAlias = domainName + "-password";

        final ArrayList<ModelNode> ops = new ArrayList<>();

        // set up trust store and manager
        final ModelNode addTrustStore = createEmptyOperation("add", PathAddress.pathAddress("subsystem", "elytron")
                .append("key-store", trustStoreName));

        try {
            if (credStoreConfig.isPresent() && !server.aliasExists(passwordAlias, credStoreConfig.get().getStoreName())) {
                printer.print("tasks.sec_dom.store_password", passwordAlias, credStoreConfig.get().getStoreName());
                server.encryptPassword(passwordAlias, credStoreConfig.get().getStoreName(), config.getTrustStorePassword());
            }
        } catch (OperationFailedException e) {
            printer.print("tasks.sec_dom.cred_store_access", credStoreConfig.get().getStoreName());
            throw e;
        }

        CredentialStoreUtil.addCredReference(addTrustStore, credStoreConfig, passwordAlias, config.getTrustStorePassword(), "credential-reference");
        server.relativise(config.getTrustStorePath(), addTrustStore);
        ops.add(addTrustStore);

        final ModelNode addTrustManager = createEmptyOperation("add", PathAddress.pathAddress("subsystem", "elytron")
                .append("trust-manager", trustManagerName));
        addTrustManager.get("key-store").set(trustStoreName);
        ops.add(addTrustManager);

        final ModelNode addKsRealm = createEmptyOperation("add", PathAddress.pathAddress("subsystem", "elytron")
                .append("key-store-realm", realmName));
        addKsRealm.get("key-store").set(trustStoreName);
        ops.add(addKsRealm);

        // map the Cert properties into a principal
        final ModelNode addAttrPrincipalDecoder = createEmptyOperation("add", PathAddress.pathAddress("subsystem", "elytron")
                .append("x500-attribute-principal-decoder", "CNDecoder"));
        if (config.isUseOid()) {
            addAttrPrincipalDecoder.get("oid").set(config.getFilterExpression());
        } else {
            addAttrPrincipalDecoder.get("attribute-name").set(config.getFilterExpression());
        }
        addAttrPrincipalDecoder.get("start-segment").set(config.getStartSegments());
        addAttrPrincipalDecoder.get("maximum-segments").set(config.getMaximumSegments());
        ops.add(addAttrPrincipalDecoder);

        // create a realm to assign fixed Roles
        final ModelNode addRoleMapper = createEmptyOperation("add", PathAddress.pathAddress("subsystem", "elytron")
                .append("constant-role-mapper", "constantClientCertRole"));
        for (String role: config.getRoles()) {
            addRoleMapper.get("roles").add(role);
        }
        ops.add(addRoleMapper);

        final ModelNode addSecurityDomain = createEmptyOperation("add", PathAddress.pathAddress("subsystem", "elytron")
                .append("security-domain", domainName));
        ModelNode realm = new ModelNode();
        realm.get("realm").set(realmName);
        addSecurityDomain.get("realms").add(realm);
        addSecurityDomain.get("default-realm").set(realmName);
        addSecurityDomain.get("permission-mapper").set("default-permission-mapper");
        addSecurityDomain.get("principal-decoder").set("CNDecoder");
        addSecurityDomain.get("role-mapper").set("constantClientCertRole");
        ops.add(addSecurityDomain);

        // create new sec application sec domain supporting CLIENT_CERT
        final ModelNode addHttpAuthFactory = createEmptyOperation("add", PathAddress.pathAddress("subsystem", "elytron")
                .append("http-authentication-factory", authFactoryName));
        addHttpAuthFactory.get("http-server-mechanism-factory").set("global");
        addHttpAuthFactory.get("security-domain").set(domainName);
        final ModelNode cfg = new ModelNode();
        cfg.get("mechanism-name").set("CLIENT_CERT");
        addHttpAuthFactory.get("mechanism-configurations").add(cfg);
        ops.add(addHttpAuthFactory);

        final ModelNode addAppDomain = createEmptyOperation("add", PathAddress.pathAddress("subsystem", "undertow")
                .append("application-security-domain", config.getApplicationDomainName()));
        addAppDomain.get("http-authentication-factory").set(authFactoryName);
        ops.add(addAppDomain);

        // create ssl-context to associate sec domain with key and trust managers and require client auth
        final ModelNode addSslContext = createEmptyOperation("add", PathAddress.pathAddress("subsystem", "elytron")
                .append("server-ssl-context", sslContextName));
        addSslContext.get("security-domain").set(domainName);
        addSslContext.get("key-manager").set("applicationKM");
        addSslContext.get("need-client-auth").set("true");
        addSslContext.get("trust-manager").set(trustManagerName);
        addSslContext.get("authentication-optional").set("true");
        ops.add(addSslContext);

        // set the new ssl context on https-listener
        final ModelNode writeSslContext = createEmptyOperation("write-attribute", PathAddress.pathAddress("subsystem", "undertow")
                .append("server", "default-server")
                .append("https-listener", "https"));
        writeSslContext.get("name").set("ssl-context");
        writeSslContext.get("value").set(sslContextName);
        ops.add(writeSslContext);

        return ops;
    }


    public static class CertificateConfig {
        // serialization keys
        private static final String FILTER_EXPRESSION = "filterExpression";
        private static final String APPLICATION_DOMAIN_NAME = "applicationDomainName";
        private static final String TRUST_STORE_PATH = "trustStorePath";
        private static final String MAX_SEGMENTS = "maxSegments";
        private static final String START_SEGMENTS = "startSegments";
        private static final String USE_OID = "useOid";
        private static final String ROLE = "role-";
        private static final String ROLE_COUNT = "role-count";
        public static final String TRUST_STORE_PASSWORD_KEY = "trustStorePassword";
        // end of serialization keys

        private String applicationDomainName;
        private String[] roles;
        private Integer maximumSegments;
        private Integer startSegments;
        private String filterExpression;
        private Path trustStorePath;
        private String trustStorePassword;
        private boolean useOid;

        public CertificateConfig() {

        }

        public CertificateConfig(Map<String, String> attributes) {
            this.filterExpression = attributes.get(FILTER_EXPRESSION);
            this.applicationDomainName = attributes.get(APPLICATION_DOMAIN_NAME);
            this.trustStorePath = Paths.get(attributes.get(TRUST_STORE_PATH));
            this.maximumSegments = Integer.parseInt(attributes.get(MAX_SEGMENTS));
            this.startSegments = Integer.parseInt(attributes.get(START_SEGMENTS));
            this.useOid = Boolean.parseBoolean(attributes.get(USE_OID));
            this.trustStorePassword = attributes.get(TRUST_STORE_PASSWORD_KEY);

            final int roleCount = Integer.parseInt(attributes.get(ROLE_COUNT));
            final ArrayList<String> roles = new ArrayList<>(roleCount);
            for (int i = 0; i < roleCount; i++) {
                roles.add(attributes.get(ROLE + i));
            }
            this.roles = roles.toArray(new String[]{});
        }

        public void setApplicationDomainName(String applicationDomainName) {
            this.applicationDomainName = applicationDomainName;
        }

        public String getApplicationDomainName() {
            return applicationDomainName;
        }

        public String[] getRoles() {
            return roles;
        }

        public void setRoles(String[] roles) {
            this.roles = roles;
        }

        public Integer getMaximumSegments() {
            return maximumSegments;
        }

        public void setMaximumSegments(Integer maximumSegments) {
            this.maximumSegments = maximumSegments;
        }

        public Integer getStartSegments() {
            return startSegments;
        }

        public void setStartSegments(Integer startSegments) {
            this.startSegments = startSegments;
        }

        public String getFilterExpression() {
            return filterExpression;
        }

        public void setFilterExpression(String filterExpression) {
            this.filterExpression = filterExpression;
        }

        public Path getTrustStorePath() {
            return trustStorePath;
        }

        public void setTrustStorePath(Path trustStorePath) {
            this.trustStorePath = trustStorePath;
        }

        public String getTrustStorePassword() {
            return trustStorePassword;
        }

        public void setTrustStorePassword(String trustStorePassword) {
            this.trustStorePassword = trustStorePassword;
        }

        public boolean isUseOid() {
            return useOid;
        }

        public void setUseOid(boolean useOid) {
            this.useOid = useOid;
        }

        public Map<String, String> toAttributes() {
            final HashMap<String, String> attrs = new HashMap<>();
            attrs.put(FILTER_EXPRESSION, filterExpression);
            attrs.put(APPLICATION_DOMAIN_NAME, applicationDomainName);
            attrs.put(TRUST_STORE_PATH, trustStorePath.toAbsolutePath().toString());
            attrs.put(MAX_SEGMENTS, "" + maximumSegments);
            attrs.put(START_SEGMENTS, "" + startSegments);
            attrs.put(USE_OID, "" + useOid);

            for (int i = 0; i < roles.length; i++) {
                attrs.put(ROLE + i, roles[i]);
            }
            attrs.put(ROLE_COUNT, "" + roles.length);
            return attrs;
        }

        @Override
        public boolean equals(Object o) {
            if (this == o) return true;
            if (o == null || getClass() != o.getClass()) return false;
            CertificateConfig that = (CertificateConfig) o;
            return useOid == that.useOid && Objects.equals(applicationDomainName, that.applicationDomainName) && Arrays.equals(roles, that.roles) && Objects.equals(maximumSegments, that.maximumSegments) && Objects.equals(startSegments, that.startSegments) && Objects.equals(filterExpression, that.filterExpression) && Objects.equals(trustStorePath, that.trustStorePath) && Objects.equals(trustStorePassword, that.trustStorePassword);
        }

        @Override
        public int hashCode() {
            int result = Objects.hash(applicationDomainName, maximumSegments, startSegments, filterExpression, trustStorePath, trustStorePassword, useOid);
            result = 31 * result + Arrays.hashCode(roles);
            return result;
        }

        @Override
        public String toString() {
            return "CertificateConfig{" +
                    "applicationDomainName='" + applicationDomainName + '\'' +
                    ", roles=" + Arrays.toString(roles) +
                    ", maximumSegments=" + maximumSegments +
                    ", startSegments=" + startSegments +
                    ", filterExpression='" + filterExpression + '\'' +
                    ", trustStorePath=" + trustStorePath +
                    ", trustStorePassword='" + trustStorePassword + '\'' +
                    ", useOid=" + useOid +
                    '}';
        }
    }
}
