package org.apereo.cas.services;

import org.apache.commons.lang3.StringUtils;
import org.apache.commons.lang3.builder.EqualsBuilder;
import org.apache.commons.lang3.builder.HashCodeBuilder;
import org.apache.commons.lang3.builder.ToStringBuilder;
import org.apereo.cas.authentication.principal.DefaultPrincipalAttributesRepository;
import org.apereo.cas.authentication.principal.Principal;
import org.apereo.cas.authentication.principal.PrincipalAttributesRepository;
import org.apereo.cas.authentication.principal.Service;
import org.apereo.cas.configuration.CasConfigurationProperties;
import org.apereo.cas.util.spring.ApplicationContextProvider;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.context.ApplicationContext;

import java.util.Map;
import java.util.Set;
import java.util.TreeMap;

/**
 * Abstract release policy for attributes, provides common shared settings such as loggers and attribute filter config.
 * Subclasses are to provide the behavior for attribute retrieval.
 *
 * @author Misagh Moayyed
 * @since 4.1.0
 */
public abstract class AbstractRegisteredServiceAttributeReleasePolicy implements RegisteredServiceAttributeReleasePolicy {
    private static final long serialVersionUID = 5325460875620586503L;

    private static final Logger LOGGER = LoggerFactory.getLogger(AbstractRegisteredServiceAttributeReleasePolicy.class);

    private RegisteredServiceAttributeFilter registeredServiceAttributeFilter;
    private PrincipalAttributesRepository principalAttributesRepository = new DefaultPrincipalAttributesRepository();

    private boolean authorizedToReleaseCredentialPassword;
    private boolean authorizedToReleaseProxyGrantingTicket;
    private boolean excludeDefaultAttributes;
    private String principalIdAttribute;

    @Override
    public void setAttributeFilter(final RegisteredServiceAttributeFilter filter) {
        this.registeredServiceAttributeFilter = filter;
    }

    public void setPrincipalAttributesRepository(final PrincipalAttributesRepository repository) {
        this.principalAttributesRepository = repository;
    }

    public PrincipalAttributesRepository getPrincipalAttributesRepository() {
        return this.principalAttributesRepository;
    }

    public RegisteredServiceAttributeFilter getAttributeFilter() {
        return this.registeredServiceAttributeFilter;
    }

    public String getPrincipalIdAttribute() {
        return principalIdAttribute;
    }

    public void setPrincipalIdAttribute(final String principalIdAttribute) {
        this.principalIdAttribute = principalIdAttribute;
    }

    @Override
    public boolean isAuthorizedToReleaseCredentialPassword() {
        return this.authorizedToReleaseCredentialPassword;
    }

    @Override
    public boolean isAuthorizedToReleaseProxyGrantingTicket() {
        return this.authorizedToReleaseProxyGrantingTicket;
    }

    public void setAuthorizedToReleaseCredentialPassword(final boolean authorizedToReleaseCredentialPassword) {
        this.authorizedToReleaseCredentialPassword = authorizedToReleaseCredentialPassword;
    }

    public void setAuthorizedToReleaseProxyGrantingTicket(final boolean authorizedToReleaseProxyGrantingTicket) {
        this.authorizedToReleaseProxyGrantingTicket = authorizedToReleaseProxyGrantingTicket;
    }

    public boolean isExcludeDefaultAttributes() {
        return excludeDefaultAttributes;
    }

    public void setExcludeDefaultAttributes(final boolean excludeDefaultAttributes) {
        this.excludeDefaultAttributes = excludeDefaultAttributes;
    }

    @Override
    public Map<String, Object> getAttributes(final Principal principal, final Service selectedService,
                                             final RegisteredService registeredService) {
        LOGGER.debug("Locating principal attributes for [{}]", principal.getId());
        final Map<String, Object> principalAttributes = getPrincipalAttributesRepository() == null
                ? principal.getAttributes() : getPrincipalAttributesRepository().getAttributes(principal);
        LOGGER.debug("Found principal attributes [{}] for [{}]", principalAttributes, principal.getId());

        LOGGER.debug("Calling attribute policy [{}] to process attributes for [{}]", getClass().getSimpleName(), principal.getId());
        final Map<String, Object> policyAttributes = getAttributesInternal(principal, principalAttributes, registeredService);
        LOGGER.debug("Attribute policy [{}] allows release of [{}] for [{}]", getClass().getSimpleName(), policyAttributes, principal.getId());

        LOGGER.debug("Attempting to merge policy attributes and default attributes");
        final Map<String, Object> attributesToRelease = new TreeMap<>(String.CASE_INSENSITIVE_ORDER);

        if (isExcludeDefaultAttributes()) {
            LOGGER.debug("Ignoring default attribute policy attributes");
        } else {
            LOGGER.debug("Checking default attribute policy attributes");
            final Map<String, Object> defaultAttributes = getReleasedByDefaultAttributes(principal, principalAttributes);
            LOGGER.debug("Default attributes found to be released are [{}]", defaultAttributes);

            LOGGER.debug("Adding default attributes first to the released set of attributes");
            attributesToRelease.putAll(defaultAttributes);
        }
        LOGGER.debug("Adding policy attributes to the released set of attributes");
        attributesToRelease.putAll(policyAttributes);

        insertPrincipalIdAsAttributeIfNeeded(principal, attributesToRelease, selectedService, registeredService);

        if (getAttributeFilter() != null) {
            LOGGER.debug("Invoking attribute filter [{}] on the final set of attributes", getAttributeFilter());
            return getAttributeFilter().filter(attributesToRelease);
        }

        return returnFinalAttributesCollection(attributesToRelease, registeredService);
    }

