package org.apereo.cas.services.web.view;

import org.apache.commons.lang3.StringUtils;
import org.apereo.cas.CasProtocolConstants;
import org.apereo.cas.CasViewConstants;
import org.apereo.cas.authentication.Authentication;
import org.apereo.cas.authentication.ProtocolAttributeEncoder;
import org.apereo.cas.authentication.RememberMeCredential;
import org.apereo.cas.authentication.principal.Principal;
import org.apereo.cas.authentication.principal.Service;
import org.apereo.cas.services.RegisteredService;
import org.apereo.cas.services.RegisteredServiceAttributeReleasePolicy;
import org.apereo.cas.services.ServicesManager;
import org.apereo.cas.validation.Assertion;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.web.servlet.view.AbstractView;

import java.time.ZonedDateTime;
import java.util.Collection;
import java.util.Collections;
import java.util.Enumeration;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.stream.Collectors;

/**
 * Abstract class to handle retrieving the Assertion from the model.
 *
 * @author Scott Battaglia
 * @since 3.1
 */
public abstract class AbstractCasView extends AbstractView {
    private static final Logger LOGGER = LoggerFactory.getLogger(AbstractCasView.class);

    /**
     * Indicate whether this view will be generating the success response or not.
     * By default, the view is treated as a failure.
     */
    protected final boolean successResponse;

    /**
     * The attribute encoder instance.
     */
    protected final ProtocolAttributeEncoder protocolAttributeEncoder;

    /**
     * The Services manager.
     */
    protected final ServicesManager servicesManager;


    /**
     * authentication context attribute name.
     */
    protected final String authenticationContextAttribute;

    public AbstractCasView(final boolean successResponse,
                           final ProtocolAttributeEncoder protocolAttributeEncoder,
                           final ServicesManager servicesManager,
                           final String authenticationContextAttribute) {
        this.successResponse = successResponse;
        this.protocolAttributeEncoder = protocolAttributeEncoder;
        this.servicesManager = servicesManager;
        this.authenticationContextAttribute = authenticationContextAttribute;
    }

    /**
     * Gets the assertion from the model.
     *
     * @param model the model
     * @return the assertion from
     */
    protected Assertion getAssertionFrom(final Map<String, Object> model) {
        return (Assertion) model.get(CasViewConstants.MODEL_ATTRIBUTE_NAME_ASSERTION);
    }

    /**
     * Gets error code from.
     *
     * @param model the model
     * @return the error code from
     */
    protected String getErrorCodeFrom(final Map<String, Object> model) {
        return model.get(CasViewConstants.MODEL_ATTRIBUTE_NAME_ERROR_CODE).toString();
    }

    /**
     * Gets error description from.
     *
     * @param model the model
     * @return the error description from
     */
    protected String getErrorDescriptionFrom(final Map<String, Object> model) {
        return model.get(CasViewConstants.MODEL_ATTRIBUTE_NAME_ERROR_DESCRIPTION).toString();
    }

    /**
     * Gets the PGT from the model.
     *
     * @param model the model
     * @return the pgt id
     */
    protected String getProxyGrantingTicketId(final Map<String, Object> model) {
        return (String) model.get(CasViewConstants.MODEL_ATTRIBUTE_NAME_PROXY_GRANTING_TICKET);
    }

    /**
     * Gets the PGT-IOU from the model.
     *
     * @param model the model
     * @return the pgt-iou id
     */
    protected String getProxyGrantingTicketIou(final Map<String, Object> model) {
        return (String) model.get(CasViewConstants.MODEL_ATTRIBUTE_NAME_PROXY_GRANTING_TICKET_IOU);
    }


    /**
     * Gets the authentication from the model.
     *
     * @param model the model
     * @return the assertion from
     * @since 4.1.0
     */
    protected Authentication getPrimaryAuthenticationFrom(final Map<String, Object> model) {
        return getAssertionFrom(model).getPrimaryAuthentication();
    }

    /**
     * Gets model attributes.
     *
     * @param model the model
     * @return the model attributes
     */
    protected Map<String, Object> getModelAttributes(final Map<String, Object> model) {
        return (Map<String, Object>) model.get(CasProtocolConstants.VALIDATION_CAS_MODEL_ATTRIBUTE_NAME_ATTRIBUTES);
    }

    /**
     * Gets authentication attributes from the primary authentication object.
     *
     * @param model the model
     * @return the authentication attribute
     */
    protected Map<String, Object> getAuthenticationAttributes(final Map<String, Object> model) {
        final Authentication authn = getPrimaryAuthenticationFrom(model);
        return authn.getAttributes();
    }

