package com.jillesvangurp.springdepend;

import com.google.gson.Gson;
import com.google.gson.GsonBuilder;
import com.jillesvangurp.springdepend.json.BeanDependency;
import com.jillesvangurp.springdepend.json.BeanDependencyStatistic;
import org.apache.commons.lang3.StringUtils;
import org.springframework.beans.factory.config.ConfigurableListableBeanFactory;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.Import;
import org.springframework.context.support.GenericApplicationContext;

import java.lang.annotation.Annotation;
import java.lang.reflect.Method;
import java.util.*;

import static java.util.stream.Collectors.toMap;

/**
 * Spring dependency analyzer that works with any GenericApplicationContext.
 */
public class SpringDependencyAnalyzer {
    private final GenericApplicationContext context;

    /**
     * @param context create your spring context the usual way and inject it here.
     */
    public SpringDependencyAnalyzer(GenericApplicationContext context) {
        this.context = context;
    }

    /**
     * Long lists of dependencies indicate low cohesiveness and high coupling. This helps you identify the problematic beans.
     *
     * @return map of dependencies for all beans in the context
     */
    public Map<String, Set<String>> getBeanDependencies() {
        Map<String, Set<String>> beanDeps = new TreeMap<>();
        ConfigurableListableBeanFactory factory = context.getBeanFactory();
        for (String beanName : factory.getBeanDefinitionNames()) {
            if (factory.getBeanDefinition(beanName).isAbstract()) {
                continue;
            }
            String[] dependenciesForBean = factory.getDependenciesForBean(beanName);
            Set<String> set = beanDeps.get(beanName);
            if (set == null) {
                set = new TreeSet<>();
                beanDeps.put(beanName, set);
            }
            for (String dependency : dependenciesForBean) {
                set.add(dependency);
            }
        }
        return beanDeps;
    }

    /**
     * If you have a lot of beans that are not depended on or only once, maybe they shouldn't be a bean at all.
     *
     * @return map of reverse dependencies for all beans in the context
     */
    public Map<String, Set<String>> getReverseBeanDependencies() {
        Map<String, Set<String>> reverseBeanDeps = new TreeMap<>();
        Map<String, Set<String>> beanDeps = getBeanDependencies();

        beanDeps.forEach((beanName, deps) -> {
            for (String dep : deps) {
                Set<String> set = reverseBeanDeps.get(dep);
                if (set == null) {
                    set = new TreeSet<>();
                    reverseBeanDeps.put(dep, set);
                }
                set.add(beanName);
            }
        });

        return reverseBeanDeps;
    }

    /**
     * Organizes the graph of configuration classes in layers that depend on each other.
     * Classes in the same layer can only import classes in lower layers. Spring does not allow import cycles.
     * A good pattern is to have a RootConfig for your application that simply imports everything else you need.
     * The more layers you have the more complex your dependencies.
     *
     * @param configurationClass the root configuration class that you want to analyze
     * @return treemap with layers of configuratino
     */
    public Map<Integer, Set<Class<?>>> getConfigurationLayers(Class<?> configurationClass) {

        SimpleGraph<Class<?>> rootGraph = getConfigurationGraph(configurationClass);
        return rootGraph.getLayers();
    }

    private void validateIsConfigurationClass(Class<?> configurationClass) {
        boolean isConfigClass = false;
        for (Annotation annotation : configurationClass.getAnnotations()) {
            Class<? extends Annotation> type = annotation.annotationType();
            if (Configuration.class.equals(type)) {
                isConfigClass = true;
            }
        }
        if (!isConfigClass) {
            throw new IllegalArgumentException("not a spring configuration class");
        }
    }

    /**
     * @param configurationClass spring configuration root class from which to calculate the configuration hierarchy
     * @return a graph of the configuration classes
     */
    public SimpleGraph<Class<?>> getConfigurationGraph(Class<?> configurationClass) {
        validateIsConfigurationClass(configurationClass);
        return SimpleGraph.treeBuilder(configurationClass, SpringDependencyAnalyzer::getConfigurationImportsFor);
    }

    public SimpleGraph<String> getBeanGraph() {
        Map<String, Set<String>> beanDeps = getBeanDependencies();
        Map<String, Set<String>> reverseBeanDeps = getReverseBeanDependencies();

        SimpleGraph<String> graph = new SimpleGraph<>();

        beanDeps.forEach((bean, deps) -> {
            if (deps.isEmpty()) {
                // bean has no deps, so we can figure out everything that depends on this bean here
                SimpleGraph<String> depGraph = new SimpleGraph<>();
                Set<String> simpleGraphs = new HashSet<>();
                SimpleGraph.buildGraph(depGraph, bean, b -> reverseBeanDeps.get(b), simpleGraphs);
                graph.put(bean, depGraph);
            }
        });
        // FIXME technically this is a reverse dependency graph, We need to revert it.
        return graph;
    }

