package org.apereo.cas.adaptors.duo.web.flow.config;

import org.apereo.cas.adaptors.duo.authn.DuoCredential;
import org.apereo.cas.configuration.model.support.mfa.MultifactorAuthenticationProperties;
import org.apereo.cas.services.MultifactorAuthenticationProvider;
import org.apereo.cas.services.VariegatedMultifactorAuthenticationProvider;
import org.apereo.cas.web.flow.AbstractMultifactorTrustedDeviceWebflowConfigurer;
import org.apereo.cas.web.flow.CasWebflowConstants;
import org.apereo.cas.web.flow.DynamicFlowModelBuilder;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.config.ConfigurableListableBeanFactory;
import org.springframework.webflow.config.FlowDefinitionRegistryBuilder;
import org.springframework.webflow.definition.registry.FlowDefinitionRegistry;
import org.springframework.webflow.engine.builder.FlowBuilder;
import org.springframework.webflow.engine.builder.model.FlowModelFlowBuilder;
import org.springframework.webflow.engine.builder.support.FlowBuilderServices;
import org.springframework.webflow.engine.model.AbstractActionModel;
import org.springframework.webflow.engine.model.AbstractStateModel;
import org.springframework.webflow.engine.model.ActionStateModel;
import org.springframework.webflow.engine.model.BinderModel;
import org.springframework.webflow.engine.model.BindingModel;
import org.springframework.webflow.engine.model.EndStateModel;
import org.springframework.webflow.engine.model.EvaluateModel;
import org.springframework.webflow.engine.model.TransitionModel;
import org.springframework.webflow.engine.model.VarModel;
import org.springframework.webflow.engine.model.ViewStateModel;
import org.springframework.webflow.engine.model.builder.DefaultFlowModelHolder;
import org.springframework.webflow.engine.model.registry.FlowModelHolder;

import java.util.LinkedList;

/**
 * This is {@link DuoMultifactorWebflowConfigurer}.
 *
 * @author Misagh Moayyed
 * @since 5.0.0
 */
public class DuoMultifactorWebflowConfigurer extends AbstractMultifactorTrustedDeviceWebflowConfigurer {
    private static final Logger LOGGER = LoggerFactory.getLogger(DuoMultifactorWebflowConfigurer.class);
    private static final String STATE_ID_VIEW_LOGIN_FORM_DUO = "viewLoginFormDuo";

    private final VariegatedMultifactorAuthenticationProvider provider;

    public DuoMultifactorWebflowConfigurer(final FlowBuilderServices flowBuilderServices, final FlowDefinitionRegistry loginFlowDefinitionRegistry,
                                           final boolean enableDeviceRegistration, final VariegatedMultifactorAuthenticationProvider provider) {
        super(flowBuilderServices, loginFlowDefinitionRegistry, enableDeviceRegistration);
        this.provider = provider;
    }

    @Override
    protected void doInitialize() throws Exception {
        provider.getProviders().forEach(p -> {
            final FlowDefinitionRegistry duoFlowRegistry = buildDuoFlowRegistry(p);
            applicationContext.getAutowireCapableBeanFactory().initializeBean(duoFlowRegistry, p.getId());
            final ConfigurableListableBeanFactory cfg = (ConfigurableListableBeanFactory) applicationContext.getAutowireCapableBeanFactory();
            cfg.registerSingleton(p.getId(), duoFlowRegistry);
            registerMultifactorProviderAuthenticationWebflow(getLoginFlow(), p.getId(), duoFlowRegistry);
        });

        casProperties.getAuthn().getMfa().getDuo()
                .stream()
                .filter(MultifactorAuthenticationProperties.Duo::isTrustedDeviceEnabled)
                .forEach(duo -> {
                    final String id = duo.getId();
                    try {
                        LOGGER.debug("Activating multifactor trusted authentication for webflow [{}]", id);
                        final FlowDefinitionRegistry registry = applicationContext.getBean(id, FlowDefinitionRegistry.class);
                        registerMultifactorTrustedAuthentication(registry);
                    } catch (final Exception e) {
                        LOGGER.error("Failed to register multifactor trusted authentication for " + id, e);
                    }
                });
    }

