package com.onsemi.middleware.junit.runtime.sb;

import java.io.File;
import java.io.IOException;
import java.net.URL;
import java.nio.file.Files;
import java.nio.file.Paths;
import java.util.AbstractMap;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collection;
import java.util.List;
import java.util.Map;
import java.util.Optional;
import java.util.Spliterator;
import java.util.Spliterators;
import java.util.stream.Collectors;
import java.util.stream.Stream;
import java.util.stream.StreamSupport;

import org.apache.commons.lang3.StringUtils;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.BeansException;
import org.springframework.beans.factory.config.BeanDefinition;
import org.springframework.beans.factory.config.BeanFactoryPostProcessor;
import org.springframework.beans.factory.config.ConfigurableListableBeanFactory;
import org.springframework.beans.factory.support.BeanDefinitionRegistry;
import org.springframework.context.ApplicationContext;
import org.springframework.context.ApplicationContextAware;
import org.springframework.context.annotation.ConditionContext;
import org.springframework.core.env.Environment;
import org.springframework.core.io.ResourceLoader;
import org.springframework.stereotype.Component;

import com.google.common.reflect.ClassPath;
import com.onsemi.middleware.spring.util.BeanDefinitionClassDeterminator;
import com.onsemi.middleware.ue.execeng.UExecClass;
import com.onsemi.middleware.ue.execeng.annotation.impl.InterfaceOwnContextCondition;
@Component
public class SpringBootContextBeanFactoryPostProcessor  implements BeanFactoryPostProcessor, ApplicationContextAware {

    private ApplicationContext applicationContext;

    private Logger logger = LoggerFactory.getLogger(this.getClass());

    private BeanDefinitionClassDeterminator beanDefinitionClassDeterminator = new BeanDefinitionClassDeterminator();

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


    @Override
    public void postProcessBeanFactory(ConfigurableListableBeanFactory beanFactory) throws BeansException {
        if (isRunningInSpringBoot() && !isInterfaceOwnContext()){
            Optional<BeanDefinition> mainClassBean = Optional.empty();
            Class<?> beanClass = null;
            for (String bean: beanFactory.getBeanDefinitionNames()){
                BeanDefinition beanDefinition = beanFactory.getBeanDefinition(bean);
                try {
                    if (beanDefinition.getBeanClassName() == null) {
                        continue;
                    }
                    beanClass = Class.forName(beanDefinition.getBeanClassName());
                    if (UExecClass.class.isAssignableFrom(beanClass)){
                        mainClassBean = Optional.of(beanDefinition);
                        break;
                    }
                } catch (ClassNotFoundException e) {
                    throw new RuntimeException(e);
                }
            }
            List<String> removedBeanNames = new ArrayList<>();
            if (mainClassBean.isPresent()){
                List<String> mainJarClasses = getMainJarClasses(mainClassBean.get());
                removedBeanNames.addAll(getBeanDefinitionNamesForClasses(mainJarClasses, beanFactory));
            }
            else {
                logger.debug("No Bean of main class present - this is OK if you are not scanning it (newer interfaces). Might be weird for older. You are likely not scanning your main class");
            }
            Collection<String> relatedJarClasses = getRelatedJarClasses();
            removedBeanNames.addAll(getBeanDefinitionNamesForClasses(relatedJarClasses, beanFactory));
            removedBeanNames.stream()
                            .forEach(bean -> ((BeanDefinitionRegistry) beanFactory).removeBeanDefinition(bean));
            logger.info("Removed " + removedBeanNames.size()
                                + " classes from Junit parent SpringBoot ApplicationContext - they are a part of interface spring context. Removed bean names are "
                                + removedBeanNames.stream()
                                                  .collect(Collectors.joining(", ")));

        }
    }

    private Collection<String> getBeanDefinitionNamesForClasses(Collection<String> classesToFindBeanNamesFor, ConfigurableListableBeanFactory beanFactory){
        return Arrays.stream(beanFactory.getBeanDefinitionNames())
                .map(bdN -> new AbstractMap.SimpleEntry<>(bdN, beanDefinitionClassDeterminator.getBeanDefinitionClass(beanFactory, bdN)))
                .filter(pair -> classesToFindBeanNamesFor.contains(pair.getValue().getDeclaredBeanClass()))
                .map(Map.Entry::getKey).collect(Collectors.toSet());
    }

