package org.apereo.cas.support.saml.web.idp.profile.builders;

import net.shibboleth.idp.attribute.IdPAttribute;
import net.shibboleth.idp.attribute.IdPAttributeValue;
import net.shibboleth.idp.attribute.StringAttributeValue;
import net.shibboleth.idp.saml.attribute.encoding.impl.SAML2StringNameIDEncoder;
import org.apache.commons.lang3.StringUtils;
import org.apereo.cas.support.saml.OpenSamlConfigBean;
import org.apereo.cas.support.saml.SamlException;
import org.apereo.cas.support.saml.SamlIdPUtils;
import org.apereo.cas.support.saml.services.SamlRegisteredService;
import org.apereo.cas.support.saml.services.idp.metadata.SamlRegisteredServiceServiceProviderMetadataFacade;
import org.apereo.cas.support.saml.util.AbstractSaml20ObjectBuilder;
import org.jasig.cas.client.authentication.AttributePrincipal;
import org.jasig.cas.client.validation.Assertion;
import org.opensaml.saml.saml2.core.AuthnRequest;
import org.opensaml.saml.saml2.core.NameID;
import org.opensaml.saml.saml2.core.NameIDType;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.util.Collections;
import java.util.List;

/**
 * This is {@link SamlProfileSamlNameIdBuilder}.
 *
 * @author Misagh Moayyed
 * @since 5.0.0
 */
public class SamlProfileSamlNameIdBuilder extends AbstractSaml20ObjectBuilder implements SamlProfileObjectBuilder<NameID> {
    private static final long serialVersionUID = -6231886395225437320L;
    private static final Logger LOGGER = LoggerFactory.getLogger(SamlProfileSamlNameIdBuilder.class);

    public SamlProfileSamlNameIdBuilder(final OpenSamlConfigBean configBean) {
        super(configBean);
    }

    @Override
    public NameID build(final AuthnRequest authnRequest, final HttpServletRequest request, final HttpServletResponse response,
                        final Assertion assertion, final SamlRegisteredService service,
                        final SamlRegisteredServiceServiceProviderMetadataFacade adaptor,
                        final String binding)
            throws SamlException {
        return buildNameId(authnRequest, assertion, service, adaptor);
    }

    /**
     * Build name id.
     * If there are no explicitly defined NameIDFormats, include the default format.
     * see: http://saml2int.org/profile/current/#section92
     *
     * @param authnRequest the authn request
     * @param assertion    the assertion
     * @param service      the service
     * @param adaptor      the adaptor
     * @return the name id
     * @throws SamlException the saml exception
     */
    private NameID buildNameId(final AuthnRequest authnRequest,
                               final Assertion assertion,
                               final SamlRegisteredService service,
                               final SamlRegisteredServiceServiceProviderMetadataFacade adaptor) throws SamlException {
        final List<String> supportedNameFormats = getSupportedNameIdFormats(service, adaptor);
        final String requiredNameFormat = getRequiredNameIdFormatIfAny(authnRequest);
        validateRequiredNameIdFormatIfAny(authnRequest, adaptor, supportedNameFormats, requiredNameFormat);
        final NameID nameid = determineNameId(authnRequest, assertion, supportedNameFormats, service, adaptor);
        return finalizeNameId(nameid, authnRequest, assertion, supportedNameFormats, service, adaptor);
    }

    /**
     * Finalize name id name id.
     *
     * @param nameid               the nameid
     * @param authnRequest         the authn request
     * @param assertion            the assertion
     * @param supportedNameFormats the supported name formats
     * @param service              the service
     * @param adaptor              the adaptor
     * @return the name id
     */
    protected NameID finalizeNameId(final NameID nameid,
                                    final AuthnRequest authnRequest,
                                    final Assertion assertion,
                                    final List<String> supportedNameFormats,
                                    final SamlRegisteredService service,
                                    final SamlRegisteredServiceServiceProviderMetadataFacade adaptor) {
        
        if (StringUtils.isNotBlank(service.getNameIdQualifier())) {
            nameid.setNameQualifier(service.getNameIdQualifier());
        }
        if (StringUtils.isNotBlank(service.getServiceProviderNameIdQualifier())) {
            nameid.setSPNameQualifier(service.getServiceProviderNameIdQualifier());
        }

        return nameid;
    }

