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

import org.apereo.cas.configuration.CasConfigurationProperties;
import org.apereo.cas.support.saml.OpenSamlConfigBean;
import org.apereo.cas.support.saml.SamlException;
import org.apereo.cas.support.saml.SamlProtocolConstants;
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.apereo.cas.support.saml.web.idp.profile.builders.SamlProfileObjectBuilder;
import org.apereo.cas.support.saml.web.idp.profile.builders.enc.BaseSamlObjectSigner;
import org.apereo.cas.support.saml.web.idp.profile.builders.enc.SamlObjectEncrypter;
import org.opensaml.core.xml.XMLObject;
import org.opensaml.saml.common.SAMLObject;
import org.opensaml.saml.saml2.core.Assertion;
import org.opensaml.saml.saml2.core.AuthnRequest;
import org.opensaml.saml.saml2.core.EncryptedAssertion;
import org.opensaml.saml.saml2.core.Issuer;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.ui.velocity.VelocityEngineFactory;

import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;

/**
 * The {@link BaseSamlProfileSamlResponseBuilder} is responsible for
 * building the final SAML assertion for the relying party.
 *
 * @author Misagh Moayyed
 * @since 4.2
 */
public abstract class BaseSamlProfileSamlResponseBuilder<T extends XMLObject>
        extends AbstractSaml20ObjectBuilder implements SamlProfileObjectBuilder {
    private static final long serialVersionUID = -1891703354216174875L;
    private static final Logger LOGGER = LoggerFactory.getLogger(BaseSamlProfileSamlResponseBuilder.class);

    /**
     * The Saml object encoder.
     */
    protected BaseSamlObjectSigner samlObjectSigner;

    /**
     * The Velocity engine factory.
     */
    protected VelocityEngineFactory velocityEngineFactory;

    @Autowired
    private CasConfigurationProperties casProperties;

    private final SamlProfileObjectBuilder<Assertion> samlProfileSamlAssertionBuilder;

    private final SamlObjectEncrypter samlObjectEncrypter;

    public BaseSamlProfileSamlResponseBuilder(final OpenSamlConfigBean openSamlConfigBean,
                                              final BaseSamlObjectSigner samlObjectSigner,
                                              final VelocityEngineFactory velocityEngineFactory,
                                              final SamlProfileObjectBuilder<Assertion> samlProfileSamlAssertionBuilder,
                                              final SamlObjectEncrypter samlObjectEncrypter) {
        super(openSamlConfigBean);
        this.samlObjectSigner = samlObjectSigner;
        this.velocityEngineFactory = velocityEngineFactory;
        this.samlProfileSamlAssertionBuilder = samlProfileSamlAssertionBuilder;
        this.samlObjectEncrypter = samlObjectEncrypter;
    }

    @Override
    public T build(final AuthnRequest authnRequest,
                   final HttpServletRequest request,
                   final HttpServletResponse response,
                   final org.jasig.cas.client.validation.Assertion casAssertion,
                   final SamlRegisteredService service,
                   final SamlRegisteredServiceServiceProviderMetadataFacade adaptor,
                   final String binding) throws SamlException {
        final Assertion assertion = buildSamlAssertion(authnRequest, request, response,
                casAssertion, service, adaptor, binding);
        final T finalResponse = buildResponse(assertion, casAssertion, authnRequest,
                service, adaptor, request, response, binding);
        return encodeFinalResponse(request, response, service, adaptor, finalResponse, binding);
    }

    /**
     * Encode final response.
     *
     * @param request       the request
     * @param response      the response
     * @param service       the service
     * @param adaptor       the adaptor
     * @param finalResponse the final response
     * @param binding       the binding
     * @return the response
     */
    protected T encodeFinalResponse(final HttpServletRequest request,
                                    final HttpServletResponse response,
                                    final SamlRegisteredService service,
                                    final SamlRegisteredServiceServiceProviderMetadataFacade adaptor,
                                    final T finalResponse,
                                    final String binding) {
        final String relayState = request.getParameter(SamlProtocolConstants.PARAMETER_SAML_RELAY_STATE);
        LOGGER.debug("RelayState is [{}]", relayState);
        return encode(service, finalResponse, response, adaptor, relayState, binding);
    }

    /**
     * Build saml assertion assertion.
     *
     * @param authnRequest the authn request
     * @param request      the request
     * @param response     the response
     * @param casAssertion the cas assertion
     * @param service      the service
     * @param adaptor      the adaptor
     * @param binding      the binding
     * @return the assertion
     */
    protected Assertion buildSamlAssertion(final AuthnRequest authnRequest,
                                           final HttpServletRequest request,
                                           final HttpServletResponse response,
                                           final org.jasig.cas.client.validation.Assertion casAssertion,
                                           final SamlRegisteredService service,
                                           final SamlRegisteredServiceServiceProviderMetadataFacade adaptor,
                                           final String binding) {
        return this.samlProfileSamlAssertionBuilder.build(authnRequest, request, response,
                casAssertion, service, adaptor, binding);
    }


    /**
     * Build response response.
     *
     * @param assertion    the assertion
     * @param casAssertion the cas assertion
     * @param authnRequest the authn request
     * @param service      the service
     * @param adaptor      the adaptor
     * @param request      the request
     * @param response     the response
     * @param binding      the binding
     * @return the response
     * @throws SamlException the saml exception
     */
    protected abstract T buildResponse(Assertion assertion,
                                       org.jasig.cas.client.validation.Assertion casAssertion,
                                       AuthnRequest authnRequest,
                                       SamlRegisteredService service,
                                       SamlRegisteredServiceServiceProviderMetadataFacade adaptor,
                                       HttpServletRequest request,
                                       HttpServletResponse response,
                                       String binding) throws SamlException;

    /**
     * Build entity issuer issuer.
     *
     * @return the issuer
     */
    protected Issuer buildEntityIssuer() {
        final Issuer issuer = newIssuer(casProperties.getAuthn().getSamlIdp().getEntityId());
        issuer.setFormat(Issuer.ENTITY);
        return issuer;
    }

    /**
     * Encode the final result into the http response.
     *
     * @param service      the service
     * @param samlResponse the saml response
     * @param httpResponse the http response; may be null to mute encoding.
     * @param adaptor      the adaptor
     * @param relayState   the relay state
     * @param binding      the binding
     * @return the t
     * @throws SamlException the saml exception
     */
    protected abstract T encode(SamlRegisteredService service,
                                T samlResponse,
                                HttpServletResponse httpResponse,
                                SamlRegisteredServiceServiceProviderMetadataFacade adaptor,
                                String relayState,
                                String binding) throws SamlException;

    /**
     * Encrypt assertion.
     *
     * @param assertion the assertion
     * @param request   the request
     * @param response  the response
     * @param service   the service
     * @param adaptor   the adaptor
     * @return the saml object
     * @throws SamlException the saml exception
     */
    protected SAMLObject encryptAssertion(final Assertion assertion,
                                          final HttpServletRequest request, final HttpServletResponse response,
                                          final SamlRegisteredService service,
                                          final SamlRegisteredServiceServiceProviderMetadataFacade adaptor) throws SamlException {
        try {
            if (service.isEncryptAssertions()) {
                LOGGER.info("SAML service [{}] requires assertions to be encrypted", adaptor.getEntityId());
                final EncryptedAssertion encryptedAssertion =
                        this.samlObjectEncrypter.encode(assertion, service, adaptor, response, request);
                return encryptedAssertion;
            }
            LOGGER.info("SAML registered service [{}] does not require assertions to be encrypted", adaptor.getEntityId());
            return assertion;
        } catch (final Exception e) {
            throw new SamlException("Unable to marshall assertion for encryption", e);
        }
    }
}