    private static List<Class<?>> getConfigurationImportsFor(Class<?> clazz) {
        List<Class<?>> list = new ArrayList<>();
        for (Annotation annotation : clazz.getAnnotations()) {
            Class<? extends Annotation> type = annotation.annotationType();
            if (Import.class.equals(type)) {
                try {
                    Method method = type.getMethod("value");
                    Class<?>[] imports = (Class<?>[]) method.invoke(annotation, (Object[]) null);
                    if (imports != null && imports.length > 0) {
                        for (Class<?> c : imports) {
                            list.add(c);
                        }
                    }
                } catch (Throwable e) {
                    throw new IllegalStateException(e);
                }
            }
        }
        return list;
    }

    public String configurationGraphCypher(Class<?> rootClass) {
        return getConfigurationGraph(rootClass).toCypher("ConfigClass", "Imports", c -> c.getSimpleName());
    }

    public String beanGraphCypher() {
        return getBeanGraph().toCypher("Bean", "DEPENDSON", s -> s.replace(".", "_").replace("-", "__"));
    }

    public void printReport(Class<?> springConfigurationClass) {
        System.err.println("Configuration layers:\n");
        getConfigurationLayers(springConfigurationClass).forEach((layer, classes) -> {
            System.err.println("" + layer + "\t" + StringUtils.join(classes, ','));
        });

        System.err.println("\n\nDependencies:\n");
        Map<String, Set<String>> beanDependencies = getBeanDependencies();
        beanDependencies.forEach((name, dependencies) -> {
            System.err.println(name + ": " + StringUtils.join(dependencies, ','));
        });
        System.err.println("\n\nReverse dependencies:\n");
        Map<String, Set<String>> reverseBeanDependencies = getReverseBeanDependencies();
        reverseBeanDependencies.forEach((name, dependencies) -> {
            System.err.println(name + ": " + StringUtils.join(dependencies, ','));
        });

        System.err.println("\n\nBean dependency graph:\n");
        System.err.println(getBeanGraph());
        System.err.println("Bean layers:\n");

        getBeanGraph().getLayers().forEach((layer, classes) -> {
            System.err.println("" + layer + "\t" + StringUtils.join(classes, ','));
        });
    }

    public String getCircularDependencyStatisticJson() {

        Map<String, Set<String>> beanDependencies = getBeanDependencies();
        LinkedHashMap<String, BeanDependency> map = new LinkedHashMap<>();
        beanDependencies.forEach((name, dependencies) -> {
            Set<String> circularDependencyDescriptions = new HashSet<>();
            findCycleDependencies(circularDependencyDescriptions, beanDependencies, dependencies, new LinkedHashSet<>(), name, 0, 4);
            map.put(name, new BeanDependency(dependencies.size(),
                    new ArrayList<>(dependencies),
                    circularDependencyDescriptions.size(),
                    new ArrayList<>(circularDependencyDescriptions)));
        });
        int count = 0;
        for (Map.Entry<String, BeanDependency> stringBeanDependencyEntry : map.entrySet()) {
            count = count + stringBeanDependencyEntry.getValue().getCircularDependencyCount();
        }
        LinkedHashMap<String, BeanDependency> collect = map.entrySet().stream().sorted((o1, o2) -> {
            BeanDependency value1 = o1.getValue();
            BeanDependency value2 = o2.getValue();
            int i = value2.getCircularDependencyCount().compareTo(value1.getCircularDependencyCount());
            return i == 0 ? o1.getKey().compareTo(o2.getKey()) : i;
        }).collect(toMap(Map.Entry::getKey, Map.Entry::getValue, (e1, e2) -> e2, LinkedHashMap::new));

        BeanDependencyStatistic beanDependencyStatistic = new BeanDependencyStatistic();
        beanDependencyStatistic.setCreateDate(new Date());
        beanDependencyStatistic.setDependencyMap(collect);
        beanDependencyStatistic.setAllBeanCircularDependencyCount(count);
        Gson gson = new GsonBuilder().setPrettyPrinting().create();
        String result = gson.toJson(beanDependencyStatistic);
        return result;
    }


    private void findCycleDependencies(Set<String> circularDependencyDescriptions, Map<String, Set<String>> allBeansDependenciesMap, Set<String> dependecies, Set<String> dependencyNameChain, String targetName, int currentDepth, int maxDepth) {
        currentDepth++;
        int stepDepthValue = currentDepth;
        for (String dep : dependecies) {
            Set<String> currentStepNameChain = new LinkedHashSet<>(dependencyNameChain);
            Set<String> dependeciesOfBeanDep = allBeansDependenciesMap.get(dep);
            if (dependeciesOfBeanDep == null || dependeciesOfBeanDep.isEmpty()) {
                currentStepNameChain.remove(dep);
                continue;
            }
            if (dependeciesOfBeanDep.contains(targetName) && !currentStepNameChain.contains(targetName)) {
                StringBuilder sb = new StringBuilder(targetName);
                if (!currentStepNameChain.isEmpty()) sb.append('-').append(StringUtils.join(currentStepNameChain, '-'));
                sb.append('-').append(dep).append('-').append(targetName);
                circularDependencyDescriptions.add(sb.toString());
            }
            if (stepDepthValue + 1 <= maxDepth) {
                currentStepNameChain.add(dep);
                findCycleDependencies(circularDependencyDescriptions, allBeansDependenciesMap, dependeciesOfBeanDep, currentStepNameChain, targetName, stepDepthValue, maxDepth);
            }

        }
    }
}