package org.apereo.cas.adaptors.duo.authn;

import org.apereo.cas.authentication.Credential;
import org.apereo.cas.authentication.HandlerResult;
import org.apereo.cas.authentication.PreventedException;
import org.apereo.cas.authentication.handler.support.AbstractPreAndPostProcessingAuthenticationHandler;
import org.apereo.cas.authentication.principal.Principal;
import org.apereo.cas.authentication.principal.PrincipalFactory;
import org.apereo.cas.services.MultifactorAuthenticationProvider;
import org.apereo.cas.services.ServicesManager;
import org.apereo.cas.services.VariegatedMultifactorAuthenticationProvider;
import org.apereo.cas.web.support.WebUtils;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.webflow.execution.RequestContext;
import org.springframework.webflow.execution.RequestContextHolder;

import javax.security.auth.login.FailedLoginException;
import java.security.GeneralSecurityException;
import java.util.ArrayList;
import java.util.Collection;

/**
 * Authenticate CAS credentials against Duo Security.
 *
 * @author Misagh Moayyed
 * @author Dmitriy Kopylenko
 * @since 4.2
 */
public class DuoAuthenticationHandler extends AbstractPreAndPostProcessingAuthenticationHandler {

    private static final Logger LOGGER = LoggerFactory.getLogger(DuoAuthenticationHandler.class);
    
    private final VariegatedMultifactorAuthenticationProvider provider;

    public DuoAuthenticationHandler(final String name, final ServicesManager servicesManager, final PrincipalFactory principalFactory,
                                    final VariegatedMultifactorAuthenticationProvider provider) {
        super(name, servicesManager, principalFactory, null);
        this.provider = provider;
    }

    /**
     * Do an out of band request using the DuoWeb api (encapsulated in DuoAuthenticationService)
     * to the hosted duo service. If it is successful
     * it will return a String containing the username of the successfully authenticated user, but if not - will
     * return a blank String or null.
     *
     * @param credential Credential to authenticate.
     * @return the result of this handler
     * @throws GeneralSecurityException general security exception for errors
     * @throws PreventedException       authentication failed exception
     */
    @Override
    protected HandlerResult doAuthentication(final Credential credential) throws GeneralSecurityException, PreventedException {
        if (credential instanceof DuoDirectCredential) {
            LOGGER.debug("Attempting to directly authenticate credential against Duo");
            return authenticateDuoApiCredential(credential);
        }
        return authenticateDuoCredential(credential);
    }

    private HandlerResult authenticateDuoApiCredential(final Credential credential) throws FailedLoginException {
        try {
            final DuoAuthenticationService duoAuthenticationService = getDuoAuthenticationService();
            final DuoDirectCredential c = DuoDirectCredential.class.cast(credential);
            if (duoAuthenticationService.authenticate(c).getKey()) {
                final Principal principal = c.getAuthentication().getPrincipal();
                LOGGER.debug("Duo has successfully authenticated [{}]", principal.getId());
                return createHandlerResult(credential, principal, new ArrayList<>());
            }
        } catch (final Exception e) {
            LOGGER.error(e.getMessage(), e);
        }
        throw new FailedLoginException("Duo authentication has failed");
    }

    private HandlerResult authenticateDuoCredential(final Credential credential) throws FailedLoginException {
        try {
            final DuoCredential duoCredential = (DuoCredential) credential;
            if (!duoCredential.isValid()) {
                throw new GeneralSecurityException("Duo credential validation failed. Ensure a username "
                        + " and the signed Duo response is configured and passed. Credential received: " + duoCredential);
            }

            final DuoAuthenticationService duoAuthenticationService = getDuoAuthenticationService();
            final String duoVerifyResponse = duoAuthenticationService.authenticate(duoCredential).getValue();
            LOGGER.debug("Response from Duo verify: [{}]", duoVerifyResponse);
            final String primaryCredentialsUsername = duoCredential.getUsername();

            final boolean isGoodAuthentication = duoVerifyResponse.equals(primaryCredentialsUsername);

            if (isGoodAuthentication) {
                LOGGER.info("Successful Duo authentication for [{}]", primaryCredentialsUsername);

                final Principal principal = this.principalFactory.createPrincipal(duoVerifyResponse);
                return createHandlerResult(credential, principal, new ArrayList<>());
            }
            throw new FailedLoginException("Duo authentication username "
                    + primaryCredentialsUsername + " does not match Duo response: " + duoVerifyResponse);

        } catch (final Exception e) {
            LOGGER.error(e.getMessage(), e);
            throw new FailedLoginException(e.getMessage());
        }
    }

    private DuoAuthenticationService getDuoAuthenticationService() {
        final RequestContext requestContext = RequestContextHolder.getRequestContext();
        if (requestContext == null) {
            throw new IllegalArgumentException("No request context is held to locate the Duo authentication service");
        }
        final Collection<MultifactorAuthenticationProvider> col = WebUtils.getResolvedMultifactorAuthenticationProviders(requestContext);
        if (col.isEmpty()) {
            throw new IllegalArgumentException("No multifactor providers are found in the current request context");
        }
        final MultifactorAuthenticationProvider pr = col.iterator().next();
        return provider.findProvider(pr.getId(), DuoMultifactorAuthenticationProvider.class).getDuoAuthenticationService();
    }

    @Override
    public boolean supports(final Credential credential) {
        return DuoCredential.class.isAssignableFrom(credential.getClass())
                || credential instanceof DuoDirectCredential;
    }
}