    private Collection<String> getRelatedJarClasses() {
        try {
            MavenPomProcessor mavenPomProcessor = new MavenPomProcessor(applicationContext.getEnvironment().getProperty("ue.jar.excluded"),
                                                                        Optional.ofNullable(applicationContext.getEnvironment().getProperty("ue.junit.gitlabUrl")).orElse(JunitConfiguration.DEFAULT_GITLAB_REPO_URL),
                                                                        Optional.ofNullable(applicationContext.getEnvironment().getProperty("ue.junit.gitlabId")).orElse(JunitConfiguration.DEFAULT_GITLAB_REPO_NAME),
                                                                        applicationContext.getEnvironment().getProperty("ue.junit.gitlabUsername"),
                                                                        applicationContext.getEnvironment().getProperty("ue.junit.gitlabPassword")
                                                                        );
            ClassLoader relatedJarsClassLoader = mavenPomProcessor.getRelatedJarsClassLoader();
            ClassPath cp = ClassPath.from(relatedJarsClassLoader);
            Stream<ClassPath.ClassInfo> iter = StreamSupport.stream(Spliterators.spliteratorUnknownSize(cp.getAllClasses()
                                                                                                          .iterator(),
                                                                                                        Spliterator.ORDERED),
                                                                    false);
           return iter.filter(ci -> !isJdkClass(ci.url()))
                            .filter(ci -> isOnsemiClass(ci))
                              .map(ci -> ci.getName())

                .collect(Collectors.toSet());

        } catch (Exception e) {
            throw new RuntimeException(e);
        }

    }
    public boolean isJdkClass(URL resourceUrl) {
        return StringUtils.containsIgnoreCase(resourceUrl.toString(), "jdk");
    }
    public boolean isOnsemiClass(ClassPath.ClassInfo classInfo) {
        return
                StringUtils.startsWith(classInfo.getName(), "com.onsemi");
    }

    private List<String> getMainJarClasses(BeanDefinition beanDefinitionOfMainClass) {
        try {
            URL resource = Class.forName(beanDefinitionOfMainClass.getBeanClassName())
                                .getResource(Class.forName(beanDefinitionOfMainClass.getBeanClassName())
                                                  .getSimpleName() + ".class");
            String filePathToMainClass = resource.getFile();

            File currFile = new File(filePathToMainClass);
            while (!currFile.getName()
                            .equals("middleware")) {
                currFile = currFile.getParentFile();
            }
            try {
                List<String> stringClassesInMainJar = Files.walk(Paths.get(currFile.toURI()))
                                                           .filter(Files::isRegularFile)
                                                           .filter(f -> f.toFile()
                                                                         .getName()
                                                                         .endsWith("class"))
                                                           .map(p -> p.toAbsolutePath())
                                                           .map(p -> p.toFile())
                                                           .map(f -> f.getAbsolutePath()
                                                                      .replaceAll("^.*\\\\com\\\\onsemi\\\\middleware",
                                                                                  "com\\\\onsemi\\\\middleware"))
                                                           .map(strClass -> strClass.replace(".class", ""))
                                                           .map(strClass -> strClass.replace("\\", "."))
                                                           .collect(Collectors.toList());
                return stringClassesInMainJar;
            } catch (IOException e) {
                throw new RuntimeException(e);
            }

        } catch (Exception e) {
            throw new RuntimeException("Could not walk and determine main jar classes ", e);
        }
    }


    private boolean isInterfaceOwnContext() {
        return new InterfaceOwnContextCondition().matches(new ConditionContext() {
            @Override
            public BeanDefinitionRegistry getRegistry() {
                return null;
            }

            @Override
            public ConfigurableListableBeanFactory getBeanFactory() {
                return null;
            }

            @Override
            public Environment getEnvironment() {
                return null;
            }

            @Override
            public ResourceLoader getResourceLoader() {
                return applicationContext;
            }

            @Override
            public ClassLoader getClassLoader() {
                return null;
            }
        }, null);
    }

    private boolean isRunningInSpringBoot() {
        String property = System.getProperty(JunitConfiguration.SPRING_JUNIT_BOOT);
        if (property != null){
            return  Boolean.parseBoolean(property);
        }
        return false;
    }
}
