/*
 * Copyright 2016 Red Hat, Inc. and/or its affiliates
 * and other 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.keycloak.adapters.saml.config.parsers;

import org.jboss.logging.Logger;
import org.keycloak.adapters.saml.DefaultSamlDeployment;
import org.keycloak.adapters.saml.SamlDeployment;
import org.keycloak.adapters.saml.config.Key;
import org.keycloak.adapters.saml.config.KeycloakSamlAdapter;
import org.keycloak.adapters.saml.config.SP;
import org.keycloak.common.enums.SslRequired;
import org.keycloak.common.util.PemUtils;
import org.keycloak.saml.SignatureAlgorithm;
import org.keycloak.saml.common.exceptions.ParsingException;

import java.io.File;
import java.io.FileInputStream;
import java.io.FileNotFoundException;
import java.io.InputStream;
import java.security.KeyPair;
import java.security.KeyStore;
import java.security.KeyStoreException;
import java.security.PrivateKey;
import java.security.PublicKey;
import java.security.cert.Certificate;
import java.util.HashSet;
import java.util.Set;
import org.keycloak.adapters.cloned.HttpClientBuilder;
import java.net.URI;

/**
 * @author <a href="mailto:bill@burkecentral.com">Bill Burke</a>
 * @version $Revision: 1 $
 */
public class DeploymentBuilder {

    protected static Logger log = Logger.getLogger(DeploymentBuilder.class);

