/**
 * 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
 *
 * 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.apache.cxf.fediz.core;


import java.net.URI;
import java.security.cert.X509Certificate;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.List;
import java.util.Map;

import javax.crypto.KeyGenerator;
import javax.crypto.SecretKey;
import javax.security.auth.callback.CallbackHandler;
import javax.xml.parsers.DocumentBuilder;
import javax.xml.parsers.DocumentBuilderFactory;

import org.w3c.dom.Document;
import org.w3c.dom.Element;

import org.apache.wss4j.common.saml.SAMLCallback;
import org.apache.wss4j.common.saml.bean.ActionBean;
import org.apache.wss4j.common.saml.bean.AttributeBean;
import org.apache.wss4j.common.saml.bean.AttributeStatementBean;
import org.apache.wss4j.common.saml.bean.AuthDecisionStatementBean;
import org.apache.wss4j.common.saml.bean.AuthenticationStatementBean;
import org.apache.wss4j.common.saml.bean.ConditionsBean;
import org.apache.wss4j.common.saml.bean.KeyInfoBean;
import org.apache.wss4j.common.saml.bean.KeyInfoBean.CERT_IDENTIFIER;
import org.apache.wss4j.common.saml.bean.SubjectBean;
import org.apache.wss4j.common.saml.bean.SubjectConfirmationDataBean;
import org.apache.wss4j.common.saml.bean.SubjectLocalityBean;
import org.apache.wss4j.common.util.KeyUtils;
import org.apache.wss4j.dom.WSConstants;
import org.apache.wss4j.dom.message.WSSecEncryptedKey;


/**
 * A base implementation of a Callback Handler for a SAML assertion. By default it creates an
 * authentication assertion.
 */
public abstract class AbstractSAMLCallbackHandler implements CallbackHandler {

    public enum Statement {
        AUTHN, ATTR, AUTHZ
    };

    public enum MultiValue {
        MULTI_VALUE, MULTI_ATTR, ENC_VALUE
    };

    public static final URI CLAIM_TYPE_LANGUAGE =
        URI.create("http://schemas.mycompany.com/claims/language");

    protected String subjectName;
    protected String subjectQualifier;
    protected String confirmationMethod;
    protected X509Certificate[] certs;
    protected Statement statement = Statement.AUTHN;
    protected boolean alsoAddAuthnStatement;
    protected CERT_IDENTIFIER certIdentifier = CERT_IDENTIFIER.X509_CERT;
    protected byte[] ephemeralKey;
    protected String issuer;
    protected String subjectNameIDFormat;
    protected String subjectLocalityIpAddress;
    protected String subjectLocalityDnsAddress;
    protected String resource;
    protected List<?> customAttributeValues;
    protected ConditionsBean conditions;
    protected SubjectConfirmationDataBean subjectConfirmationData;
    protected List<String> roles = Arrays.asList("User", "Admin");
    protected Map<String, String> claims;
    protected MultiValue multiValueType = MultiValue.MULTI_VALUE;
    protected String roleSeperator = ",";
    protected String roleAttributeName = FedizConstants.DEFAULT_ROLE_URI.toString();
    protected String countryClaimName = ClaimTypes.COUNTRY.toString();
    protected String customClaimName = CLAIM_TYPE_LANGUAGE.toString();
    protected String attributeNameFormat = "urn:oasis:names:tc:SAML:2.0:attrname-format:unspecified";
    protected boolean useNameFormatAsNamespace;
    private boolean addGivenName;
    private boolean addRoleValue = true;

    public void setSubjectConfirmationData(SubjectConfirmationDataBean subjectConfirmationData) {
        this.subjectConfirmationData = subjectConfirmationData;
    }

    public void setConditions(ConditionsBean conditionsBean) {
        this.conditions = conditionsBean;
    }

    public void setConfirmationMethod(String confMethod) {
        confirmationMethod = confMethod;
    }

    public void setStatement(Statement statement) {
        this.statement = statement;
    }

    public void setCertIdentifier(CERT_IDENTIFIER certIdentifier) {
        this.certIdentifier = certIdentifier;
    }

    public void setCerts(X509Certificate[] certs) {
        this.certs = certs;
    }

    public byte[] getEphemeralKey() {
        return ephemeralKey;
    }

    public void setIssuer(String issuer) {
        this.issuer = issuer;
    }

    public void setSubjectNameIDFormat(String subjectNameIDFormat) {
        this.subjectNameIDFormat = subjectNameIDFormat;
    }

    public void setSubjectName(String subjectName) {
        this.subjectName = subjectName;
    }

    public void setSubjectLocality(String ipAddress, String dnsAddress) {
        this.subjectLocalityIpAddress = ipAddress;
        this.subjectLocalityDnsAddress = dnsAddress;
    }

