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.apereo.cas.authentication.Authentication;
import org.apereo.cas.authentication.AuthenticationException;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.webflow.execution.Event;

import java.io.Serializable;

/**
 * The {@link AbstractMultifactorAuthenticationProvider} is responsible for
 * as the parent of all providers.
 *
 * @author Misagh Moayyed
 * @since 4.3
 */
public abstract class AbstractMultifactorAuthenticationProvider implements MultifactorAuthenticationProvider, Serializable {

    private static final long serialVersionUID = 4789727148134156909L;

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

    private MultifactorAuthenticationProviderBypass bypassEvaluator;

    private String globalFailureMode;

    private String id;

    private int order;

    @Override
    public String getId() {
        return id;
    }

    @Override
    public int getOrder() {
        return this.order;
    }

    public void setId(final String id) {
        this.id = id;
    }

    public void setOrder(final int order) {
        this.order = order;
    }

    public void setGlobalFailureMode(final String globalFailureMode) {
        this.globalFailureMode = globalFailureMode;
    }

    @Override
    public final boolean supports(final Event e, final Authentication authentication, final RegisteredService registeredService) {
        if (e == null || !e.getId().matches(getId())) {
            LOGGER.debug("Provided event id [{}] is not applicable to this provider identified by [{}]", e.getId(), getId());
            return false;
        }
        if (bypassEvaluator != null && !bypassEvaluator.isAuthenticationRequestHonored(authentication, registeredService, this)) {
            LOGGER.debug("Request cannot be supported by provider [{}] as it's configured for bypass", getId());
            return false;
        }

        if (supportsInternal(e, authentication, registeredService)) {
            LOGGER.debug("[{}] voted to support this authentication request", getClass().getSimpleName());
            return true;
        }

        LOGGER.debug("[{}] voted does not support this authentication request", getClass().getSimpleName());
        return false;
    }

    /**
     * Determine internally if provider is able to support this authentication request
     * for multifactor, and account for bypass rules..
     *
     * @param e                 the event
     * @param authentication    the authentication
     * @param registeredService the registered service
     * @return the boolean
     */
    protected boolean supportsInternal(final Event e, final Authentication authentication, final RegisteredService registeredService) {
        return true;
    }

    @Override
    public boolean isAvailable(final RegisteredService service) throws AuthenticationException {
        RegisteredServiceMultifactorPolicy.FailureModes failureMode = RegisteredServiceMultifactorPolicy.FailureModes.CLOSED;
        final RegisteredServiceMultifactorPolicy policy = service.getMultifactorPolicy();
        if (policy != null) {
            failureMode = policy.getFailureMode();
            LOGGER.debug("Multifactor failure mode for [{}] is defined as [{}]", service.getServiceId(), failureMode);
        } else if (StringUtils.isNotBlank(this.globalFailureMode)) {
            failureMode = RegisteredServiceMultifactorPolicy.FailureModes.valueOf(this.globalFailureMode);
            LOGGER.debug("Using global multifactor failure mode for [{}] defined as [{}]", service.getServiceId(), failureMode);
        }

        if (failureMode != RegisteredServiceMultifactorPolicy.FailureModes.NONE) {
            if (isAvailable()) {
                return true;
            }
            if (failureMode == RegisteredServiceMultifactorPolicy.FailureModes.CLOSED) {
                LOGGER.warn("[{}] could not be reached. Authentication shall fail for [{}]",
                        getClass().getSimpleName(), service.getServiceId());
                throw new AuthenticationException();
            }

            LOGGER.warn("[{}] could not be reached. Since the authentication provider is configured for the "
                            + "failure mode of [{}] authentication will proceed without [{}] for service [{}]",
                    getClass().getSimpleName(), failureMode, getClass().getSimpleName(), service.getServiceId());
            return false;
        }
        LOGGER.debug("Failure mode is set to [{}]. Assuming the provider is available.", failureMode);
        return true;
    }

    /**
     * Is provider available?
     *
     * @return the true/false
     */
    protected boolean isAvailable() {
        return true;
    }

    public void setBypassEvaluator(final MultifactorAuthenticationProviderBypass bypassEvaluator) {
        this.bypassEvaluator = bypassEvaluator;
    }

    @Override
    public boolean equals(final Object obj) {
        if (obj == null) {
            return false;
        }
        if (obj == this) {
            return true;
        }
        if (obj.getClass() != getClass()) {
            return false;
        }
        final MultifactorAuthenticationProvider rhs = (MultifactorAuthenticationProvider) obj;
        return new EqualsBuilder()
                .append(this.getOrder(), rhs.getOrder())
                .append(this.getId(), rhs.getId())
                .isEquals();
    }

    @Override
    public int hashCode() {
        return new HashCodeBuilder()
                .append(getOrder())
                .append(getId())
                .toHashCode();
    }

    @Override
    public String toString() {
        return getClass().getSimpleName();
    }

    @Override
    public boolean matches(final String identifier) {
        return getId().matches(identifier);
    }
}
