package org.apereo.cas.web.report;

import org.apache.commons.lang3.StringUtils;
import org.apereo.cas.configuration.CasConfigurationProperties;
import org.springframework.binding.expression.Expression;
import org.springframework.http.MediaType;
import org.springframework.util.ReflectionUtils;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.ResponseBody;
import org.springframework.webflow.definition.registry.FlowDefinitionRegistry;
import org.springframework.webflow.engine.EndState;
import org.springframework.webflow.engine.Flow;
import org.springframework.webflow.engine.FlowVariable;
import org.springframework.webflow.engine.State;
import org.springframework.webflow.engine.TransitionableState;
import org.springframework.webflow.engine.ViewState;

import java.lang.reflect.Field;
import java.util.Arrays;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.stream.Collectors;
import java.util.stream.StreamSupport;

/**
 * This is {@link SpringWebflowReportController}.
 *
 * @author Misagh Moayyed
 * @since 5.1.0
 */
public class SpringWebflowReportController extends BaseCasMvcEndpoint {
    /**
     * Instantiates a new Base cas mvc endpoint.
     *
     * @param casProperties the cas properties
     */
    public SpringWebflowReportController(final CasConfigurationProperties casProperties) {
        super("swfReport", "/swf", casProperties.getMonitor().getEndpoints().getSpringWebflowReport(), casProperties);
    }

    /**
     * Get SWF report.
     *
     * @return JSON representing the current state of SWF.
     */
    @GetMapping(produces = MediaType.APPLICATION_JSON_VALUE)
    @ResponseBody
    public Map<?, ?> getReport() {
        final Map<String, Object> jsonMap = new HashMap<>();
        final Map<String, FlowDefinitionRegistry> map =
                this.applicationContext.getBeansOfType(FlowDefinitionRegistry.class, false, true);

        map.forEach((k, v) -> {
            Arrays.stream(v.getFlowDefinitionIds()).forEach(id -> {
                final Map<String, Object> flowDetails = new HashMap<>();
                final Flow def = Flow.class.cast(v.getFlowDefinition(id));

                final Map<String, Map> states = new HashMap<>();
                Arrays.stream(def.getStateIds()).forEach(st -> {

                    final State state = (State) def.getState(st);
                    final Map<String, Object> stateMap = new HashMap<>();

                    if (!state.getAttributes().asMap().isEmpty()) {
                        stateMap.put("attributes", state.getAttributes().asMap());
                    }
                    if (StringUtils.isNotBlank(state.getCaption())) {
                        stateMap.put("caption", state.getCaption());
                    }

                    List acts = StreamSupport.stream(state.getEntryActionList().spliterator(), false)
                            .map(Object::toString)
                            .collect(Collectors.toList());

                    if (!acts.isEmpty()) {
                        stateMap.put("entryActions", acts);
                    }

                    if (state instanceof EndState) {
                        stateMap.put("isEndState", Boolean.TRUE);
                    }
                    if (state.isViewState()) {
                        stateMap.put("isViewState", state.isViewState());
                        stateMap.put("isRedirect", ((ViewState) state).getRedirect());
                       
                        acts = StreamSupport.stream(state.getEntryActionList().spliterator(), false)
                                .map(Object::toString)
                                .collect(Collectors.toList());

                        if (!acts.isEmpty()) {
                            stateMap.put("renderActions", ((ViewState) state).getRenderActionList());
                        }

                        acts = Arrays.stream(((ViewState) state).getVariables())
                                .map(value -> value.getName() + " -> " + value.getValueFactory().toString())
                                .collect(Collectors.toList());

                        if (!acts.isEmpty()) {
                            stateMap.put("viewVariables", acts);
                        }

                        final Field field = ReflectionUtils.findField(((ViewState) state).getViewFactory().getClass(), "viewId");
                        ReflectionUtils.makeAccessible(field);
                        final Expression exp = (Expression) ReflectionUtils.getField(field, ((ViewState) state).getViewFactory());
                        stateMap.put("viewId", StringUtils.defaultIfBlank(exp.getExpressionString(), exp.getValue(null).toString()));

                    }

                    if (state instanceof TransitionableState) {
                        final TransitionableState stDef = TransitionableState.class.cast(state);

                        acts = StreamSupport.stream(stDef.getExitActionList().spliterator(), false)
                                .map(Object::toString)
                                .collect(Collectors.toList());

                        if (!acts.isEmpty()) {
                            stateMap.put("exitActions", acts);
                        }

                        acts = Arrays.stream(stDef.getTransitions())
                                .map(tr -> tr.getId() + " -> " + tr.getTargetStateId())
                                .collect(Collectors.toList());

                        if (!acts.isEmpty()) {
                            stateMap.put("transitions", acts);
                        }
                    }

                    states.put(st, stateMap);
                });
                flowDetails.put("states", states);
                flowDetails.put("startState", def.getStartState().getId());
                flowDetails.put("possibleOutcomes", def.getPossibleOutcomes());
                flowDetails.put("stateCount", def.getStateCount());


                List acts = StreamSupport.stream(def.getEndActionList().spliterator(), false)
                        .map(Object::toString)
                        .collect(Collectors.toList());
                if (!acts.isEmpty()) {
                    flowDetails.put("endActions", acts);
                }

                acts = StreamSupport.stream(def.getGlobalTransitionSet().spliterator(), false)
                        .map(tr -> tr.getId() + " -> " + tr.getTargetStateId() + " @ " + tr.getExecutionCriteria().toString())
                        .collect(Collectors.toList());
                if (!acts.isEmpty()) {
                    flowDetails.put("globalTransitions", acts);
                }

                acts = Arrays.stream(def.getExceptionHandlerSet().toArray())
                        .map(Object::toString)
                        .collect(Collectors.toList());
                if (!acts.isEmpty()) {
                    flowDetails.put("exceptionHandlers", acts);
                }

                final String vars = Arrays.stream(def.getVariables())
                        .map(FlowVariable::getName)
                        .collect(Collectors.joining(","));

                if (StringUtils.isNotBlank(vars)) {
                    flowDetails.put("variables", vars);
                }

                jsonMap.put(id, flowDetails);
            });
        });

        return jsonMap;
    }
}