    private FlowDefinitionRegistry buildDuoFlowRegistry(final MultifactorAuthenticationProvider p) {
        final DynamicFlowModelBuilder modelBuilder = new DynamicFlowModelBuilder();

        // vars
        final LinkedList<VarModel> vars = new LinkedList<>();
        vars.add(new VarModel(CasWebflowConstants.VAR_ID_CREDENTIAL, DuoCredential.class.getName()));
        modelBuilder.setVars(vars);

        // starts
        final LinkedList<AbstractActionModel> starts = new LinkedList<>();
        starts.add(new EvaluateModel("initialFlowSetupAction"));
        modelBuilder.setOnStartActions(starts);

        // states
        final LinkedList<AbstractStateModel> states = new LinkedList<>();

        ActionStateModel actModel = new ActionStateModel(CasWebflowConstants.STATE_ID_INIT_LOGIN_FORM);
        LinkedList<AbstractActionModel> actions = new LinkedList<>();
        actions.add(new EvaluateModel("initializeLoginAction"));
        actModel.setActions(actions);

        LinkedList<TransitionModel> trans = new LinkedList<>();
        TransitionModel transModel = new TransitionModel();
        transModel.setOn(CasWebflowConstants.TRANSITION_ID_SUCCESS);
        transModel.setTo("determineDuoRequest");
        trans.add(transModel);

        actModel.setTransitions(trans);
        states.add(actModel);

        ///////////////

        actModel = new ActionStateModel("determineDuoRequest");
        actions = new LinkedList<>();
        actions.add(new EvaluateModel("checkWebAuthenticationRequestAction"));
        actModel.setActions(actions);

        trans = new LinkedList<>();

        transModel = new TransitionModel();
        transModel.setOn(CasWebflowConstants.TRANSITION_ID_YES);
        transModel.setTo(STATE_ID_VIEW_LOGIN_FORM_DUO);
        trans.add(transModel);

        transModel = new TransitionModel();
        transModel.setOn(CasWebflowConstants.TRANSITION_ID_NO);
        transModel.setTo("doNonWebAuthentication");
        trans.add(transModel);

        actModel.setTransitions(trans);
        states.add(actModel);

        ///////////////

        actModel = new ActionStateModel("doNonWebAuthentication");
        actions = new LinkedList<>();
        actions.add(new EvaluateModel("duoNonWebAuthenticationAction"));
        actModel.setActions(actions);

        trans = new LinkedList<>();

        transModel = new TransitionModel();
        transModel.setOn(CasWebflowConstants.TRANSITION_ID_SUCCESS);
        transModel.setTo("finalizeAuthentication");
        trans.add(transModel);

        actModel.setTransitions(trans);
        states.add(actModel);

        ///////////////

        actModel = new ActionStateModel("finalizeAuthentication");
        actions = new LinkedList<>();
        actions.add(new EvaluateModel("duoAuthenticationWebflowAction"));
        actModel.setActions(actions);

        trans = new LinkedList<>();

        transModel = new TransitionModel();
        transModel.setOn(CasWebflowConstants.TRANSITION_ID_SUCCESS);
        transModel.setTo(CasWebflowConstants.TRANSITION_ID_SUCCESS);
        trans.add(transModel);

        actModel.setTransitions(trans);
        states.add(actModel);

        /////////////////

        final ViewStateModel viewState = new ViewStateModel(STATE_ID_VIEW_LOGIN_FORM_DUO);
        viewState.setView("casDuoLoginView");
        viewState.setModel(CasWebflowConstants.VAR_ID_CREDENTIAL);
        final BinderModel bm = new BinderModel();
        final LinkedList<BindingModel> bindings = new LinkedList<>();
        final BindingModel bme = new BindingModel("signedDuoResponse", null, null);
        bindings.add(bme);
        bm.setBindings(bindings);
        viewState.setBinder(bm);

        actions = new LinkedList<>();
        actions.add(new EvaluateModel("prepareDuoWebLoginFormAction"));
        viewState.setOnEntryActions(actions);

        transModel = new TransitionModel();
        transModel.setOn(CasWebflowConstants.TRANSITION_ID_SUBMIT);
        transModel.setTo(CasWebflowConstants.TRANSITION_ID_REAL_SUBMIT);
        transModel.setBind(Boolean.TRUE.toString());
        transModel.setValidate(Boolean.FALSE.toString());

        trans.add(transModel);
        viewState.setTransitions(trans);
        states.add(viewState);

        /////////////////

        actModel = new ActionStateModel(CasWebflowConstants.TRANSITION_ID_REAL_SUBMIT);
        actions = new LinkedList<>();
        actions.add(new EvaluateModel("duoAuthenticationWebflowAction"));
        actModel.setActions(actions);

        trans = new LinkedList<>();

        transModel = new TransitionModel();
        transModel.setOn(CasWebflowConstants.TRANSITION_ID_SUCCESS);
        transModel.setTo(CasWebflowConstants.TRANSITION_ID_SUCCESS);
        trans.add(transModel);

        transModel = new TransitionModel();
        transModel.setOn(CasWebflowConstants.TRANSITION_ID_ERROR);
        transModel.setTo(CasWebflowConstants.STATE_ID_INIT_LOGIN_FORM);
        trans.add(transModel);

        actModel.setTransitions(trans);
        states.add(actModel);

        ////////////////////

        states.add(new EndStateModel(CasWebflowConstants.TRANSITION_ID_SUCCESS));

        ////////////////////

        modelBuilder.setStates(states);

        final FlowModelHolder holder = new DefaultFlowModelHolder(modelBuilder);
        final FlowBuilder flowBuilder = new FlowModelFlowBuilder(holder);
        final FlowDefinitionRegistryBuilder builder = new FlowDefinitionRegistryBuilder(this.applicationContext, flowBuilderServices);
        builder.addFlowBuilder(flowBuilder, p.getId());
        return builder.build();
    }
}
