package org.apereo.cas.support.openid.authentication.principal;

import org.apache.commons.lang3.StringUtils;
import org.apereo.cas.CentralAuthenticationService;
import org.apereo.cas.authentication.principal.AbstractWebApplicationServiceResponseBuilder;
import org.apereo.cas.authentication.principal.Response;
import org.apereo.cas.authentication.principal.WebApplicationService;
import org.apereo.cas.support.openid.OpenIdProtocolConstants;
import org.apereo.cas.ticket.AbstractTicketException;
import org.apereo.cas.validation.Assertion;
import org.apereo.cas.web.support.WebUtils;
import org.openid4java.association.Association;
import org.openid4java.message.AuthRequest;
import org.openid4java.message.Message;
import org.openid4java.message.MessageException;
import org.openid4java.message.ParameterList;
import org.openid4java.server.ServerManager;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import java.util.HashMap;
import java.util.Map;

/**
 * Builds responses to Openid authN requests.
 *
 * @author Misagh Moayyed
 * @since 4.2
 */
public class OpenIdServiceResponseBuilder extends AbstractWebApplicationServiceResponseBuilder {
    private static final Logger LOGGER = LoggerFactory.getLogger(OpenIdServiceResponseBuilder.class);

    private static final long serialVersionUID = -4581238964007702423L;

    private ServerManager serverManager;

    private CentralAuthenticationService centralAuthenticationService;

    private String openIdPrefixUrl;

    /**
     * Instantiates a new Open id service response builder.
     *
     * @param openIdPrefixUrl              the open id prefix url
     * @param serverManager                the server manager
     * @param centralAuthenticationService the central authentication service
     */
    public OpenIdServiceResponseBuilder(final String openIdPrefixUrl,
                                        final ServerManager serverManager,
                                        final CentralAuthenticationService centralAuthenticationService) {
        this.openIdPrefixUrl = openIdPrefixUrl;
        this.serverManager = serverManager;
        this.centralAuthenticationService = centralAuthenticationService;
    }

    /**
     * Generates an Openid response.
     * If no ticketId is found, response is negative.
     * If we have a ticket id, then we check if we have an association.
     * If so, we ask OpenId server manager to generate the answer according with the existing association.
     * If not, we send back an answer with the ticket id as association handle.
     * This will force the consumer to ask a verification, which will validate the service ticket.
     *
     * @param ticketId              the service ticket to provide to the service.
     * @param webApplicationService the service requesting an openid response
     * @return the generated authentication answer
     */
    @Override
    public Response build(final WebApplicationService webApplicationService, final String ticketId) {

        final OpenIdService service = (OpenIdService) webApplicationService;
        final ParameterList parameterList = new ParameterList(WebUtils.getHttpServletRequestFromRequestAttributes().getParameterMap());

        final Map<String, String> parameters = new HashMap<>();

        if (StringUtils.isBlank(ticketId)) {
            parameters.put(OpenIdProtocolConstants.OPENID_MODE, OpenIdProtocolConstants.CANCEL);
            return buildRedirect(service, parameters);
        }

        final Association association = getAssociation(serverManager, parameterList);
        final boolean associated = association != null;
        final boolean associationValid = isAssociationValid(association);
        boolean successFullAuthentication = true;

        Assertion assertion = null;
        try {
            if (associated && associationValid) {
                assertion = centralAuthenticationService.validateServiceTicket(ticketId, service);
                LOGGER.debug("Validated openid ticket [{}] for [{}]", ticketId, service);
            } else if (!associated) {
                LOGGER.debug("Responding to non-associated mode. Service ticket [{}] must be validated by the RP", ticketId);
            } else {
                LOGGER.warn("Association does not exist or is not valid");
                successFullAuthentication = false;
            }
        } catch (final AbstractTicketException e) {
            LOGGER.error("Could not validate ticket : [{}]", e.getMessage(), e);
            successFullAuthentication = false;
        }
        final String id = determineIdentity(service, assertion);
        return buildAuthenticationResponse(service, parameters, successFullAuthentication, id, parameterList);
    }

    /**
     * Determine identity.
     *
     * @param service   the service
     * @param assertion the assertion
     * @return the string
     */
    protected String determineIdentity(final OpenIdService service, final Assertion assertion) {
        final String id;
        if (assertion != null && OpenIdProtocolConstants.OPENID_IDENTIFIERSELECT.equals(service.getIdentity())) {
            id = this.openIdPrefixUrl + '/' + assertion.getPrimaryAuthentication().getPrincipal().getId();
        } else {
            id = service.getIdentity();
        }
        return id;
    }

    /**
     * We sign directly (final 'true') because we don't add extensions
     * response message can be either a DirectError or an AuthSuccess here.
     * Note:
     * The association handle returned in the Response is either the 'public'
     * created in a previous association, or is a 'private' handle created
     * specifically for the verification step when in non-association mode
     *
     * @param service                   the service
     * @param parameters                the parameters
     * @param successFullAuthentication the success full authentication
     * @param id                        the id
     * @param parameterList             the parameter list
     * @return response response
     */
    protected Response buildAuthenticationResponse(final OpenIdService service,
                                                   final Map<String, String> parameters,
                                                   final boolean successFullAuthentication,
                                                   final String id,
                                                   final ParameterList parameterList) {
        final Message response = serverManager.authResponse(parameterList, id, id, successFullAuthentication, true);
        parameters.putAll(response.getParameterMap());
        LOGGER.debug("Parameters passed for the OpenID response are [{}]", parameters.keySet());
        return buildRedirect(service, parameters);
    }

    /**
     * Gets association.
     *
     * @param serverManager the server manager
     * @param parameterList the parameter list
     * @return the association
     */
    protected Association getAssociation(final ServerManager serverManager, final ParameterList parameterList) {
        try {
            final AuthRequest authReq = AuthRequest.createAuthRequest(parameterList, serverManager.getRealmVerifier());
            final Map parameterMap = authReq.getParameterMap();
            if (parameterMap != null && !parameterMap.isEmpty()) {
                final String assocHandle = (String) parameterMap.get(OpenIdProtocolConstants.OPENID_ASSOCHANDLE);
                if (assocHandle != null) {
                    return serverManager.getSharedAssociations().load(assocHandle);
                }
            }
        } catch (final MessageException e) {
            LOGGER.error("Message exception : [{}]", e.getMessage(), e);
        }
        return null;
    }

    @Override
    public boolean supports(final WebApplicationService service) {
        return service instanceof OpenIdService;
    }

    /**
     * Is association valid.
     *
     * @param association the association
     * @return true/false
     */
    protected boolean isAssociationValid(final Association association) {
        return association != null && !association.hasExpired();
    }
}
