/*
 * Copyright 2018 LINE Corporation
 *
 * LINE Corporation 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:
 *
 *   https://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 com.linecorp.centraldogma.server.auth.saml;

import static com.google.common.base.MoreObjects.firstNonNull;
import static java.util.Objects.requireNonNull;

import java.util.Map;

import javax.annotation.Nullable;

import org.opensaml.xmlsec.signature.support.SignatureConstants;

import com.fasterxml.jackson.annotation.JsonCreator;
import com.fasterxml.jackson.annotation.JsonProperty;
import com.fasterxml.jackson.core.JsonProcessingException;
import com.google.common.collect.ImmutableMap;
import com.google.common.collect.ImmutableMap.Builder;

import com.linecorp.armeria.server.saml.SamlBindingProtocol;
import com.linecorp.armeria.server.saml.SamlEndpoint;
import com.linecorp.armeria.server.saml.SamlNameIdFormat;
import com.linecorp.centraldogma.internal.Jackson;

/**
 * Properties which are used to configure SAML authentication for Central Dogma server.
 * A user can specify them as the authentication property in the {@code dogma.json} as follows:
 * <pre>{@code
 * "authentication": {
 *     "factoryClassName": "com.linecorp.centraldogma.server.auth.saml.SamlAuthProviderFactory",
 *     "properties": {
 *         "entityId": "...the service provider ID...",
 *         "hostname": "dogma-example.linecorp.com",
 *         "signingKey": "...the name of signing key (optional)...",
 *         "encryptionKey": "...the name of encryption key (optional)...",
 *         "keyStore": {
 *             "type": "...the type of the keystore (optional)...",
 *             "path": "...the path where keystore file exists...",
 *             "password": "...the password of the keystore (optional)...",
 *             "keyPasswords": {
 *                 "signing": "...the password of the signing key...",
 *                 "encryption": "...the password of the encryption key..."
 *             },
 *             "signatureAlgorithm": "...the signature algorithm for signing and encryption (optional)..."
 *         },
 *         "idp": {
 *             "entityId": "...the identity provider ID...",
 *             "uri": "https://idp-example.linecorp.com/saml/sso",
 *             "binding": "HTTP_POST or HTTP_REDIRECT (optional)",
 *             "signingKey": "...the name of signing certificate (optional)...",
 *             "encryptionKey": "...the name of encryption certificate (optional)...",
 *             "subjectLoginNameIdFormat":
 *                  "...the name ID format of a subject which holds a login name (optional)...",
 *             "attributeLoginName": "...the attribute name which holds a login name (optional)..."
 *         }
 *     }
 * }
 * }</pre>
 */
final class SamlAuthProperties {
    /**
     * A default key name for signing.
     */
    private static final String DEFAULT_SIGNING_KEY = "signing";

    /**
     * A default key name for encryption.
     */
    private static final String DEFAULT_ENCRYPTION_KEY = "encryption";

    /**
     * An ID of this service provider.
     */
    private final String entityId;

    /**
     * A hostname of this service provider.
     */
    private final String hostname;

    /**
     * A key name which is used for signing. The default name is {@value DEFAULT_SIGNING_KEY}.
     */
    private final String signingKey;

    /**
     * A key name which is used for encryption. The default name is {@value DEFAULT_ENCRYPTION_KEY}.
     */
    private final String encryptionKey;

    /**
     * A configuration for the keystore.
     */
    private final KeyStore keyStore;

    /**
     * An identity provider configuration. A single identity provider is supported.
     */
    private final Idp idp;

    @JsonCreator
    SamlAuthProperties(
            @JsonProperty("entityId") String entityId,
            @JsonProperty("hostname") String hostname,
            @JsonProperty("signingKey") @Nullable String signingKey,
            @JsonProperty("encryptionKey") @Nullable String encryptionKey,
            @JsonProperty("keyStore") KeyStore keyStore,
            @JsonProperty("idp") Idp idp) {
        this.entityId = requireNonNull(entityId, "entityId");
        this.hostname = requireNonNull(hostname, "hostname");
        this.signingKey = firstNonNull(signingKey, DEFAULT_SIGNING_KEY);
        this.encryptionKey = firstNonNull(encryptionKey, DEFAULT_ENCRYPTION_KEY);
        this.keyStore = requireNonNull(keyStore, "keyStore");
        this.idp = requireNonNull(idp, "idp");
    }

    @JsonProperty
    public String entityId() {
        return entityId;
    }

    @JsonProperty
    public String hostname() {
        return hostname;
    }

    @JsonProperty
    public String signingKey() {
        return signingKey;
    }

    @JsonProperty
    public String encryptionKey() {
        return encryptionKey;
    }

    @JsonProperty
    public KeyStore keyStore() {
        return keyStore;
    }

    @JsonProperty
    public Idp idp() {
        return idp;
    }

    @Override
    public String toString() {
        try {
            return Jackson.writeValueAsPrettyString(this);
        } catch (JsonProcessingException e) {
            throw new IllegalStateException(e);
        }
    }

    static class KeyStore {
        /**
         * A default signature algorithm.
         */
        private static final String DEFAULT_SIGNATURE_ALGORITHM = SignatureConstants.ALGO_ID_SIGNATURE_RSA;

        /**
         * A type of the keystore. The default value is retrieved from
         * {@code java.security.KeyStore.getDefaultType()}.
         */
        private final String type;