    public SamlDeployment build(InputStream xml, ResourceLoader resourceLoader) throws ParsingException {
        DefaultSamlDeployment deployment = new DefaultSamlDeployment();
        DefaultSamlDeployment.DefaultIDP idp = new DefaultSamlDeployment.DefaultIDP();
        DefaultSamlDeployment.DefaultSingleSignOnService sso = new DefaultSamlDeployment.DefaultSingleSignOnService();
        DefaultSamlDeployment.DefaultSingleLogoutService slo = new DefaultSamlDeployment.DefaultSingleLogoutService();
        idp.setSingleSignOnService(sso);
        idp.setSingleLogoutService(slo);

        KeycloakSamlAdapter adapter = (KeycloakSamlAdapter) KeycloakSamlAdapterParser.getInstance().parse(xml);
        SP sp = adapter.getSps().get(0);
        deployment.setConfigured(true);
        deployment.setEntityID(sp.getEntityID());
        deployment.setForceAuthentication(sp.isForceAuthentication());
        deployment.setIsPassive(sp.isIsPassive());
        deployment.setNameIDPolicyFormat(sp.getNameIDPolicyFormat());
        deployment.setLogoutPage(sp.getLogoutPage());
        deployment.setSignatureCanonicalizationMethod(sp.getIdp().getSignatureCanonicalizationMethod());
        deployment.setAutodetectBearerOnly(sp.isAutodetectBearerOnly());
        deployment.setSignatureAlgorithm(SignatureAlgorithm.RSA_SHA256);
        if (sp.getIdp().getSignatureAlgorithm() != null) {
            deployment.setSignatureAlgorithm(SignatureAlgorithm.valueOf(sp.getIdp().getSignatureAlgorithm()));
        }
        if (sp.getPrincipalNameMapping() != null) {
            SamlDeployment.PrincipalNamePolicy policy = SamlDeployment.PrincipalNamePolicy.valueOf(sp.getPrincipalNameMapping().getPolicy());
            deployment.setPrincipalNamePolicy(policy);
            deployment.setPrincipalAttributeName(sp.getPrincipalNameMapping().getAttributeName());
        }
        deployment.setRoleAttributeNames(sp.getRoleAttributes());
        if (sp.getRoleAttributes() == null) {
            Set<String> roles = new HashSet<>();
            roles.add("Role");
            deployment.setRoleAttributeNames(roles);
        }
        if (sp.getSslPolicy() != null) {
            SslRequired ssl = SslRequired.valueOf(sp.getSslPolicy());
            deployment.setSslRequired(ssl);
        }
        if (sp.getKeys() != null) {
            for (Key key : sp.getKeys()) {
                if (key.isSigning()) {
                    PrivateKey privateKey = null;
                    PublicKey publicKey = null;
                    if (key.getKeystore() != null) {
                        KeyStore keyStore = loadKeystore(resourceLoader, key);
                        Certificate cert = null;
                        try {
                            log.debugf("Try to load key [%s]", key.getKeystore().getCertificateAlias());
                            cert = keyStore.getCertificate(key.getKeystore().getCertificateAlias());
                            if(cert == null) {
                                log.errorf("Key alias %s is not found into keystore", key.getKeystore().getCertificateAlias());
                            }
                            privateKey = (PrivateKey) keyStore.getKey(key.getKeystore().getPrivateKeyAlias(), key.getKeystore().getPrivateKeyPassword().toCharArray());
                            publicKey = cert.getPublicKey();
                        } catch (Exception e) {
                            throw new RuntimeException(e);
                        }
                    } else {
                        if (key.getPrivateKeyPem() == null) {
                            throw new RuntimeException("SP signing key must have a PrivateKey defined");
                        }
                        try {
                            privateKey = PemUtils.decodePrivateKey(key.getPrivateKeyPem().trim());
                            if (key.getPublicKeyPem() == null && key.getCertificatePem() == null) {
                                throw new RuntimeException("Sp signing key must have a PublicKey or Certificate defined");
                            }
                            publicKey = getPublicKeyFromPem(key);
                        } catch (Exception e) {
                            throw new RuntimeException(e);
                        }
                    }
                    KeyPair keyPair = new KeyPair(publicKey, privateKey);
                    deployment.setSigningKeyPair(keyPair);

                }
                if (key.isEncryption()) {
                    if (key.getKeystore() != null) {

                        KeyStore keyStore = loadKeystore(resourceLoader, key);
                        try {
                            PrivateKey privateKey = (PrivateKey) keyStore.getKey(key.getKeystore().getPrivateKeyAlias(), key.getKeystore().getPrivateKeyPassword().toCharArray());
                            deployment.setDecryptionKey(privateKey);
                        } catch (Exception e) {
                            throw new RuntimeException(e);
                        }
                    } else {
                        if (key.getPrivateKeyPem() == null) {
                            throw new RuntimeException("SP signing key must have a PrivateKey defined");
                        }
                        try {
                            PrivateKey privateKey = PemUtils.decodePrivateKey(key.getPrivateKeyPem().trim());
                            deployment.setDecryptionKey(privateKey);
                        } catch (Exception e) {
                            throw new RuntimeException(e);
                        }

                    }
                }
            }
        }

        deployment.setIdp(idp);
        idp.setEntityID(sp.getIdp().getEntityID());
        sso.setRequestBinding(SamlDeployment.Binding.parseBinding(sp.getIdp().getSingleSignOnService().getRequestBinding()));
        sso.setRequestBindingUrl(sp.getIdp().getSingleSignOnService().getBindingUrl());
        if (sp.getIdp().getSingleSignOnService().getResponseBinding() != null) {
            sso.setResponseBinding(SamlDeployment.Binding.parseBinding(sp.getIdp().getSingleSignOnService().getResponseBinding()));
        }
        if (sp.getIdp().getSingleSignOnService().getAssertionConsumerServiceUrl() != null) {
            if (! sp.getIdp().getSingleSignOnService().getAssertionConsumerServiceUrl().endsWith("/saml")) {
                throw new RuntimeException("AssertionConsumerServiceUrl must end with \"/saml\".");
            }
            sso.setAssertionConsumerServiceUrl(URI.create(sp.getIdp().getSingleSignOnService().getAssertionConsumerServiceUrl()));
        }
        sso.setSignRequest(sp.getIdp().getSingleSignOnService().isSignRequest());
        sso.setValidateResponseSignature(sp.getIdp().getSingleSignOnService().isValidateResponseSignature());
        sso.setValidateAssertionSignature(sp.getIdp().getSingleSignOnService().isValidateAssertionSignature());

        slo.setSignRequest(sp.getIdp().getSingleLogoutService().isSignRequest());
        slo.setSignResponse(sp.getIdp().getSingleLogoutService().isSignResponse());
        slo.setValidateResponseSignature(sp.getIdp().getSingleLogoutService().isValidateResponseSignature());
        slo.setValidateRequestSignature(sp.getIdp().getSingleLogoutService().isValidateRequestSignature());
        slo.setRequestBinding(SamlDeployment.Binding.parseBinding(sp.getIdp().getSingleLogoutService().getRequestBinding()));
        slo.setResponseBinding(SamlDeployment.Binding.parseBinding(sp.getIdp().getSingleLogoutService().getResponseBinding()));
        if (slo.getRequestBinding() == SamlDeployment.Binding.POST) {
            slo.setRequestBindingUrl(sp.getIdp().getSingleLogoutService().getPostBindingUrl());
        } else {
            slo.setRequestBindingUrl(sp.getIdp().getSingleLogoutService().getRedirectBindingUrl());
        }
        if (slo.getResponseBinding() == SamlDeployment.Binding.POST) {
            slo.setResponseBindingUrl(sp.getIdp().getSingleLogoutService().getPostBindingUrl());
        } else {
            slo.setResponseBindingUrl(sp.getIdp().getSingleLogoutService().getRedirectBindingUrl());
        }
        if (sp.getIdp().getKeys() != null) {
            for (Key key : sp.getIdp().getKeys()) {
                if (key.isSigning()) {
                    processSigningKey(idp, key, resourceLoader);
                }
            }
        }

        idp.setClient(new HttpClientBuilder().build(sp.getIdp().getHttpClientConfig()));
        idp.refreshKeyLocatorConfiguration();

        return deployment;
    }