    /**
     * Gets an authentication attribute from the primary authentication object.
     *
     * @param model         the model
     * @param attributeName the attribute name
     * @return the authentication attribute
     */
    protected String getAuthenticationAttribute(final Map<String, Object> model, final String attributeName) {
        final Authentication authn = getPrimaryAuthenticationFrom(model);
        return (String) authn.getAttributes().get(attributeName);
    }

    /**
     * Gets the principal from the model.
     *
     * @param model the model
     * @return the assertion from
     * @since 4.1.0
     */
    protected Principal getPrincipal(final Map<String, Object> model) {
        return getPrimaryAuthenticationFrom(model).getPrincipal();
    }

    /**
     * Gets principal attributes.
     * Single-valued attributes are converted to a collection
     * so the review can easily loop through all.
     *
     * @param model the model
     * @return the attributes
     * @see #convertAttributeValuesToMultiValuedObjects(java.util.Map)
     * @since 4.1.0
     */
    protected Map<String, Object> getPrincipalAttributesAsMultiValuedAttributes(final Map<String, Object> model) {
        return convertAttributeValuesToMultiValuedObjects(getPrincipal(model).getAttributes());
    }

    /**
     * Gets authentication attributes.
     * Single-valued attributes are converted to a collection
     * so the review can easily loop through all.
     *
     * @param model the model
     * @return the attributes
     * @see #convertAttributeValuesToMultiValuedObjects(java.util.Map)
     * @since 4.1.0
     */
    protected Map<String, Object> getAuthenticationAttributesAsMultiValuedAttributes(final Map<String, Object> model) {
        return convertAttributeValuesToMultiValuedObjects(getPrimaryAuthenticationFrom(model).getAttributes());
    }

    /**
     * Is remember me authentication?
     * looks at the authentication object to find {@link RememberMeCredential#AUTHENTICATION_ATTRIBUTE_REMEMBER_ME}
     * and expects the assertion to also note a new login session.
     *
     * @param model the model
     * @return true if remember-me, false if otherwise.
     */
    protected boolean isRememberMeAuthentication(final Map<String, Object> model) {
        final Map<String, Object> authnAttributes = getAuthenticationAttributesAsMultiValuedAttributes(model);
        final Collection authnMethod = (Collection) authnAttributes.get(RememberMeCredential.AUTHENTICATION_ATTRIBUTE_REMEMBER_ME);
        return authnMethod != null && authnMethod.contains(Boolean.TRUE) && isAssertionBackedByNewLogin(model);
    }


    /**
     * Gets satisfied multifactor authentication provider.
     *
     * @param model the model
     * @return the satisfied multifactor authentication provider
     */
    protected String getSatisfiedMultifactorAuthenticationProviderId(final Map<String, Object> model) {
        if (StringUtils.isNotBlank(authenticationContextAttribute) && model.containsKey(this.authenticationContextAttribute)) {
            return model.get(this.authenticationContextAttribute).toString();
        }
        return null;
    }

    /**
     * Is assertion backed by new login?
     *
     * @param model the model
     * @return true/false.
     */
    protected boolean isAssertionBackedByNewLogin(final Map<String, Object> model) {
        return getAssertionFrom(model).isFromNewLogin();
    }

    /**
     * Convert attribute values to multi valued objects.
     *
     * @param attributes the attributes
     * @return the map of attributes to return
     */
    private static Map<String, Object> convertAttributeValuesToMultiValuedObjects(final Map<String, Object> attributes) {
        final Set<Map.Entry<String, Object>> entries = attributes.entrySet();
        return entries.stream().collect(Collectors.toMap(Map.Entry::getKey, entry -> {
            final Object value = entry.getValue();
            if (value instanceof Collection || value instanceof Map || value instanceof Object[]
                    || value instanceof Iterator || value instanceof Enumeration) {
                return value;
            }
            return Collections.singleton(value);

        }));
    }

    /**
     * Gets authentication date.
     *
     * @param model the model
     * @return the authentication date
     * @since 4.1.0
     */
    protected ZonedDateTime getAuthenticationDate(final Map<String, Object> model) {
        return getPrimaryAuthenticationFrom(model).getAuthenticationDate();
    }

    /**
     * Gets validated service from the model.
     *
     * @param model the model
     * @return the validated service from
     */
    protected Service getServiceFrom(final Map<String, Object> model) {
        return (Service) model.get(CasViewConstants.MODEL_ATTRIBUTE_NAME_SERVICE);
    }

