package ru.yandex.webmaster3.core.http;

import java.util.ArrayList;
import java.util.Collections;
import java.util.HashMap;
import java.util.HashSet;
import java.util.LinkedList;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.stream.Collectors;

import lombok.Getter;
import org.apache.commons.lang3.StringUtils;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.BeansException;
import org.springframework.beans.factory.annotation.Required;
import org.springframework.context.ApplicationContext;
import org.springframework.context.ApplicationContextAware;

import ru.yandex.webmaster3.core.http.internal.ActionReflectionUtils;
import ru.yandex.webmaster3.core.http.util.RequestFilterHelper;

/**
 * @author aherman
 */
public class ActionsHolder implements ApplicationContextAware {
    private static final Logger log = LoggerFactory.getLogger(ActionsHolder.class);

    private boolean assertReadWriteAnnotationsPresent;

    @Getter
    Map<String, Action> actions = Collections.emptyMap();
    Map<String, List<String>> prefixes = Collections.emptyMap();
    List<RequestFilter> requestFilters;
    Map<Class<? extends ActionRequest>, List<RequestFilter>> reqClass2reqFilters = new HashMap<>();
    Map<Class<? extends ResponseFilter>, ResponseFilter> registeredResponseFilters = Collections.emptyMap();

    private ApplicationContext applicationContext;

    public void init() {
        RequestFilterHelper<RequestFilter> requestFilterHelper = new RequestFilterHelper<>(
                Collections.singleton(ActionRequest.class),
                requestFilters,
                ActionReflectionUtils::getRequestFilterRequestType
        );
        requestFilterHelper.verifyFilterDepsTree();

        actions = applicationContext.getBeansOfType(Action.class);
        log.info("Actions found: {}", actions.size());

        prefixes = new HashMap<>();

        List<String> allActionNames = new ArrayList<>(actions.keySet());
        Collections.sort(allActionNames);
        prefixes.put("/", allActionNames);

        for (String actionName : allActionNames) {
            String[] parts = StringUtils.split(actionName, '/');

            if (parts.length <= 1) {
                continue;
            }

            StringBuilder sb = new StringBuilder();
            sb.append('/');
            for (int i = 0; i < parts.length - 1; i++) {
                sb.append(parts[i]).append('/');
                String actionPrefix = sb.toString();
                List<String> actionsList = prefixes.get(actionPrefix);
                if (actionsList == null) {
                    actionsList = new LinkedList<>();
                    prefixes.put(actionPrefix, actionsList);
                }
                actionsList.add(actionName);
            }
        }

        log.info("Prefixes found: {}", prefixes.size());

        Set<Class<? extends ResponseFilter>> allResponseFilters = new HashSet<>();
        for (Map.Entry<String, Action> actionEntry : actions.entrySet()) {
            Action<?, ?> action = actionEntry.getValue();
            Class<? extends Action> actionClass = action.getClass();
            if (assertReadWriteAnnotationsPresent) {
                if (ActionReflectionUtils.getActionEffect(actionClass) == null) {
                    throw new RuntimeException("Every action must be annotated with exactly one of ReadAction or WriteAction. " +
                            "Not annotated action found: " + actionClass.getName());
                }
            }

            Class<? extends ActionRequest> reqClass = ActionReflectionUtils.getActionParameters(actionClass).requestClass;
            reqClass2reqFilters.put(
                    reqClass,
                    requestFilterHelper.getFiltersForRequest(reqClass)
                            .stream()
                            .filter(f -> f.isApplicable(action))
                            .collect(Collectors.toList())
            );

            List<Class<? extends ResponseFilter>> actionResponseFilters = action.getResponseFilters();
            for (Class<? extends ResponseFilter> filterClass : actionResponseFilters) {
                if (!ResponseFilter.class.isAssignableFrom(filterClass)) {
                    throw new RuntimeException("Unsupported response filter class: action=" + actionEntry.getKey()
                            + " actionClass=" + actionClass.getName()
                            + " filter=" + filterClass.getName());
                }
            }
            allResponseFilters.addAll(actionResponseFilters);
        }

        Map<Class<? extends ResponseFilter>, ResponseFilter> responseFilters = new HashMap<>();
        for (Class<? extends ResponseFilter> filterClass : allResponseFilters) {
            Map<String, ? extends ResponseFilter> beans = applicationContext.getBeansOfType(filterClass);
            if (beans.isEmpty()) {
                throw new RuntimeException("Unable to find response filter: filter=" + filterClass.getName());
            }
            if (beans.size() > 1) {
                log.warn("Found several response filters: filter={}", filterClass.getName());
                for (Map.Entry<String, ? extends ResponseFilter> entry : beans.entrySet()) {
                    log.warn("Candidate response filter: name={}, class={}", entry.getKey(), entry.getValue().getClass().getName());
                }
            }
            ResponseFilter filter = beans.values().iterator().next();
            log.info("Resolved response filter: filter={} actual={}", filterClass.getName(), filter.getClass().getName());
            responseFilters.put(filterClass, filter);
        }
        registeredResponseFilters = Collections.unmodifiableMap(responseFilters);
    }

    @Override
    public void setApplicationContext(ApplicationContext applicationContext) throws BeansException {
        this.applicationContext = applicationContext;
    }

    @Required
    public void setAssertReadWriteAnnotationsPresent(boolean assertReadWriteAnnotationsPresent) {
        this.assertReadWriteAnnotationsPresent = assertReadWriteAnnotationsPresent;
    }

    @Required
    public void setRequestFilters(List<RequestFilter> requestFilters) {
        this.requestFilters = requestFilters;
    }
}