    public void setResource(String resource) {
        this.resource = resource;
    }

    public void setCustomAttributeValues(List<?> customAttributeValues) {
        this.customAttributeValues = customAttributeValues;
    }

    public void setRoles(List<String> roles) {
        this.roles = roles;
    }

    public void setClaims(Map<String, String> claims) {
        this.claims = claims;
    }

    public void setMultiValueType(MultiValue multiValueType) {
        this.multiValueType = multiValueType;
    }

    public void setRoleAttributeName(String roleAttributeName) {
        this.roleAttributeName = roleAttributeName;
    }

    public String getRoleAttributeName() {
        return this.roleAttributeName;
    }

    public void setCountryClaimName(String countryClaimName) {
        this.countryClaimName = countryClaimName;
    }

    public String getCountryClaimName() {
        return this.countryClaimName;
    }

    public void setCustomClaimName(String customClaimName) {
        this.customClaimName = customClaimName;
    }

    public String getCustomClaimName() {
        return this.customClaimName;
    }

    public void setAttributeNameFormat(String attributeNameFormat) {
        this.attributeNameFormat = attributeNameFormat;
    }

    public String getAttributeNameFormat() {
        return this.attributeNameFormat;
    }

    public boolean isUseNameFormatAsNamespace() {
        return useNameFormatAsNamespace;
    }

    public void setUseNameFormatAsNamespace(boolean useNameFormatAsNamespace) {
        this.useNameFormatAsNamespace = useNameFormatAsNamespace;
    }