    /**
     * Validate required name id format if any.
     *
     * @param authnRequest         the authn request
     * @param adaptor              the adaptor
     * @param supportedNameFormats the supported name formats
     * @param requiredNameFormat   the required name format
     */
    protected void validateRequiredNameIdFormatIfAny(final AuthnRequest authnRequest,
                                                     final SamlRegisteredServiceServiceProviderMetadataFacade adaptor,
                                                     final List<String> supportedNameFormats,
                                                     final String requiredNameFormat) {
        if (StringUtils.isNotBlank(requiredNameFormat) && !supportedNameFormats.contains(requiredNameFormat)) {
            LOGGER.warn("Required NameID format [{}] in the AuthN request issued by [{}] is not supported based on the metadata for [{}]",
                    requiredNameFormat, SamlIdPUtils.getIssuerFromSamlRequest(authnRequest), adaptor.getEntityId());
            throw new SamlException("Unsupported required NameID format cannot be provided");
        }
    }

    /**
     * Gets required name id format if any.
     *
     * @param authnRequest the authn request
     * @return the required name id format if any
     */
    protected String getRequiredNameIdFormatIfAny(final AuthnRequest authnRequest) {
        String requiredNameFormat = null;
        if (authnRequest.getNameIDPolicy() != null) {
            requiredNameFormat = authnRequest.getNameIDPolicy().getFormat();
            LOGGER.debug("AuthN request indicates [{}] is the required NameID format", requiredNameFormat);
            if (NameID.ENCRYPTED.equals(requiredNameFormat)) {
                LOGGER.warn("Encrypted NameID formats are not supported");
                requiredNameFormat = null;
            }
        }
        return requiredNameFormat;
    }

    /**
     * Gets supported name id formats.
     *
     * @param service the service
     * @param adaptor the adaptor
     * @return the supported name id formats
     */
    protected static List<String> getSupportedNameIdFormats(final SamlRegisteredService service,
                                                            final SamlRegisteredServiceServiceProviderMetadataFacade adaptor) {
        final List<String> supportedNameFormats = adaptor.getSupportedNameIdFormats();
        LOGGER.debug("Metadata for [{}] declares the following NameIDs [{}]", adaptor.getEntityId(), supportedNameFormats);

        if (supportedNameFormats.isEmpty()) {
            supportedNameFormats.add(NameIDType.TRANSIENT);
            LOGGER.debug("No supported nameId formats could be determined from metadata. Added default [{}]", NameIDType.TRANSIENT);
        }
        if (StringUtils.isNotBlank(service.getRequiredNameIdFormat())) {
            final String fmt = parseAndBuildRequiredNameIdFormat(service);
            supportedNameFormats.add(0, fmt);
            LOGGER.debug("Added required nameId format [{}] based on saml service configuration for [{}]", fmt, service.getServiceId());
        }
        return supportedNameFormats;
    }

    /**
     * Determine name id name id.
     *
     * @param authnRequest         the authn request
     * @param assertion            the assertion
     * @param supportedNameFormats the supported name formats
     * @param service              the service
     * @param adaptor              the adaptor
     * @return the name id
     */
    protected NameID determineNameId(final AuthnRequest authnRequest,
                                     final Assertion assertion,
                                     final List<String> supportedNameFormats,
                                     final SamlRegisteredService service,
                                     final SamlRegisteredServiceServiceProviderMetadataFacade adaptor) {
        for (final String nameFormat : supportedNameFormats) {
            LOGGER.debug("Evaluating NameID format [{}]", nameFormat);
            final NameID nameid = encodeNameIdBasedOnNameFormat(authnRequest, assertion, nameFormat, service, adaptor);
            if (nameid != null) {
                return nameid;
            }
        }
        return null;
    }