        /**
         * A path of the keystore.
         */
        private final String path;

        /**
         * A password of the keystore. The empty string is used by default.
         */
        @Nullable
        private final String password;

        /**
         * A map of the key name and its password.
         */
        private final Map<String, String> keyPasswords;

        /**
         * A signature algorithm for signing and encryption. The default algorithm is
         * {@value DEFAULT_SIGNATURE_ALGORITHM}.
         *
         * @see SignatureConstants for more information about the signature algorithm
         */
        private final String signatureAlgorithm;

        @JsonCreator
        KeyStore(@JsonProperty("type") @Nullable String type,
                 @JsonProperty("path") String path,
                 @JsonProperty("password") @Nullable String password,
                 @JsonProperty("keyPasswords") @Nullable Map<String, String> keyPasswords,
                 @JsonProperty("signatureAlgorithm") @Nullable String signatureAlgorithm) {
            this.type = firstNonNull(type, java.security.KeyStore.getDefaultType());
            this.path = requireNonNull(path, "path");
            this.password = password;
            this.keyPasswords = sanitizePasswords(keyPasswords);
            this.signatureAlgorithm = firstNonNull(signatureAlgorithm, DEFAULT_SIGNATURE_ALGORITHM);
        }

        private static Map<String, String> sanitizePasswords(@Nullable Map<String, String> keyPasswords) {
            if (keyPasswords == null) {
                return ImmutableMap.of();
            }
            final ImmutableMap.Builder<String, String> builder = new Builder<>();
            keyPasswords.forEach((key, password) -> builder.put(key, firstNonNull(password, "")));
            return builder.build();
        }

        @JsonProperty
        public String type() {
            return type;
        }

        @JsonProperty
        public String path() {
            return path;
        }

        @Nullable
        @JsonProperty
        public String password() {
            return password;
        }

        @JsonProperty
        public Map<String, String> keyPasswords() {
            return keyPasswords;
        }

        @JsonProperty
        public String signatureAlgorithm() {
            return signatureAlgorithm;
        }
    }

    static class Idp {
        /**
         * An ID of the identity provider.
         */
        private final String entityId;

        /**
         * A location of the single sign-on service.
         */
        private final String uri;

        /**
         * A name of a {@link SamlBindingProtocol}. The default name is the
         * {@link SamlBindingProtocol#HTTP_POST}.
         */
        private final SamlBindingProtocol binding;

        /**
         * A certificate name which is used for signing. The default name is the {@link #entityId()}.
         */
        private final String signingKey;

        /**
         * A certificate name which is used for encryption. The default name is the {@link #entityId()}.
         */
        private final String encryptionKey;

        /**
         * A name ID format of a subject which holds a login name of an authenticated user. If both
         * {@code subjectLoginNameIdFormat} and {@code attributeLoginName} are {@code null},
         * {@code urn:oasis:names:tc:SAML:1.1:nameid-format:emailAddress} is set by default.
         */
        @Nullable
        private final String subjectLoginNameIdFormat;

        /**
         * An attribute name which holds a login name of an authenticated user.
         */
        @Nullable
        private final String attributeLoginName;

        @JsonCreator
        Idp(@JsonProperty("entityId") String entityId,
            @JsonProperty("uri") String uri,
            @JsonProperty("binding") @Nullable String binding,
            @JsonProperty("signingKey") @Nullable String signingKey,
            @JsonProperty("encryptionKey") @Nullable String encryptionKey,
            @JsonProperty("subjectLoginNameIdFormat") @Nullable String subjectLoginNameIdFormat,
            @JsonProperty("attributeLoginName") @Nullable String attributeLoginName) {
            this.entityId = requireNonNull(entityId, "entityId");
            this.uri = requireNonNull(uri, "uri");
            this.binding = binding != null ? SamlBindingProtocol.valueOf(binding)
                                           : SamlBindingProtocol.HTTP_POST;
            this.signingKey = firstNonNull(signingKey, entityId);
            this.encryptionKey = firstNonNull(encryptionKey, entityId);

            if (subjectLoginNameIdFormat == null && attributeLoginName == null) {
                this.subjectLoginNameIdFormat = SamlNameIdFormat.EMAIL.urn();
                this.attributeLoginName = null;
            } else {
                this.subjectLoginNameIdFormat = subjectLoginNameIdFormat;
                this.attributeLoginName = attributeLoginName;
            }
        }

        @JsonProperty
        public String entityId() {
            return entityId;
        }

        @JsonProperty
        public String uri() {
            return uri;
        }

        @JsonProperty
        public String binding() {
            return binding.name();
        }

        @JsonProperty
        public String signingKey() {
            return signingKey;
        }

        @JsonProperty
        public String encryptionKey() {
            return encryptionKey;
        }

        @Nullable
        @JsonProperty
        public String subjectLoginNameIdFormat() {
            return subjectLoginNameIdFormat;
        }

        @Nullable
        @JsonProperty
        public String attributeLoginName() {
            return attributeLoginName;
        }

        public SamlEndpoint endpoint() {
            switch (binding) {
                case HTTP_POST:
                    return SamlEndpoint.ofHttpPost(uri);
                case HTTP_REDIRECT:
                    return SamlEndpoint.ofHttpRedirect(uri);
                default:
                    throw new IllegalStateException("Failed to get an endpoint of the IdP: " + entityId);
            }
        }
    }
}