    /**
     * Note that the SubjectBean parameter should be null for SAML2.0
     */
    //CHECKSTYLE:OFF
    protected void createAndSetStatement(SubjectBean subjectBean, SAMLCallback callback) {
        if (alsoAddAuthnStatement || statement == Statement.AUTHN) {
            AuthenticationStatementBean authBean = new AuthenticationStatementBean();
            if (subjectBean != null) {
                authBean.setSubject(subjectBean);
            }
            if (subjectLocalityIpAddress != null || subjectLocalityDnsAddress != null) {
                SubjectLocalityBean subjectLocality = new SubjectLocalityBean();
                subjectLocality.setIpAddress(subjectLocalityIpAddress);
                subjectLocality.setDnsAddress(subjectLocalityDnsAddress);
                authBean.setSubjectLocality(subjectLocality);
            }
            authBean.setAuthenticationMethod("Password");
            callback.setAuthenticationStatementData(Collections.singletonList(authBean));
        }

        if (statement == Statement.ATTR) {
            AttributeStatementBean attrStateBean = new AttributeStatementBean();
            if (subjectBean != null) {
                attrStateBean.setSubject(subjectBean);
            }

            if (this.roles == null) {
                AttributeBean attributeBean = new AttributeBean();
                if (subjectBean != null) {
                    attributeBean.setSimpleName("name");
                    attributeBean.setQualifiedName("dummy-ns");
                } else {
                    attributeBean.setQualifiedName("dummy-ns");
                }
                attributeBean.addAttributeValue("myvalue");
                attrStateBean.setSamlAttributes(Collections.singletonList(attributeBean));
                callback.setAttributeStatementData(Collections.singletonList(attrStateBean));
                return;
            }

            List<AttributeBean> attributeList = new ArrayList<>();

            if (this.multiValueType.equals(MultiValue.MULTI_VALUE)
                || this.multiValueType.equals(MultiValue.ENC_VALUE)) {
//              <saml:Attribute xmlns:saml="urn:oasis:names:tc:SAML:1.0:assertion"
//                AttributeNamespace="http://schemas.xmlsoap.org/claims" AttributeName="roles">
//                <saml:AttributeValue>Value1</saml:AttributeValue>
//                <saml:AttributeValue>Value2</saml:AttributeValue>
//              </saml:Attribute>
//                 or
//              <saml:Attribute xmlns:saml="urn:oasis:names:tc:SAML:1.0:assertion"
//              AttributeNamespace="http://schemas.xmlsoap.org/claims" AttributeName="roles">
//              <saml:AttributeValue>Value1,Value2</saml:AttributeValue>
//            </saml:Attribute>
                AttributeBean attributeBean = new AttributeBean();
                if (subjectBean != null) {
                    // SAML 1.1
                    if (this.isUseNameFormatAsNamespace()) {
                        //Workaround for CXF-4484
                        attributeBean.setSimpleName(this.roleAttributeName);
                        //QualifiedName maps to AttributeNamespace in SAML1ComponentBuilder.createSamlv1Attribute()
                        attributeBean.setQualifiedName(ClaimTypes.URI_BASE.toString());
                    } else {
                        attributeBean.setSimpleName(getNameOfClaimType(this.roleAttributeName));
                        //QualifiedName maps to AttributeNamespace in SAML1ComponentBuilder.createSamlv1Attribute()
                        attributeBean.setQualifiedName(getNamespaceOfClaimType(this.roleAttributeName));
                    }
                } else {
                    // SAML 2.0
                    attributeBean.setQualifiedName(this.roleAttributeName);
                    attributeBean.setNameFormat(this.getAttributeNameFormat());
                }
                if (this.multiValueType.equals(MultiValue.MULTI_VALUE)) {
                    if (addRoleValue) {
                        for (String role : roles) {
                            attributeBean.addAttributeValue(role);
                        }
                    }
                } else {
                    StringBuilder sb = new StringBuilder();
                    for (String role: roles) {
                        sb.append(role).append(this.roleSeperator);
                    }
                    String value = sb.substring(0, sb.length() - this.roleSeperator.length());
                    attributeBean.addAttributeValue(value);
                }
                attributeList.add(attributeBean);
            } else if (this.multiValueType.equals(MultiValue.MULTI_ATTR)) {
//              <saml:Attribute xmlns:saml="urn:oasis:names:tc:SAML:1.0:assertion"
//                AttributeNamespace="http://schemas.xmlsoap.org/claims" AttributeName="roles">
//                <saml:AttributeValue>Value1</saml:AttributeValue>
//              </saml:Attribute>
//              <saml:Attribute xmlns:saml="urn:oasis:names:tc:SAML:1.0:assertion"
//                AttributeNamespace="http://schemas.xmlsoap.org/claims" AttributeName="roles">
//                <saml:AttributeValue>Value2</saml:AttributeValue>
//              </saml:Attribute>

                //List<AttributeBean> attrBeans = new ArrayList<>();
                for (String role: roles) {
                    AttributeBean attributeBean = new AttributeBean();
                    if (subjectBean != null) {
                        // SAML 1.1
                        if (this.isUseNameFormatAsNamespace()) {
                            //Workaround for CXF-4484
                            attributeBean.setSimpleName(this.roleAttributeName);
                            //QualifiedName maps to AttributeNamespace in SAML1ComponentBuilder.createSamlv1Attribute()
                            attributeBean.setQualifiedName(ClaimTypes.URI_BASE.toString());
                        } else {
                            attributeBean.setSimpleName(getNameOfClaimType(this.roleAttributeName));
                            //QualifiedName maps to AttributeNamespace in SAML1ComponentBuilder.createSamlv1Attribute()
                            attributeBean.setQualifiedName(getNamespaceOfClaimType(this.roleAttributeName));
                        }
                    } else {
                        // SAML 2.0
                        attributeBean.setQualifiedName(this.roleAttributeName);
                        attributeBean.setNameFormat(this.getAttributeNameFormat());
                    }
                    
                    if (addRoleValue) {
                        attributeBean.addAttributeValue(role);
                    }
                    attributeList.add(attributeBean);
                }
            }

            //ClaimTypes.COUNTRY
            AttributeBean attributeBean = new AttributeBean();
            if (subjectBean != null) {
                //SAML 1.1
                attributeBean.setSimpleName(getNameOfClaimType(this.countryClaimName));
                //QualifiedName maps to AttributeNamespace in SAML1ComponentBuilder.createSamlv1Attribute()
                attributeBean.setQualifiedName(getNamespaceOfClaimType(this.countryClaimName));

            } else {
                //SAML 2.0
                attributeBean.setQualifiedName(this.countryClaimName);
                attributeBean.setNameFormat(this.getAttributeNameFormat());
            }
            attributeBean.addAttributeValue("CH");
            attributeList.add(attributeBean);

            //custom claim language
            AttributeBean attributeBean2 = new AttributeBean();
            if (subjectBean != null) {
                // SAML 1.1
                if (this.isUseNameFormatAsNamespace()) {
                    //Workaround for CXF-4484
                    attributeBean2.setSimpleName(this.customClaimName);
                    //QualifiedName maps to AttributeNamespace in SAML1ComponentBuilder.createSamlv1Attribute()
                    attributeBean2.setQualifiedName(ClaimTypes.URI_BASE.toString());
                } else {
                    attributeBean2.setSimpleName(getNameOfClaimType(this.customClaimName));
                    //QualifiedName maps to AttributeNamespace in SAML1ComponentBuilder.createSamlv1Attribute()
                    attributeBean2.setQualifiedName(getNamespaceOfClaimType(this.customClaimName));
                }
            } else {
                // SAML 2
                attributeBean2.setQualifiedName(this.customClaimName);
                attributeBean2.setNameFormat(this.getAttributeNameFormat());
            }
            if (customAttributeValues != null && !customAttributeValues.isEmpty()) {
                for (Object obj : customAttributeValues) {
                    attributeBean2.addAttributeValue(obj);
                }
            } else {
                attributeBean2.addAttributeValue("CH");
            }
            attributeList.add(attributeBean2);

            if (addGivenName) {
                AttributeBean attributeBean3 = new AttributeBean();
                if (subjectBean != null) {
                    //SAML 1.1
                    attributeBean3.setSimpleName(getNameOfClaimType(ClaimTypes.FIRSTNAME.toString()));
                    //QualifiedName maps to AttributeNamespace in SAML1ComponentBuilder.createSamlv1Attribute()
                    attributeBean3.setQualifiedName(getNamespaceOfClaimType((ClaimTypes.FIRSTNAME.toString())));

                } else {
                    //SAML 2.0
                    attributeBean3.setQualifiedName((ClaimTypes.FIRSTNAME.toString()));
                    attributeBean3.setNameFormat(this.getAttributeNameFormat());
                }
                attributeBean3.addAttributeValue("alice");
                attributeList.add(attributeBean3);
            }

            attrStateBean.setSamlAttributes(attributeList);
            callback.setAttributeStatementData(Collections.singletonList(attrStateBean));

        } else if (statement == Statement.AUTHZ) {
            AuthDecisionStatementBean authzBean = new AuthDecisionStatementBean();
            if (subjectBean != null) {
                authzBean.setSubject(subjectBean);
            }
            ActionBean actionBean = new ActionBean();
            actionBean.setContents("Read");
            authzBean.setActions(Collections.singletonList(actionBean));
            authzBean.setResource("endpoint");
            authzBean.setDecision(AuthDecisionStatementBean.Decision.PERMIT);
            authzBean.setResource(resource);
            callback.setAuthDecisionStatementData(Collections.singletonList(authzBean));
        }
    }