    /**
     * Release principal id as attribute if needed.
     *
     * @param principal           the principal
     * @param attributesToRelease the attributes to release
     * @param service             the service
     * @param registeredService   the registered service
     */
    protected void insertPrincipalIdAsAttributeIfNeeded(final Principal principal,
                                                        final Map<String, Object> attributesToRelease,
                                                        final Service service,
                                                        final RegisteredService registeredService) {
        if (StringUtils.isNotBlank(getPrincipalIdAttribute())) {
            LOGGER.debug("Attempting to resolve the principal id for service [{}]", registeredService.getServiceId());
            final String id = registeredService.getUsernameAttributeProvider().resolveUsername(principal, service, registeredService);
            LOGGER.debug("Releasing resolved principal id [{}] as attribute [{}]", id, getPrincipalIdAttribute());
            attributesToRelease.put(getPrincipalIdAttribute(), principal.getId());
        }
    }

    /**
     * Return the final attributes collection.
     * Subclasses may override this minute to impose last minute rules.
     *
     * @param attributesToRelease the attributes to release
     * @param service             the service
     * @return the map
     */
    protected Map<String, Object> returnFinalAttributesCollection(final Map<String, Object> attributesToRelease,
                                                                  final RegisteredService service) {
        LOGGER.debug("Final collection of attributes allowed are: [{}]", attributesToRelease);
        return attributesToRelease;
    }

    /**
     * Determines a default bundle of attributes that may be released to all services
     * without the explicit mapping for each service.
     *
     * @param p          the principal
     * @param attributes the attributes
     * @return the released by default attributes
     */
    protected Map<String, Object> getReleasedByDefaultAttributes(final Principal p, final Map<String, Object> attributes) {
        final ApplicationContext ctx = ApplicationContextProvider.getApplicationContext();
        if (ctx != null) {
            LOGGER.debug("Located application context. Retrieving default attributes for release, if any");
            final CasConfigurationProperties props = ctx.getAutowireCapableBeanFactory().getBean(CasConfigurationProperties.class);
            final Set<String> defaultAttrs = props.getAuthn().getAttributeRepository().getDefaultAttributesToRelease();
            LOGGER.debug("Default attributes for release are: [{}]", defaultAttrs);

            final Map<String, Object> defaultAttributesToRelease = new TreeMap<>(String.CASE_INSENSITIVE_ORDER);
            defaultAttrs.stream().forEach(key -> {
                if (attributes.containsKey(key)) {
                    LOGGER.debug("Found and added default attribute for release: [{}]", key);
                    defaultAttributesToRelease.put(key, attributes.get(key));
                }
            });
            return defaultAttributesToRelease;
        }

        return new TreeMap<>();
    }

    /**
     * Gets the attributes internally from the implementation.
     *
     * @param principal  the principal
     * @param attributes the principal attributes
     * @param service    the service
     * @return the attributes allowed for release
     */
    protected abstract Map<String, Object> getAttributesInternal(Principal principal,
                                                                 Map<String, Object> attributes,
                                                                 RegisteredService service);

    @Override
    public int hashCode() {
        return new HashCodeBuilder(13, 133)
                .append(getAttributeFilter())
                .append(isAuthorizedToReleaseCredentialPassword())
                .append(isAuthorizedToReleaseProxyGrantingTicket())
                .append(getPrincipalAttributesRepository())
                .append(isExcludeDefaultAttributes())
                .append(getPrincipalIdAttribute())
                .toHashCode();
    }

    @Override
    public boolean equals(final Object o) {
        if (o == null) {
            return false;
        }

        if (this == o) {
            return true;
        }

        if (!(o instanceof AbstractRegisteredServiceAttributeReleasePolicy)) {
            return false;
        }

        final AbstractRegisteredServiceAttributeReleasePolicy that = (AbstractRegisteredServiceAttributeReleasePolicy) o;
        final EqualsBuilder builder = new EqualsBuilder();
        return builder
                .append(getAttributeFilter(), that.getAttributeFilter())
                .append(isAuthorizedToReleaseCredentialPassword(), that.isAuthorizedToReleaseCredentialPassword())
                .append(isAuthorizedToReleaseProxyGrantingTicket(), that.isAuthorizedToReleaseProxyGrantingTicket())
                .append(getPrincipalAttributesRepository(), that.getPrincipalAttributesRepository())
                .append(isExcludeDefaultAttributes(), that.isExcludeDefaultAttributes())
                .append(getPrincipalIdAttribute(), that.getPrincipalIdAttribute())
                .isEquals();
    }


    @Override
    public String toString() {
        return new ToStringBuilder(this)
                .append("attributeFilter", getAttributeFilter())
                .append("principalAttributesRepository", getPrincipalAttributesRepository())
                .append("authorizedToReleaseCredentialPassword", isAuthorizedToReleaseCredentialPassword())
                .append("authorizedToReleaseProxyGrantingTicket", isAuthorizedToReleaseProxyGrantingTicket())
                .append("excludeDefaultAttributes", isExcludeDefaultAttributes())
                .append("principalIdAttribute", getPrincipalIdAttribute())
                .toString();
    }
}