    /**
     * Encode name id based on name format name id.
     *
     * @param authnRequest the authn request
     * @param assertion    the assertion
     * @param nameFormat   the name format
     * @param service      the service
     * @param adaptor      the adaptor
     * @return the name id
     */
    protected NameID encodeNameIdBasedOnNameFormat(final AuthnRequest authnRequest,
                                                   final Assertion assertion,
                                                   final String nameFormat,
                                                   final SamlRegisteredService service,
                                                   final SamlRegisteredServiceServiceProviderMetadataFacade adaptor) {
        try {
            final IdPAttribute attribute = prepareNameIdAttribute(assertion);
            final SAML2StringNameIDEncoder encoder = prepareNameIdEncoder(authnRequest, nameFormat, attribute, service, adaptor);
            LOGGER.debug("Encoding NameID based on [{}]", nameFormat);
            final NameID nameid = encoder.encode(attribute);
            LOGGER.debug("Final NameID encoded with format [{}] has value [{}]", nameid.getFormat(), nameid.getValue());
            return nameid;
        } catch (final Exception e) {
            LOGGER.error(e.getMessage(), e);
        }
        return null;
    }

    /**
     * Prepare name id attribute id p attribute.
     *
     * @param assertion the assertion
     * @return the id p attribute
     */
    protected IdPAttribute prepareNameIdAttribute(final Assertion assertion) {
        final IdPAttribute attribute = new IdPAttribute(AttributePrincipal.class.getName());
        final IdPAttributeValue<String> value = new StringAttributeValue(assertion.getPrincipal().getName());
        LOGGER.debug("NameID attribute value is set to [{}]", assertion.getPrincipal().getName());
        attribute.setValues(Collections.singletonList(value));
        return attribute;
    }

    /**
     * Prepare name id encoder saml 2 string name id encoder.
     *
     * @param authnRequest the authn request
     * @param nameFormat   the name format
     * @param attribute    the attribute
     * @param service      the service
     * @param adaptor      the adaptor
     * @return the saml 2 string name id encoder
     */
    protected SAML2StringNameIDEncoder prepareNameIdEncoder(final AuthnRequest authnRequest,
                                                            final String nameFormat,
                                                            final IdPAttribute attribute,
                                                            final SamlRegisteredService service,
                                                            final SamlRegisteredServiceServiceProviderMetadataFacade adaptor) {
        final SAML2StringNameIDEncoder encoder = new SAML2StringNameIDEncoder();
        encoder.setNameFormat(nameFormat);
        if (authnRequest.getNameIDPolicy() != null) {
            final String qualifier = authnRequest.getNameIDPolicy().getSPNameQualifier();
            LOGGER.debug("NameID qualifier is set to [{}]", qualifier);
            encoder.setNameQualifier(qualifier);
        }
        return encoder;
    }

    private static String parseAndBuildRequiredNameIdFormat(final SamlRegisteredService service) {
        final String fmt = service.getRequiredNameIdFormat().trim();
        if (StringUtils.containsIgnoreCase(NameIDType.EMAIL, fmt)) {
            return NameIDType.EMAIL;
        }
        if (StringUtils.containsIgnoreCase(NameIDType.TRANSIENT, fmt)) {
            return NameIDType.TRANSIENT;
        }
        if (StringUtils.containsIgnoreCase(NameIDType.UNSPECIFIED, fmt)) {
            return NameIDType.UNSPECIFIED;
        }
        if (StringUtils.containsIgnoreCase(NameIDType.PERSISTENT, fmt)) {
            return NameIDType.PERSISTENT;
        }
        if (StringUtils.containsIgnoreCase(NameIDType.ENTITY, fmt)) {
            return NameIDType.ENTITY;
        }
        if (StringUtils.containsIgnoreCase(NameIDType.X509_SUBJECT, fmt)) {
            return NameIDType.X509_SUBJECT;
        }
        if (StringUtils.containsIgnoreCase(NameIDType.WIN_DOMAIN_QUALIFIED, fmt)) {
            return NameIDType.WIN_DOMAIN_QUALIFIED;
        }
        if (StringUtils.containsIgnoreCase(NameIDType.KERBEROS, fmt)) {
            return NameIDType.KERBEROS;
        }
        if (StringUtils.containsIgnoreCase(NameIDType.ENCRYPTED, fmt)) {
            return NameIDType.ENCRYPTED;
        }
        return fmt;
    }

}