    protected KeyInfoBean createKeyInfo() throws Exception {
        KeyInfoBean keyInfo = new KeyInfoBean();
        if (alsoAddAuthnStatement || statement == Statement.AUTHN) {
            keyInfo.setCertificate(certs[0]);
            keyInfo.setCertIdentifer(certIdentifier);
        } else if (statement == Statement.ATTR) {
            // Build a new Document
            DocumentBuilderFactory docBuilderFactory =
                DocumentBuilderFactory.newInstance();
            docBuilderFactory.setNamespaceAware(true);
            DocumentBuilder docBuilder = docBuilderFactory.newDocumentBuilder();
            Document doc = docBuilder.newDocument();

            // Create an Encrypted Key
            WSSecEncryptedKey encrKey = new WSSecEncryptedKey(doc);
            encrKey.setKeyIdentifierType(WSConstants.ISSUER_SERIAL);
            encrKey.setUseThisCert(certs[0]);

            KeyGenerator keyGen = KeyUtils.getKeyGenerator(WSConstants.AES_128);
            SecretKey symmetricKey = keyGen.generateKey();

            encrKey.prepare(null, symmetricKey);
            ephemeralKey = symmetricKey.getEncoded();
            Element encryptedKeyElement = encrKey.getEncryptedKeyElement();

            // Append the EncryptedKey to a KeyInfo element
            Element keyInfoElement =
                doc.createElementNS(
                    WSConstants.SIG_NS, WSConstants.SIG_PREFIX + ":" + WSConstants.KEYINFO_LN
                );
            keyInfoElement.setAttributeNS(
                WSConstants.XMLNS_NS, "xmlns:" + WSConstants.SIG_PREFIX, WSConstants.SIG_NS
            );
            keyInfoElement.appendChild(encryptedKeyElement);

            keyInfo.setElement(keyInfoElement);
        }
        return keyInfo;
    }

    protected String getNamespaceOfClaimType(String claimType) {
        int i = claimType.lastIndexOf('/');
        return claimType.substring(0, i);
    }

    protected String getNameOfClaimType(String claimType) {
        int i = claimType.lastIndexOf('/');
        return claimType.substring(i + 1);
    }

    public boolean isAlsoAddAuthnStatement() {
        return alsoAddAuthnStatement;
    }

    public void setAlsoAddAuthnStatement(boolean alsoAddAuthnStatement) {
        this.alsoAddAuthnStatement = alsoAddAuthnStatement;
    }

    public boolean isAddGivenName() {
        return addGivenName;
    }

    public void setAddGivenName(boolean addGivenName) {
        this.addGivenName = addGivenName;
    }

    public boolean isAddRoleValue() {
        return addRoleValue;
    }

    public void setAddRoleValue(boolean addRoleValue) {
        this.addRoleValue = addRoleValue;
    }
}
