package org.apereo.cas.integration.pac4j.authentication.handler.support;

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.AbstractPac4jAuthenticationHandler;
import org.apereo.cas.authentication.principal.ClientCredential;
import org.apereo.cas.authentication.principal.PrincipalFactory;
import org.apereo.cas.services.ServicesManager;
import org.apereo.cas.web.support.WebUtils;
import org.pac4j.core.context.WebContext;
import org.pac4j.core.credentials.Credentials;
import org.pac4j.core.credentials.authenticator.Authenticator;
import org.pac4j.core.profile.UserProfile;
import org.pac4j.core.profile.creator.AuthenticatorProfileCreator;
import org.pac4j.core.profile.creator.ProfileCreator;
import org.pac4j.core.util.CommonHelper;
import org.pac4j.core.util.InitializableObject;
import org.pac4j.core.util.InitializableWebObject;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import javax.security.auth.login.FailedLoginException;
import java.security.GeneralSecurityException;

/**
 * Abstract pac4j authentication handler which uses a pac4j authenticator and profile creator.
 *
 * @author Jerome Leleu
 * @param <I> the type parameter
 * @param <C> the type parameter
 * @since 4.2.0
 */
public abstract class AbstractWrapperAuthenticationHandler<I extends Credential, C extends Credentials>
        extends AbstractPac4jAuthenticationHandler {

    private static final Logger LOGGER = LoggerFactory.getLogger(AbstractWrapperAuthenticationHandler.class);
    
    /**
     * The pac4j profile creator used for authentication.
     */
    protected ProfileCreator profileCreator = AuthenticatorProfileCreator.INSTANCE;

    public AbstractWrapperAuthenticationHandler(final String name, final ServicesManager servicesManager, final PrincipalFactory principalFactory,
                                                final Integer order) {
        super(name, servicesManager, principalFactory, order);
    }

    @Override
    public boolean supports(final Credential credential) {
        return credential != null && getCasCredentialsType().isAssignableFrom(credential.getClass());
    }

    @Override
    protected HandlerResult doAuthentication(final Credential credential) throws GeneralSecurityException, PreventedException {
        CommonHelper.assertNotNull("profileCreator", this.profileCreator);

        final C credentials = convertToPac4jCredentials((I) credential);
        LOGGER.debug("credentials: [{}]", credentials);

        try {
            final Authenticator authenticator = getAuthenticator(credential);
     
            if (authenticator instanceof InitializableObject) {
                ((InitializableObject) authenticator).init();
            }
            if (authenticator instanceof InitializableWebObject) {
                ((InitializableWebObject) authenticator).init(getWebContext());
            }
            
            CommonHelper.assertNotNull("authenticator", authenticator);
            authenticator.validate(credentials, getWebContext());

            final UserProfile profile = this.profileCreator.create(credentials, getWebContext());
            LOGGER.debug("profile: [{}]", profile);

            return createResult(new ClientCredential(credentials), profile);
        } catch (final Exception e) {
            LOGGER.error("Failed to validate credentials", e);
            throw new FailedLoginException("Failed to validate credentials: " + e.getMessage());
        }
    }

    /**
     * Gets the web context from the current thread-bound object.
     *
     * @return the web context
     */
    protected static WebContext getWebContext() {
        return WebUtils.getPac4jJ2EContext(
                        WebUtils.getHttpServletRequestFromRequestAttributes(),
                        WebUtils.getHttpServletResponseFromRequestAttributes());
    }

    /**
     * Convert a CAS credential into a pac4j credentials to play the authentication.
     *
     * @param casCredential the CAS credential
     * @return the pac4j credentials
     * @throws GeneralSecurityException On authentication failure.
     * @throws PreventedException On the indeterminate case when authentication is prevented.
     */
    protected abstract C convertToPac4jCredentials(I casCredential) throws GeneralSecurityException,
            PreventedException;

    /**
     * Return the CAS credential supported by this handler (to be converted in a pac4j credentials
     * by {@link #convertToPac4jCredentials(Credential)}).
     *
     * @return the CAS credential class
     */
    protected abstract Class<I> getCasCredentialsType();

    /**
     * Gets authenticator.
     *
     * @param credential the credential
     * @return the authenticator
     */
    protected abstract Authenticator getAuthenticator(Credential credential);

    public ProfileCreator getProfileCreator() {
        return this.profileCreator;
    }

    public void setProfileCreator(final ProfileCreator profileCreator) {
        this.profileCreator = profileCreator;
    }
}