    private void processSigningKey(DefaultSamlDeployment.DefaultIDP idp, Key key, ResourceLoader resourceLoader) throws RuntimeException {
        PublicKey publicKey;
        if (key.getKeystore() != null) {
            KeyStore keyStore = loadKeystore(resourceLoader, key);
            Certificate cert = null;
            try {
                cert = keyStore.getCertificate(key.getKeystore().getCertificateAlias());
            } catch (KeyStoreException e) {
                throw new RuntimeException(e);
            }
            publicKey = cert.getPublicKey();
        } else {
            if (key.getPublicKeyPem() == null && key.getCertificatePem() == null) {
                throw new RuntimeException("IDP signing key must have a PublicKey or Certificate defined");
            }
            publicKey = getPublicKeyFromPem(key);
        }

        idp.addSignatureValidationKey(publicKey);
    }

    protected static PublicKey getPublicKeyFromPem(Key key) {
        PublicKey publicKey;
        if (key.getPublicKeyPem() != null) {
            publicKey = PemUtils.decodePublicKey(key.getPublicKeyPem().trim());
        } else {
            Certificate cert = PemUtils.decodeCertificate(key.getCertificatePem().trim());
            publicKey = cert.getPublicKey();
        }
        return publicKey;
    }

    protected static KeyStore loadKeystore(ResourceLoader resourceLoader, Key key) {
        String type = key.getKeystore().getType();
        if (type == null) type = "JKS";
        KeyStore keyStore = null;
        try {
            keyStore = KeyStore.getInstance(type);
        } catch (KeyStoreException e) {
            throw new RuntimeException(e);
        }
        InputStream is = null;
        if (key.getKeystore().getFile() != null) {
            File fp = new File(key.getKeystore().getFile());
            if (!fp.exists()) {
            }
            try {
                is = new FileInputStream(fp);
            } catch (FileNotFoundException e) {
                throw new RuntimeException("KeyStore " + key.getKeystore().getFile() + " does not exist");
            }

        } else {
            is = resourceLoader.getResourceAsStream(key.getKeystore().getResource());
            if (is == null) {
                throw new RuntimeException("KeyStore " + key.getKeystore().getResource() + " does not exist");
            }
        }
        try {
            keyStore.load(is, key.getKeystore().getPassword().toCharArray());
        } catch (Exception e) {
            throw new RuntimeException(e);
        }
        return keyStore;
    }
}