    /**
     * Gets chained authentications.
     * Note that the last index in the list always describes the primary authentication
     * event. All others in the chain should denote proxies. Per the CAS protocol,
     * when authentication has proceeded through multiple proxies,
     * the order in which the proxies were traversed MUST be reflected in the response.
     * The most recently-visited proxy MUST be the first proxy listed, and all the
     * other proxies MUST be shifted down as new proxies are added.
     *
     * @param model the model
     * @return the chained authentications
     */
    protected Collection<Authentication> getChainedAuthentications(final Map<String, Object> model) {
        final Assertion assertion = getAssertionFrom(model);
        final List<Authentication> chainedAuthentications = assertion.getChainedAuthentications();
        return chainedAuthentications.stream().limit(chainedAuthentications.size() - 1).collect(Collectors.toList());
    }

    /**
     * Decide if credential password should be released as attribute.
     * The credential must have been cached as an authentication attribute
     * and the attribute release policy must be allowed to release the
     * attribute.
     *
     * @param attributes the attributes
     * @param model      the model
     * @param service    the service
     */
    protected void decideIfCredentialPasswordShouldBeReleasedAsAttribute(final Map<String, Object> attributes,
                                                                         final Map<String, Object> model,
                                                                         final RegisteredService service) {

        final RegisteredServiceAttributeReleasePolicy policy = service.getAttributeReleasePolicy();
        final boolean isAuthorized = policy != null && policy.isAuthorizedToReleaseCredentialPassword();

        decideAttributeReleaseBasedOnServiceAttributePolicy(attributes,
                getAuthenticationAttribute(model, CasViewConstants.MODEL_ATTRIBUTE_NAME_PRINCIPAL_CREDENTIAL),
                CasViewConstants.MODEL_ATTRIBUTE_NAME_PRINCIPAL_CREDENTIAL,
                service, isAuthorized);
    }

    /**
     * Decide if PGT should be released as attribute.
     * The PGT must have been cached as an authentication attribute
     * and the attribute release policy must be allowed to release the
     * attribute.
     *
     * @param attributes the attributes
     * @param model      the model
     * @param service    the service
     */
    protected void decideIfProxyGrantingTicketShouldBeReleasedAsAttribute(final Map<String, Object> attributes,
                                                                          final Map<String, Object> model,
                                                                          final RegisteredService service) {
        final RegisteredServiceAttributeReleasePolicy policy = service.getAttributeReleasePolicy();
        final boolean isAuthorized = policy != null && policy.isAuthorizedToReleaseProxyGrantingTicket();

        decideAttributeReleaseBasedOnServiceAttributePolicy(attributes,
                getProxyGrantingTicketId(model),
                CasViewConstants.MODEL_ATTRIBUTE_NAME_PROXY_GRANTING_TICKET,
                service, isAuthorized);
    }

    /**
     * Decide attribute release based on service attribute policy.
     *
     * @param attributes               the attributes
     * @param attributeValue           the attribute value
     * @param attributeName            the attribute name
     * @param service                  the service
     * @param doesAttributePolicyAllow does attribute policy allow release of this attribute?
     */
    protected void decideAttributeReleaseBasedOnServiceAttributePolicy(final Map<String, Object> attributes,
                                                                       final String attributeValue,
                                                                       final String attributeName,
                                                                       final RegisteredService service,
                                                                       final boolean doesAttributePolicyAllow) {
        if (StringUtils.isNotBlank(attributeValue)) {
            LOGGER.debug("Obtained [{}] as an authentication attribute", attributeName);

            if (doesAttributePolicyAllow) {
                LOGGER.debug("Obtained [{}] is passed to the CAS validation payload", attributeName);
                attributes.put(attributeName, Collections.singleton(attributeValue));
            } else {
                LOGGER.debug("Attribute release policy for [{}] does not authorize the release of [{}]",
                        service.getServiceId(), attributeName);
                attributes.remove(attributeName);
            }
        } else {
            LOGGER.trace("[{}] is not available and will not be released to the validation response.", attributeName);
        }
    }


    /**
     * Put into model.
     *
     * @param model the model
     * @param key   the key
     * @param value the value
     */
    protected void putIntoModel(final Map<String, Object> model, final String key, final Object value) {
        model.put(key, value);
    }

    /**
     * Put all into model.
     *
     * @param model  the model
     * @param values the values
     */
    protected void putAllIntoModel(final Map<String, Object> model, final Map<String, Object> values) {
        model.putAll(values);
    }

    public ProtocolAttributeEncoder getProtocolAttributeEncoder() {
        return this.protocolAttributeEncoder;
    }

    public ServicesManager getServicesManager() {
        return this.servicesManager;
    }

    public String getAuthenticationContextAttribute() {
        return authenticationContextAttribute;
    }

}
